rschema 2.4.0 → 3.0.1.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/README.md +408 -197
 - data/lib/rschema.rb +26 -367
 - data/lib/rschema/dsl.rb +103 -0
 - data/lib/rschema/error.rb +46 -0
 - data/lib/rschema/http_coercer.rb +177 -0
 - data/lib/rschema/options.rb +15 -0
 - data/lib/rschema/result.rb +39 -0
 - data/lib/rschema/schemas/anything.rb +17 -0
 - data/lib/rschema/schemas/boolean.rb +27 -0
 - data/lib/rschema/schemas/enum.rb +31 -0
 - data/lib/rschema/schemas/fixed_hash.rb +118 -0
 - data/lib/rschema/schemas/fixed_length_array.rb +60 -0
 - data/lib/rschema/schemas/maybe.rb +23 -0
 - data/lib/rschema/schemas/pipeline.rb +27 -0
 - data/lib/rschema/schemas/predicate.rb +27 -0
 - data/lib/rschema/schemas/set.rb +56 -0
 - data/lib/rschema/schemas/sum.rb +36 -0
 - data/lib/rschema/schemas/type.rb +27 -0
 - data/lib/rschema/schemas/variable_hash.rb +67 -0
 - data/lib/rschema/schemas/variable_length_array.rb +49 -0
 - data/lib/rschema/version.rb +1 -1
 - metadata +27 -10
 - data/lib/rschema/rails_interop.rb +0 -19
 
| 
         @@ -0,0 +1,46 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module RSchema
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Error
         
     | 
| 
      
 3 
     | 
    
         
            +
                attr_reader :schema, :value, :symbolic_name, :vars
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(schema:, value:, symbolic_name:, vars: nil)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @schema = schema
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @value = value
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @symbolic_name = symbolic_name
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @vars = vars
         
     | 
| 
      
 10 
     | 
    
         
            +
                  freeze
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def to_s(detailed=false)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  if detailed
         
     | 
| 
      
 15 
     | 
    
         
            +
                    <<~EOS
         
     | 
| 
      
 16 
     | 
    
         
            +
                      Error: #{symbolic_name}
         
     | 
| 
      
 17 
     | 
    
         
            +
                      Schema: #{schema.class.name}
         
     | 
| 
      
 18 
     | 
    
         
            +
                      Value: #{value.inspect}
         
     | 
| 
      
 19 
     | 
    
         
            +
                      Vars: #{vars.inspect}
         
     | 
| 
      
 20 
     | 
    
         
            +
                    EOS
         
     | 
| 
      
 21 
     | 
    
         
            +
                  else
         
     | 
| 
      
 22 
     | 
    
         
            +
                    "Error #{schema.class}/#{symbolic_name} for value: #{value.inspect}"
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def to_json
         
     | 
| 
      
 27 
     | 
    
         
            +
                  {
         
     | 
| 
      
 28 
     | 
    
         
            +
                    schema: schema.class.name,
         
     | 
| 
      
 29 
     | 
    
         
            +
                    error: symbolic_name.to_s,
         
     | 
| 
      
 30 
     | 
    
         
            +
                    value: jsonify(value),
         
     | 
| 
      
 31 
     | 
    
         
            +
                    vars: jsonify(vars),
         
     | 
| 
      
 32 
     | 
    
         
            +
                  }
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                private
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  def jsonify(value)
         
     | 
| 
      
 38 
     | 
    
         
            +
                    case value
         
     | 
| 
      
 39 
     | 
    
         
            +
                    when String, Symbol, Numeric, TrueClass, FalseClass, NilClass then value
         
     | 
| 
      
 40 
     | 
    
         
            +
                    when Array then value.map{ |element| jsonify(element) }
         
     | 
| 
      
 41 
     | 
    
         
            +
                    when Hash then value.map{ |k, v| [jsonify(k), jsonify(v)] }.to_h
         
     | 
| 
      
 42 
     | 
    
         
            +
                    else String(value)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,177 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module RSchema
         
     | 
| 
      
 2 
     | 
    
         
            +
              module HTTPCoercer
         
     | 
| 
      
 3 
     | 
    
         
            +
                class CanNotBeWrappedError < StandardError; end
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                def self.wrap(schema)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  coercer_klass = begin
         
     | 
| 
      
 7 
     | 
    
         
            +
                    case schema
         
     | 
| 
      
 8 
     | 
    
         
            +
                    when Schemas::Type then TYPE_COERCERS[schema.type]
         
     | 
| 
      
 9 
     | 
    
         
            +
                    when Schemas::Boolean then BoolCoercer
         
     | 
| 
      
 10 
     | 
    
         
            +
                    when Schemas::FixedHash then FixedHashCoercer
         
     | 
| 
      
 11 
     | 
    
         
            +
                    end
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  wrapped_schema = schema.with_wrapped_subschemas(self)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  coercer_klass ? coercer_klass.new(wrapped_schema) : wrapped_schema
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                class Coercer
         
     | 
| 
      
 19 
     | 
    
         
            +
                  attr_reader :subschema
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  def initialize(subschema)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @subschema = subschema
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def call(value, options=RSchema::Options.default)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    @subschema.call(coerce(value), options)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  rescue CoercionFailedError
         
     | 
| 
      
 28 
     | 
    
         
            +
                    return Result.failure(Error.new(
         
     | 
| 
      
 29 
     | 
    
         
            +
                      schema: self,
         
     | 
| 
      
 30 
     | 
    
         
            +
                      value: value,
         
     | 
| 
      
 31 
     | 
    
         
            +
                      symbolic_name: :coercion_failure,
         
     | 
| 
      
 32 
     | 
    
         
            +
                    ))
         
     | 
| 
      
 33 
     | 
    
         
            +
                  end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                  def with_wrapped_subschemas(wrapper)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    raise CanNotBeWrappedError, <<~EOS
         
     | 
| 
      
 37 
     | 
    
         
            +
                      This schema has already been wrapped by RSchema::HTTPCoercer.
         
     | 
| 
      
 38 
     | 
    
         
            +
                      Wrapping the schema again will most likely result in a schema that
         
     | 
| 
      
 39 
     | 
    
         
            +
                      crashes when it is called.
         
     | 
| 
      
 40 
     | 
    
         
            +
                    EOS
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  def invalid!
         
     | 
| 
      
 44 
     | 
    
         
            +
                    raise CoercionFailedError
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                  class CoercionFailedError < StandardError; end
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                class TimeCoercer < Coercer
         
     | 
| 
      
 51 
     | 
    
         
            +
                  def coerce(value)
         
     | 
| 
      
 52 
     | 
    
         
            +
                    case value
         
     | 
| 
      
 53 
     | 
    
         
            +
                    when Time then value
         
     | 
| 
      
 54 
     | 
    
         
            +
                    when String then Time.iso8601(value) rescue invalid!
         
     | 
| 
      
 55 
     | 
    
         
            +
                    else invalid!
         
     | 
| 
      
 56 
     | 
    
         
            +
                    end
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                class DateCoercer < Coercer
         
     | 
| 
      
 61 
     | 
    
         
            +
                  def coerce(value)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    case value
         
     | 
| 
      
 63 
     | 
    
         
            +
                    when Date then value
         
     | 
| 
      
 64 
     | 
    
         
            +
                    when String then Date.iso8601(value) rescue invalid!
         
     | 
| 
      
 65 
     | 
    
         
            +
                    else invalid!
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
                  end
         
     | 
| 
      
 68 
     | 
    
         
            +
                end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                class SymbolCoercer < Coercer
         
     | 
| 
      
 71 
     | 
    
         
            +
                  def coerce(value)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    case value
         
     | 
| 
      
 73 
     | 
    
         
            +
                    when Symbol then value
         
     | 
| 
      
 74 
     | 
    
         
            +
                    when String then value.to_sym
         
     | 
| 
      
 75 
     | 
    
         
            +
                    else invalid!
         
     | 
| 
      
 76 
     | 
    
         
            +
                    end
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
                end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                class IntegerCoercer < Coercer
         
     | 
| 
      
 81 
     | 
    
         
            +
                  def coerce(value)
         
     | 
| 
      
 82 
     | 
    
         
            +
                    Integer(value)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  rescue ArgumentError
         
     | 
| 
      
 84 
     | 
    
         
            +
                    invalid!
         
     | 
| 
      
 85 
     | 
    
         
            +
                  end
         
     | 
| 
      
 86 
     | 
    
         
            +
                end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                class FloatCoercer < Coercer
         
     | 
| 
      
 89 
     | 
    
         
            +
                  def coerce(value)
         
     | 
| 
      
 90 
     | 
    
         
            +
                    Float(value)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  rescue ArgumentError
         
     | 
| 
      
 92 
     | 
    
         
            +
                    invalid!
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                class BoolCoercer < Coercer
         
     | 
| 
      
 97 
     | 
    
         
            +
                  TRUTHY_STRINGS = ['on', '1', 'true']
         
     | 
| 
      
 98 
     | 
    
         
            +
                  FALSEY_STRINGS = ['off', '0', 'false']
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                  def coerce(value)
         
     | 
| 
      
 101 
     | 
    
         
            +
                    case value
         
     | 
| 
      
 102 
     | 
    
         
            +
                    when true, false then value
         
     | 
| 
      
 103 
     | 
    
         
            +
                    when nil then false
         
     | 
| 
      
 104 
     | 
    
         
            +
                    when String
         
     | 
| 
      
 105 
     | 
    
         
            +
                      case
         
     | 
| 
      
 106 
     | 
    
         
            +
                      when TRUTHY_STRINGS.include?(value.downcase) then true
         
     | 
| 
      
 107 
     | 
    
         
            +
                      when FALSEY_STRINGS.include?(value.downcase) then false
         
     | 
| 
      
 108 
     | 
    
         
            +
                      else invalid!
         
     | 
| 
      
 109 
     | 
    
         
            +
                      end
         
     | 
| 
      
 110 
     | 
    
         
            +
                    else invalid!
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
      
 112 
     | 
    
         
            +
                  end
         
     | 
| 
      
 113 
     | 
    
         
            +
                end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                class FixedHashCoercer < Coercer
         
     | 
| 
      
 116 
     | 
    
         
            +
                  def coerce(value)
         
     | 
| 
      
 117 
     | 
    
         
            +
                    default_bools_to_false(symbolize_keys(value))
         
     | 
| 
      
 118 
     | 
    
         
            +
                  end
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                  def symbolize_keys(hash)
         
     | 
| 
      
 121 
     | 
    
         
            +
                    keys = keys_to_symbolize(hash)
         
     | 
| 
      
 122 
     | 
    
         
            +
                    if keys.any?
         
     | 
| 
      
 123 
     | 
    
         
            +
                      hash.dup.tap do |new_hash|
         
     | 
| 
      
 124 
     | 
    
         
            +
                        keys.each { |k| new_hash[k.to_sym] = new_hash.delete(k) }
         
     | 
| 
      
 125 
     | 
    
         
            +
                      end
         
     | 
| 
      
 126 
     | 
    
         
            +
                    else
         
     | 
| 
      
 127 
     | 
    
         
            +
                      hash
         
     | 
| 
      
 128 
     | 
    
         
            +
                    end
         
     | 
| 
      
 129 
     | 
    
         
            +
                  end
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                  def keys_to_symbolize(hash)
         
     | 
| 
      
 132 
     | 
    
         
            +
                    # these could be cached if we know for sure that the subschema is immutable
         
     | 
| 
      
 133 
     | 
    
         
            +
                    symbol_keys = subschema.attributes
         
     | 
| 
      
 134 
     | 
    
         
            +
                      .map(&:key)
         
     | 
| 
      
 135 
     | 
    
         
            +
                      .select{ |k| k.is_a?(Symbol) }
         
     | 
| 
      
 136 
     | 
    
         
            +
                      .map(&:to_s)
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                    string_keys = subschema.attributes
         
     | 
| 
      
 139 
     | 
    
         
            +
                      .map(&:key)
         
     | 
| 
      
 140 
     | 
    
         
            +
                      .select{ |k| k.is_a?(String) }
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                    hash.keys.select do |k|
         
     | 
| 
      
 143 
     | 
    
         
            +
                      k.is_a?(String) && symbol_keys.include?(k) && !string_keys.include?(k)
         
     | 
| 
      
 144 
     | 
    
         
            +
                    end
         
     | 
| 
      
 145 
     | 
    
         
            +
                  end
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                  def default_bools_to_false(hash)
         
     | 
| 
      
 148 
     | 
    
         
            +
                    # The HTTP standard says that when a form is submitted, all unchecked
         
     | 
| 
      
 149 
     | 
    
         
            +
                    # check boxes will _not_ be sent to the server. That is, they will not
         
     | 
| 
      
 150 
     | 
    
         
            +
                    # be present at all in the params hash.
         
     | 
| 
      
 151 
     | 
    
         
            +
                    #
         
     | 
| 
      
 152 
     | 
    
         
            +
                    # This method coerces these missing values into `false`.
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
                    # some of this could be cached if we know for sure that the subschema is immutable
         
     | 
| 
      
 155 
     | 
    
         
            +
                    keys_to_default = subschema.attributes
         
     | 
| 
      
 156 
     | 
    
         
            +
                      .select { |attr| attr.value_schema.is_a?(BoolCoercer) }
         
     | 
| 
      
 157 
     | 
    
         
            +
                      .map(&:key)
         
     | 
| 
      
 158 
     | 
    
         
            +
                      .reject { |key| hash.has_key?(key) }
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                    if keys_to_default.any?
         
     | 
| 
      
 161 
     | 
    
         
            +
                      defaults = keys_to_default.map{ |k| [k, false] }.to_h
         
     | 
| 
      
 162 
     | 
    
         
            +
                      hash.merge(defaults)
         
     | 
| 
      
 163 
     | 
    
         
            +
                    else
         
     | 
| 
      
 164 
     | 
    
         
            +
                      hash # no coercion necessary
         
     | 
| 
      
 165 
     | 
    
         
            +
                    end
         
     | 
| 
      
 166 
     | 
    
         
            +
                  end
         
     | 
| 
      
 167 
     | 
    
         
            +
                end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                TYPE_COERCERS = {
         
     | 
| 
      
 170 
     | 
    
         
            +
                  Symbol => SymbolCoercer,
         
     | 
| 
      
 171 
     | 
    
         
            +
                  Integer => IntegerCoercer,
         
     | 
| 
      
 172 
     | 
    
         
            +
                  Float => FloatCoercer,
         
     | 
| 
      
 173 
     | 
    
         
            +
                  Time => TimeCoercer,
         
     | 
| 
      
 174 
     | 
    
         
            +
                  Date => DateCoercer,
         
     | 
| 
      
 175 
     | 
    
         
            +
                }
         
     | 
| 
      
 176 
     | 
    
         
            +
              end
         
     | 
| 
      
 177 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module RSchema
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Result
         
     | 
| 
      
 3 
     | 
    
         
            +
                def self.success(value)
         
     | 
| 
      
 4 
     | 
    
         
            +
                  new(true, value, nil)
         
     | 
| 
      
 5 
     | 
    
         
            +
                end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def self.failure(error)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  new(false, nil, error)
         
     | 
| 
      
 9 
     | 
    
         
            +
                end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                def initialize(valid, value, error)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @valid = valid
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @value = value
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @error = error
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def valid?
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @valid
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def invalid?
         
     | 
| 
      
 22 
     | 
    
         
            +
                  not valid?
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                def value
         
     | 
| 
      
 26 
     | 
    
         
            +
                  if valid?
         
     | 
| 
      
 27 
     | 
    
         
            +
                    @value
         
     | 
| 
      
 28 
     | 
    
         
            +
                  else
         
     | 
| 
      
 29 
     | 
    
         
            +
                    raise InvalidError
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def error
         
     | 
| 
      
 34 
     | 
    
         
            +
                  @error
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                class InvalidError < StandardError; end
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,17 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module RSchema
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Schemas
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Anything
         
     | 
| 
      
 4 
     | 
    
         
            +
                  def self.instance
         
     | 
| 
      
 5 
     | 
    
         
            +
                    @instance ||= new
         
     | 
| 
      
 6 
     | 
    
         
            +
                  end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def call(value, options=Options.default)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    Result.success(value)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  def with_wrapped_subschemas(wrapper)
         
     | 
| 
      
 13 
     | 
    
         
            +
                    self
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,27 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module RSchema
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Schemas
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                class Boolean
         
     | 
| 
      
 5 
     | 
    
         
            +
                  def self.instance
         
     | 
| 
      
 6 
     | 
    
         
            +
                    @instance ||= new
         
     | 
| 
      
 7 
     | 
    
         
            +
                  end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  def call(value, options=Options.default)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    if value.equal?(true) || value.equal?(false)
         
     | 
| 
      
 11 
     | 
    
         
            +
                      Result.success(value)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    else
         
     | 
| 
      
 13 
     | 
    
         
            +
                      Result.failure(Error.new(
         
     | 
| 
      
 14 
     | 
    
         
            +
                        schema: self,
         
     | 
| 
      
 15 
     | 
    
         
            +
                        value: value,
         
     | 
| 
      
 16 
     | 
    
         
            +
                        symbolic_name: :not_a_boolean,
         
     | 
| 
      
 17 
     | 
    
         
            +
                      ))
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  def with_wrapped_subschemas(wrapper)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    self
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,31 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module RSchema
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Schemas
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Enum
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_reader :members, :subschema
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  def initialize(members, subschema)
         
     | 
| 
      
 7 
     | 
    
         
            +
                    @members = members
         
     | 
| 
      
 8 
     | 
    
         
            +
                    @subschema = subschema
         
     | 
| 
      
 9 
     | 
    
         
            +
                  end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def call(value, options=Options.default)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    subresult = subschema.call(value, options)
         
     | 
| 
      
 13 
     | 
    
         
            +
                    if subresult.invalid?
         
     | 
| 
      
 14 
     | 
    
         
            +
                      subresult
         
     | 
| 
      
 15 
     | 
    
         
            +
                    elsif members.include?(subresult.value)
         
     | 
| 
      
 16 
     | 
    
         
            +
                      subresult
         
     | 
| 
      
 17 
     | 
    
         
            +
                    else
         
     | 
| 
      
 18 
     | 
    
         
            +
                      Result.failure(Error.new(
         
     | 
| 
      
 19 
     | 
    
         
            +
                        schema: self,
         
     | 
| 
      
 20 
     | 
    
         
            +
                        value: subresult.value,
         
     | 
| 
      
 21 
     | 
    
         
            +
                        symbolic_name: :not_a_member,
         
     | 
| 
      
 22 
     | 
    
         
            +
                      ))
         
     | 
| 
      
 23 
     | 
    
         
            +
                    end
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  def with_wrapped_subschemas(wrapper)
         
     | 
| 
      
 27 
     | 
    
         
            +
                    self.class.new(members, wrapper.wrap(subschema))
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,118 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module RSchema
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Schemas
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                class FixedHash
         
     | 
| 
      
 5 
     | 
    
         
            +
                  attr_reader :attributes
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                  def initialize(attributes)
         
     | 
| 
      
 8 
     | 
    
         
            +
                    @attributes = attributes
         
     | 
| 
      
 9 
     | 
    
         
            +
                  end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def call(value, options=Options.default)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    return not_a_hash_result(value) unless value.is_a?(Hash)
         
     | 
| 
      
 13 
     | 
    
         
            +
                    return missing_attrs_result(value) if missing_keys(value).any?
         
     | 
| 
      
 14 
     | 
    
         
            +
                    return extraneous_attrs_result(value) if extraneous_keys(value).any?
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    subresults = attr_subresults(value, options)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    if subresults.values.any?(&:invalid?)
         
     | 
| 
      
 18 
     | 
    
         
            +
                      Result.failure(failure_error(subresults))
         
     | 
| 
      
 19 
     | 
    
         
            +
                    else
         
     | 
| 
      
 20 
     | 
    
         
            +
                      Result.success(success_value(subresults))
         
     | 
| 
      
 21 
     | 
    
         
            +
                    end
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def with_wrapped_subschemas(wrapper)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    wrapped_attributes = attributes.map do |attr|
         
     | 
| 
      
 26 
     | 
    
         
            +
                      attr.with_wrapped_value_schema(wrapper)
         
     | 
| 
      
 27 
     | 
    
         
            +
                    end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    self.class.new(wrapped_attributes)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  def [](attr_key)
         
     | 
| 
      
 33 
     | 
    
         
            +
                    attributes.find{ |attr| attr.key == attr_key }
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  Attribute = Struct.new(:key, :value_schema, :optional) do
         
     | 
| 
      
 37 
     | 
    
         
            +
                    def with_wrapped_value_schema(wrapper)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      self.class.new(key, wrapper.wrap(value_schema), optional)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  private
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  def missing_keys(value)
         
     | 
| 
      
 45 
     | 
    
         
            +
                    attributes
         
     | 
| 
      
 46 
     | 
    
         
            +
                      .reject(&:optional)
         
     | 
| 
      
 47 
     | 
    
         
            +
                      .map(&:key)
         
     | 
| 
      
 48 
     | 
    
         
            +
                      .reject{ |k| value.has_key?(k) }
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                  def missing_attrs_result(value)
         
     | 
| 
      
 52 
     | 
    
         
            +
                    Result.failure(Error.new(
         
     | 
| 
      
 53 
     | 
    
         
            +
                      schema: self,
         
     | 
| 
      
 54 
     | 
    
         
            +
                      value: value,
         
     | 
| 
      
 55 
     | 
    
         
            +
                      symbolic_name: :missing_attributes,
         
     | 
| 
      
 56 
     | 
    
         
            +
                      vars: missing_keys(value),
         
     | 
| 
      
 57 
     | 
    
         
            +
                    ))
         
     | 
| 
      
 58 
     | 
    
         
            +
                  end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                  def extraneous_keys(value)
         
     | 
| 
      
 61 
     | 
    
         
            +
                    allowed_keys = attributes.map(&:key)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    value.keys.reject{ |k| allowed_keys.include?(k) }
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  def extraneous_attrs_result(value)
         
     | 
| 
      
 66 
     | 
    
         
            +
                    Result.failure(Error.new(
         
     | 
| 
      
 67 
     | 
    
         
            +
                      schema: self,
         
     | 
| 
      
 68 
     | 
    
         
            +
                      value: value,
         
     | 
| 
      
 69 
     | 
    
         
            +
                      symbolic_name: :extraneous_attributes,
         
     | 
| 
      
 70 
     | 
    
         
            +
                      vars: extraneous_keys(value),
         
     | 
| 
      
 71 
     | 
    
         
            +
                    ))
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                  def attr_subresults(value, options)
         
     | 
| 
      
 75 
     | 
    
         
            +
                    subresults_by_key = {}
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                    @attributes.map do |attr|
         
     | 
| 
      
 78 
     | 
    
         
            +
                      if value.has_key?(attr.key)
         
     | 
| 
      
 79 
     | 
    
         
            +
                        subresult = attr.value_schema.call(value[attr.key], options)
         
     | 
| 
      
 80 
     | 
    
         
            +
                        subresults_by_key[attr.key] = subresult
         
     | 
| 
      
 81 
     | 
    
         
            +
                        break if subresult.invalid? && options.fail_fast?
         
     | 
| 
      
 82 
     | 
    
         
            +
                      end
         
     | 
| 
      
 83 
     | 
    
         
            +
                    end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                    subresults_by_key
         
     | 
| 
      
 86 
     | 
    
         
            +
                  end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                  def failure_error(results)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    error = {}
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                    results.each do |key, attr_result|
         
     | 
| 
      
 92 
     | 
    
         
            +
                      if attr_result.invalid?
         
     | 
| 
      
 93 
     | 
    
         
            +
                        error[key] = attr_result.error
         
     | 
| 
      
 94 
     | 
    
         
            +
                      end
         
     | 
| 
      
 95 
     | 
    
         
            +
                    end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                    error
         
     | 
| 
      
 98 
     | 
    
         
            +
                  end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                  def success_value(subresults)
         
     | 
| 
      
 101 
     | 
    
         
            +
                    subresults
         
     | 
| 
      
 102 
     | 
    
         
            +
                      .map{ |key, attr_result| [key, attr_result.value] }
         
     | 
| 
      
 103 
     | 
    
         
            +
                      .to_h
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                  def not_a_hash_result(value)
         
     | 
| 
      
 107 
     | 
    
         
            +
                    Result.failure(
         
     | 
| 
      
 108 
     | 
    
         
            +
                      Error.new(
         
     | 
| 
      
 109 
     | 
    
         
            +
                        schema: self,
         
     | 
| 
      
 110 
     | 
    
         
            +
                        value: value,
         
     | 
| 
      
 111 
     | 
    
         
            +
                        symbolic_name: :not_a_hash,
         
     | 
| 
      
 112 
     | 
    
         
            +
                      )
         
     | 
| 
      
 113 
     | 
    
         
            +
                    )
         
     | 
| 
      
 114 
     | 
    
         
            +
                  end
         
     | 
| 
      
 115 
     | 
    
         
            +
                end
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
              end
         
     | 
| 
      
 118 
     | 
    
         
            +
            end
         
     |