From 28c689938484668719af2edfc01655ff2bae008b Mon Sep 17 00:00:00 2001
From: Julien Reichardt <mi@jrei.ch>
Date: Sat, 21 Jul 2018 18:53:09 +0200
Subject: [PATCH] Create an abstract Any for JSON/YAML

---
 spec/std/json/any_spec.cr |   3 -
 src/any.cr                | 160 ++++++++++++++++++++++++++++++++++++
 src/json/any.cr           | 169 ++------------------------------------
 src/yaml.cr               |   6 +-
 src/yaml/any.cr           | 154 ++--------------------------------
 5 files changed, 175 insertions(+), 317 deletions(-)
 create mode 100644 src/any.cr

diff --git a/spec/std/json/any_spec.cr b/spec/std/json/any_spec.cr
index ec033b62c62e..b93cd9f29a31 100644
--- a/spec/std/json/any_spec.cr
+++ b/spec/std/json/any_spec.cr
@@ -26,11 +26,8 @@ describe JSON::Any do
 
     it "gets float" do
       JSON.parse("123.45").as_f.should eq(123.45)
-      JSON.parse("123.45").as_f32.should eq(123.45_f32)
       JSON.parse("123.45").as_f?.should eq(123.45)
-      JSON.parse("123.45").as_f32?.should eq(123.45_f32)
       JSON.parse("true").as_f?.should be_nil
-      JSON.parse("true").as_f32?.should be_nil
     end
 
     it "gets string" do
diff --git a/src/any.cr b/src/any.cr
new file mode 100644
index 000000000000..af924a30150b
--- /dev/null
+++ b/src/any.cr
@@ -0,0 +1,160 @@
+abstract struct Any
+  # 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
+
+  # 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?
+    @raw.as?(Bool)
+  end
+
+  # Checks that the underlying value is `Float64`, and returns its value.
+  # Raises otherwise.
+  def as_f : Float64
+    @raw.as(Float64)
+  end
+
+  # Checks that the underlying value is `Float64`, and returns its value.
+  # Returns `nil` otherwise.
+  def as_f? : Float64?
+    @raw.as?(Float64)
+  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 `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 as `Int32`.
+  # Raises otherwise.
+  def as_i : Int32
+    as_i64.to_i
+  end
+
+  # Checks that the underlying value is `Int64`, and returns its value as `Int32`.
+  # Returns `nil` otherwise.
+  def as_i? : Int32?
+    as_i64?.try &.to_i
+  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 `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
+
+  # :nodoc:
+  def inspect(io)
+    @raw.inspect(io)
+  end
+
+  # :nodoc:
+  def to_s(io)
+    @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 : 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
+end
+
+macro any_classes(type)
+  class Object
+    def ===(other : {{type.id}}::Any)
+      self === other.raw
+    end
+  end
+
+  struct Value
+    def ==(other : {{type.id}}::Any)
+      self == other.raw
+    end
+  end
+
+  class Reference
+    def ==(other : {{type.id}}::Any)
+      self == other.raw
+    end
+  end
+
+  class Array
+    def ==(other : {{type.id}}::Any)
+      self == other.raw
+    end
+  end
+
+  class Hash
+    def ==(other : {{type.id}}::Any)
+      self == other.raw
+    end
+  end
+
+  class Regex
+    def ===(other : {{type.id}}::Any)
+      value = self === other.raw
+      $~ = $~
+      value
+    end
+  end
+end
diff --git a/src/json/any.cr b/src/json/any.cr
index 5048b6351acc..86b24e835d5c 100644
--- a/src/json/any.cr
+++ b/src/json/any.cr
@@ -1,3 +1,5 @@
+require "../any"
+
 # `JSON::Any` is a convenient wrapper around all possible JSON types (`JSON::Any::Type`)
 # and can be used for traversing dynamic or unknown JSON structures.
 #
@@ -13,7 +15,7 @@
 # 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 JSON::Any
+struct JSON::Any < ::Any
   # All possible JSON types.
   alias Type = Nil | Bool | Int64 | Float64 | String | Array(Any) | Hash(String, Any)
 
@@ -54,19 +56,6 @@ struct JSON::Any
   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`.
@@ -115,84 +104,6 @@ struct JSON::Any
     end
   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(Float).to_f
-  end
-
-  # Checks that the underlying value is `Float`, and returns its value as an `Float64`.
-  # Returns `nil` otherwise.
-  def as_f? : Float64?
-    as_f if @raw.is_a?(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?(Float32) || @raw.is_a?(Float64))
-  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)
@@ -202,7 +113,7 @@ struct JSON::Any
   # 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)
+    @raw.as?(Array)
   end
 
   # Checks that the underlying value is `Hash`, and returns its value.
@@ -214,87 +125,23 @@ struct JSON::Any
   # 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)
-    @raw.inspect(io)
+    @raw.as?(Hash)
   end
 
-  # :nodoc:
-  def to_s(io)
-    @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 : JSON::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
 
-  # Returns a new JSON::Any instance with the `raw` value `dup`ed.
+  # Returns a new Any instance with the `raw` value `dup`ed.
   def dup
     Any.new(raw.dup)
   end
 
-  # Returns a new JSON::Any instance with the `raw` value `clone`ed.
+  # Returns a new Any instance with the `raw` value `clone`ed.
   def clone
     Any.new(raw.clone)
   end
 end
 
-class Object
-  def ===(other : JSON::Any)
-    self === other.raw
-  end
-end
-
-struct Value
-  def ==(other : JSON::Any)
-    self == other.raw
-  end
-end
-
-class Reference
-  def ==(other : JSON::Any)
-    self == other.raw
-  end
-end
-
-class Array
-  def ==(other : JSON::Any)
-    self == other.raw
-  end
-end
-
-class Hash
-  def ==(other : JSON::Any)
-    self == other.raw
-  end
-end
-
-class Regex
-  def ===(other : JSON::Any)
-    value = self === other.raw
-    $~ = $~
-    value
-  end
-end
+any_classes "JSON"
diff --git a/src/yaml.cr b/src/yaml.cr
index f4d51d85cc45..86afc3dd85b6 100644
--- a/src/yaml.cr
+++ b/src/yaml.cr
@@ -1,8 +1,4 @@
-require "./yaml/*"
-require "./yaml/schema/*"
-require "./yaml/schema/core/*"
-require "./yaml/nodes/*"
-
+require "./yaml/**"
 require "base64"
 
 # The YAML module provides serialization and deserialization of YAML
diff --git a/src/yaml/any.cr b/src/yaml/any.cr
index 97def02a7857..dd520bb2752b 100644
--- a/src/yaml/any.cr
+++ b/src/yaml/any.cr
@@ -1,3 +1,5 @@
+require "../any"
+
 # `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.
@@ -23,7 +25,7 @@
 # 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 YAML::Any
+struct YAML::Any < ::Any
   # All valid YAML core schema types.
   alias Type = Nil | Bool | Int64 | Float64 | String | Time | Bytes | Array(Any) | Hash(Any, Any) | Set(Any)
 
@@ -74,20 +76,6 @@ struct YAML::Any
   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` or `Hash`
   # and returns the element at the given *index_or_key*.
   #
@@ -126,60 +114,6 @@ struct YAML::Any
     end
   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 `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?
-    @raw.as?(Int64).try &.to_i
-  end
-
-  # Checks that the underlying value is `Float64`, and returns its value.
-  # Raises otherwise.
-  def as_f : Float64
-    @raw.as(Float64)
-  end
-
-  # Checks that the underlying value is `Float64`, and returns its value.
-  # Returns `nil` otherwise.
-  def as_f? : Float64?
-    @raw.as?(Float64)
-  end
-
   # Checks that the underlying value is `Time`, and returns its value.
   # Raises otherwise.
   def as_time : Time
@@ -216,96 +150,20 @@ struct 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
-
-  # :nodoc:
-  def inspect(io)
-    @raw.inspect(io)
-  end
-
-  # :nodoc:
-  def to_s(io)
-    @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)
     raw.to_yaml(io)
   end
 
-  # Returns a new YAML::Any instance with the `raw` value `dup`ed.
+  # Returns a new Any instance with the `raw` value `dup`ed.
   def dup
     Any.new(raw.dup)
   end
 
-  # Returns a new YAML::Any instance with the `raw` value `clone`ed.
+  # Returns a new Any instance with the `raw` value `clone`ed.
   def clone
     Any.new(raw.clone)
   end
 end
 
-class Object
-  def ===(other : YAML::Any)
-    self === other.raw
-  end
-end
-
-struct Value
-  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
+any_classes "YAML"