# `YAML::Any` is a convenient wrapper around all possible YAML core types # (`YAML::Any::Type`) and can be used for traversing dynamic or # unknown YAML structures. # # ``` # require "yaml" # # data = YAML.parse <<-YAML # --- # foo: # bar: # baz: # - qux # - fox # YAML # data["foo"]["bar"]["baz"][0].as_s # => "qux" # data["foo"]["bar"]["baz"].as_a # => ["qux", "fox"] # ``` # # Note that methods used to traverse a YAML structure, `#[]`, `#[]?` and `#each`, # always return a `YAML::Any` to allow further traversal. To convert them to `String`, # `Array`, etc., use the `as_` methods, such as `#as_s`, `#as_a`, 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`. There are also nil-able variants (`#as_i?`, `#as_s?`, ...), # which return `nil` when the underlying value type won't match. struct YAML::Any # All valid YAML core schema types. alias Type = Nil | Bool | Int64 | Float64 | String | Time | Bytes | Array(YAML::Any) | Hash(YAML::Any, YAML::Any) | Set(YAML::Any) def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) case node when YAML::Nodes::Scalar new YAML::Schema::Core.parse_scalar(node) when YAML::Nodes::Sequence new Array(self).new(ctx, node) when YAML::Nodes::Mapping new Hash(self, self).new(ctx, node) when YAML::Nodes::Alias if value = node.value new(ctx, value) else raise "YAML::Nodes::Alias misses anchor value" end else raise "Unknown node: #{node.class}" end end # Returns the raw underlying value, a `Type`. getter raw : Type # Creates a `YAML::Any` that wraps the given `Type`. def initialize(@raw : Type) end # :ditto: def self.new(raw : Int) # FIXME: Workaround for https://github.com/crystal-lang/crystal/issues/11645 new(raw.to_i64) end # :ditto: def self.new(raw : Float) # FIXME: Workaround for https://github.com/crystal-lang/crystal/issues/11645 new(raw.to_f64) 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` or `Hash` # and returns the element at the given *index_or_key*. # # Raises if the underlying value is not an `Array` nor a `Hash`. def [](index_or_key) : YAML::Any case object = @raw when Array if index_or_key.is_a?(Int) object[index_or_key] else raise "Expected int key for Array#[], not #{object.class}" end when Hash object[index_or_key] else raise "Expected Array or Hash, not #{object.class}" end end # Assumes the underlying value is an `Array` or `Hash` and returns the element # at the given *index_or_key*, or `nil` if out of bounds or the key is missing. # # Raises if the underlying value is not an `Array` nor a `Hash`. def []?(index_or_key) : YAML::Any? case object = @raw when Array if index_or_key.is_a?(Int) object[index_or_key]? else nil end when Hash object[index_or_key]? else raise "Expected Array or Hash, not #{object.class}" end end # Traverses the depth of a structure and returns the value. # Returns `nil` if not found. def dig?(index_or_key, *subkeys) : YAML::Any? self[index_or_key]?.try &.dig?(*subkeys) end # :nodoc: def dig?(index_or_key) : YAML::Any? case @raw when Hash, Array self[index_or_key]? else nil end end # Traverses the depth of a structure and returns the value, otherwise raises. def dig(index_or_key, *subkeys) : YAML::Any self[index_or_key].dig(*subkeys) end # :nodoc: def dig(index_or_key) : YAML::Any self[index_or_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 `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? @raw.as?(String) end # Checks that the underlying value is `Int64`, and returns its value. # Raises otherwise. def as_i64 : Int64 @raw.as(Int64) end # Checks that the underlying value is `Int64`, and returns its value. # Returns `nil` otherwise. def as_i64? : Int64? @raw.as?(Int64) end # Checks that the underlying value is `Int64`, and returns its value as `Int32`. # Raises otherwise. def as_i : Int32 @raw.as(Int64).to_i end # Checks that the underlying value is `Int64`, and returns its value as `Int32`. # Returns `nil` otherwise. def as_i? : Int32? as_i if @raw.is_a?(Int) end # Checks that the underlying value is `Float` (or `Int`), and returns its value. # Raises otherwise. def as_f : Float64 case raw = @raw when Int raw.to_f else raw.as(Float64) end end # Checks that the underlying value is `Float` (or `Int`), and returns its value. # Returns `nil` otherwise. def as_f? : Float64? case raw = @raw when Int raw.to_f else raw.as?(Float64) end end # Checks that the underlying value is `Float` (or `Int`), and returns its value as an `Float32`. # Raises otherwise. def as_f32 : Float32 case raw = @raw when Int raw.to_f32 else raw.as(Float).to_f32 end end # Checks that the underlying value is `Float` (or `Int`), and returns its value as an `Float32`. # Returns `nil` otherwise. def as_f32? : Float32? case raw = @raw when Int raw.to_f32 when Float raw.to_f32 else nil end end # Checks that the underlying value is `Time`, and returns its value. # Raises otherwise. def as_time : Time @raw.as(Time) end # Checks that the underlying value is `Time`, and returns its value. # Returns `nil` otherwise. def as_time? : Time? @raw.as?(Time) end # Checks that the underlying value is `Array`, and returns its value. # Raises otherwise. def as_a : Array(YAML::Any) @raw.as(Array) end # Checks that the underlying value is `Array`, and returns its value. # Returns `nil` otherwise. def as_a? : Array(YAML::Any)? @raw.as?(Array) end # Checks that the underlying value is `Hash`, and returns its value. # Raises otherwise. def as_h : Hash(YAML::Any, YAML::Any) @raw.as(Hash) end # Checks that the underlying value is `Hash`, and returns its value. # Returns `nil` otherwise. def as_h? : Hash(YAML::Any, YAML::Any)? @raw.as?(Hash) end # Checks that the underlying value is `Bytes`, and returns its value. # Raises otherwise. def as_bytes : Bytes @raw.as(Bytes) end # Checks that the underlying value is `Bytes`, and returns its value. # Returns `nil` otherwise. def as_bytes? : Bytes? @raw.as?(Bytes) end def inspect(io : IO) : Nil @raw.inspect(io) end 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 : YAML::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_yaml(io) : Nil raw.to_yaml(io) end def to_json(builder : JSON::Builder) : Nil if (raw = self.raw).is_a?(Slice) raise "Can't serialize #{raw.class} to JSON" else raw.to_json(builder) end end # Returns a new YAML::Any instance with the `raw` value `dup`ed. def dup YAML::Any.new(raw.dup) end # Returns a new YAML::Any instance with the `raw` value `clone`ed. def clone YAML::Any.new(raw.clone) end # Forwards `to_json_object_key` to `raw` if it responds to that method, # raises `JSON::Error` otherwise. def to_json_object_key : String raw = @raw if raw.responds_to?(:to_json_object_key) raw.to_json_object_key else raise JSON::Error.new("Can't convert #{raw.class} to a JSON object key") end end end class Object def ===(other : YAML::Any) self === other.raw end end struct Value def ==(other : YAML::Any) self == other.raw end end struct Struct def ==(other : YAML::Any) self == other.raw end end class Reference def ==(other : YAML::Any) self == other.raw end end class Array def ==(other : YAML::Any) self == other.raw end end class Hash def ==(other : YAML::Any) self == other.raw end end class Regex def ===(other : YAML::Any) value = self === other.raw $~ = $~ value end end