Add CBOR::Any.

remotes/dev/pr-multi
Karchnu 2020-11-23 13:56:55 +01:00
parent 3c82ed329f
commit 0f25c5da9f
2 changed files with 397 additions and 0 deletions

392
src/cbor/any.cr Normal file
View File

@ -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

View File

@ -10,6 +10,11 @@ class CBOR::Encoder
def initialize(@io : IO = IO::Memory.new) def initialize(@io : IO = IO::Memory.new)
end end
def write(cbor : CBOR::Any)
# Test each possible value of CBOR::Any
write cbor.raw
end
def write(j : JSON::Any) def write(j : JSON::Any)
write j.raw write j.raw
end end