From 0f25c5da9fd4bb30df5488ca3491b11c64872e1c Mon Sep 17 00:00:00 2001 From: Karchnu Date: Mon, 23 Nov 2020 13:56:55 +0100 Subject: [PATCH] Add CBOR::Any. --- src/cbor/any.cr | 392 ++++++++++++++++++++++++++++++++++++++++++++ src/cbor/encoder.cr | 5 + 2 files changed, 397 insertions(+) create mode 100644 src/cbor/any.cr diff --git a/src/cbor/any.cr b/src/cbor/any.cr new file mode 100644 index 0000000..f6a551b --- /dev/null +++ b/src/cbor/any.cr @@ -0,0 +1,392 @@ +# `CBOR::Any` is a convenient wrapper around all possible CBOR types (`CBOR::Any::Type`) +# and can be used for traversing dynamic or unknown CBOR structures. +# +# ``` +# require "json" +# +# obj = CBOR::Any.new JSON.parse(%({"access": [{"name": "mapping", "speed": "fast"}, {"name": "any", "speed": "slow"}]})) +# obj["access"][1]["name"].as_s # => "any" +# obj["access"][1]["speed"].as_s # => "slow" +# +# # through a cbor buffer +# hash = {"access" => [{"name" => "mapping", "speed" => "fast"}, {"name" => "any", "speed" => "slow"}]} +# obj2 = CBOR::Any.new hash.to_cbor +# obj2["access"][1]["name"].as_s # => "any" +# obj2["access"][1]["speed"].as_s # => "slow" +# ``` +# +# Note that methods used to traverse a CBOR structure, `#[]` and `#[]?`, +# always return a `CBOR::Any` to allow further traversal. To convert them to `String`, +# `Int32`, etc., use the `as_` methods, such as `#as_s`, `#as_i`, which perform +# a type check against the raw underlying value. This means that invoking `#as_s` +# when the underlying value is not a String will raise: the value won't automatically +# be converted (parsed) to a `String`. +struct CBOR::Any + + # All possible CBOR types. + alias Type = Nil | Bool | String | Bytes | + Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 | Int64 | UInt64 | Int128 | + Float32 | Float64 | + Array(Any) | + Hash(String, Any) | Hash(Any, Any) + + # Reads a `CBOR::Any` from a JSON::Any structure. + def self.new(json : JSON::Any) + new json.raw + end + + def self.new(value) + case value + when Nil + new nil + when Bool + new value + when Int64 + new value + when Float64 + new value + when String + new value + + when Array(Type) + ary = [] of CBOR::Any + value.each do |v| + ary << new(v) + end + new ary + + when Array(JSON::Any) + ary = [] of CBOR::Any + value.each do |v| + ary << new(v) + end + new ary + + when Array(CBOR::Type) + ary = [] of CBOR::Any + value.each do |v| + ary << new(v) + end + new ary + + when Hash(String, JSON::Any) + hash = {} of String => CBOR::Any + value.each do |key, v| + hash[key] = new(v) + end + new hash + + when Hash(CBOR::Type, CBOR::Type) + hash = {} of CBOR::Any => CBOR::Any + value.each do |key, v| + hash[new(key)] = new(v) + end + new hash + + when Hash(String, Type) + hash = {} of String => CBOR::Any + value.each do |key, v| + hash[key] = new(v) + end + new hash + else + raise "Unknown value type: #{value.class}" + end + end + + # Reads a `CBOR::Any` from a Decoder. + def self.new(decoder : CBOR::Decoder) + new decoder.read_value + end + + # Reads a `CBOR::Any` from a buffer. + def self.new(input : Slice(UInt8)) + new CBOR::Decoder.new(input) + end + + # Returns the raw underlying value. + getter raw : Type + + # Creates a `CBOR::Any` that wraps the given value. + def initialize(@raw : Type) + end + + # Assumes the underlying value is an `Array` or `Hash` and returns its size. + # Raises if the underlying value is not an `Array` or `Hash`. + def size : Int + case object = @raw + when Array + object.size + when Hash + object.size + else + raise "Expected Array or Hash for #size, not #{object.class}" + end + end + + # Assumes the underlying value is an `Array` and returns the element + # at the given index. + # Raises if the underlying value is not an `Array`. + def [](index : Int) : CBOR::Any + case object = @raw + when Array + object[index] + else + raise "Expected Array for #[](index : Int), not #{object.class}" + end + end + + # Assumes the underlying value is an `Array` and returns the element + # at the given index, or `nil` if out of bounds. + # Raises if the underlying value is not an `Array`. + def []?(index : Int) : CBOR::Any? + case object = @raw + when Array + object[index]? + else + raise "Expected Array for #[]?(index : Int), not #{object.class}" + end + end + + # Assumes the underlying value is a `Hash` and returns the element + # with the given key. + # Raises if the underlying value is not a `Hash`. + def [](key : String) : CBOR::Any + case object = @raw + when Hash + object[key] + else + raise "Expected Hash for #[](key : String), not #{object.class}" + end + end + + # Assumes the underlying value is a `Hash` and returns the element + # with the given key, or `nil` if the key is not present. + # Raises if the underlying value is not a `Hash`. + def []?(key : String) : CBOR::Any? + case object = @raw + when Hash + object[key]? + else + raise "Expected Hash for #[]?(key : String), not #{object.class}" + end + end + + # Traverses the depth of a structure and returns the value. + # Returns `nil` if not found. + def dig?(key : String | Int, *subkeys) + if value = self[key]? + value.dig?(*subkeys) + end + end + + # :nodoc: + def dig?(key : String | Int) + case @raw + when Hash, Array + self[key]? + else + nil + end + end + + # Traverses the depth of a structure and returns the value, otherwise raises. + def dig(key : String | Int, *subkeys) + if (value = self[key]) && value.responds_to?(:dig) + return value.dig(*subkeys) + end + raise "CBOR::Any value not diggable for key: #{key.inspect}" + end + + # :nodoc: + def dig(key : String | Int) + self[key] + end + + # Checks that the underlying value is `Nil`, and returns `nil`. + # Raises otherwise. + def as_nil : Nil + @raw.as(Nil) + end + + # Checks that the underlying value is `Bool`, and returns its value. + # Raises otherwise. + def as_bool : Bool + @raw.as(Bool) + end + + # Checks that the underlying value is `Bool`, and returns its value. + # Returns `nil` otherwise. + def as_bool? : Bool? + as_bool if @raw.is_a?(Bool) + end + + # Checks that the underlying value is `Int`, and returns its value as an `Int32`. + # Raises otherwise. + def as_i : Int32 + @raw.as(Int).to_i + end + + # Checks that the underlying value is `Int`, and returns its value as an `Int32`. + # Returns `nil` otherwise. + def as_i? : Int32? + as_i if @raw.is_a?(Int) + end + + # Checks that the underlying value is `Int`, and returns its value as an `Int64`. + # Raises otherwise. + def as_i64 : Int64 + @raw.as(Int).to_i64 + end + + # Checks that the underlying value is `Int`, and returns its value as an `Int64`. + # Returns `nil` otherwise. + def as_i64? : Int64? + as_i64 if @raw.is_a?(Int64) + end + + # Checks that the underlying value is `Float`, and returns its value as an `Float64`. + # Raises otherwise. + def as_f : Float64 + @raw.as(Float64) + end + + # Checks that the underlying value is `Float`, and returns its value as an `Float64`. + # Returns `nil` otherwise. + def as_f? : Float64? + @raw.as?(Float64) + end + + # Checks that the underlying value is `Float`, and returns its value as an `Float32`. + # Raises otherwise. + def as_f32 : Float32 + @raw.as(Float).to_f32 + end + + # Checks that the underlying value is `Float`, and returns its value as an `Float32`. + # Returns `nil` otherwise. + def as_f32? : Float32? + as_f32 if @raw.is_a?(Float) + end + + # Checks that the underlying value is `String`, and returns its value. + # Raises otherwise. + def as_s : String + @raw.as(String) + end + + # Checks that the underlying value is `String`, and returns its value. + # Returns `nil` otherwise. + def as_s? : String? + as_s if @raw.is_a?(String) + end + + # Checks that the underlying value is `Array`, and returns its value. + # Raises otherwise. + def as_a : Array(Any) + @raw.as(Array) + end + + # Checks that the underlying value is `Array`, and returns its value. + # Returns `nil` otherwise. + def as_a? : Array(Any)? + as_a if @raw.is_a?(Array) + end + + # Checks that the underlying value is `Hash`, and returns its value. + # Raises otherwise. + def as_h : Hash(String, Any) + @raw.as(Hash) + end + + # Checks that the underlying value is `Hash`, and returns its value. + # Returns `nil` otherwise. + def as_h? : Hash(String, Any)? + as_h if @raw.is_a?(Hash) + end + + # :nodoc: + def inspect(io : IO) : Nil + @raw.inspect(io) + end + + # :nodoc: + def to_s(io : IO) : Nil + @raw.to_s(io) + end + + # :nodoc: + def pretty_print(pp) + @raw.pretty_print(pp) + end + + # Returns `true` if both `self` and *other*'s raw object are equal. + def ==(other : CBOR::Any) + raw == other.raw + end + + # Returns `true` if the raw object is equal to *other*. + def ==(other) + raw == other + end + + # See `Object#hash(hasher)` + def_hash raw + + # :nodoc: + def to_json(json : JSON::Builder) + raw.to_json(json) + end + + def to_yaml(yaml : YAML::Nodes::Builder) + raw.to_yaml(yaml) + end + + # Returns a new CBOR::Any instance with the `raw` value `dup`ed. + def dup + Any.new(raw.dup) + end + + # Returns a new CBOR::Any instance with the `raw` value `clone`ed. + def clone + Any.new(raw.clone) + end +end + +class Object + def ===(other : CBOR::Any) + self === other.raw + end +end + +struct Value + def ==(other : CBOR::Any) + self == other.raw + end +end + +class Reference + def ==(other : CBOR::Any) + self == other.raw + end +end + +class Array + def ==(other : CBOR::Any) + self == other.raw + end +end + +class Hash + def ==(other : CBOR::Any) + self == other.raw + end +end + +class Regex + def ===(other : CBOR::Any) + value = self === other.raw + $~ = $~ + value + end +end diff --git a/src/cbor/encoder.cr b/src/cbor/encoder.cr index 92678b4..b355a59 100644 --- a/src/cbor/encoder.cr +++ b/src/cbor/encoder.cr @@ -10,6 +10,11 @@ class CBOR::Encoder def initialize(@io : IO = IO::Memory.new) end + def write(cbor : CBOR::Any) + # Test each possible value of CBOR::Any + write cbor.raw + end + def write(j : JSON::Any) write j.raw end