dynamicschema 1.0.0 → 2.0.0

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.
@@ -4,11 +4,47 @@ require 'date'
4
4
  require 'uri'
5
5
 
6
6
  module DynamicSchema
7
- module BuilderMethods
8
- module Conversion
7
+ module Converter
8
+ extend self
9
9
 
10
- DEFAULT_CONVERTERS = {
10
+ def register_converter( klass, &block )
11
+ self.converters[ klass ] = block
12
+ end
13
+
14
+ def convert( value, to:, &block )
15
+ return value if value.nil? || to.nil?
16
+
17
+ to = Array( to )
18
+ result = nil
19
+
20
+ if value.respond_to?( :is_a? )
21
+ to.each do | type |
22
+ result = value.is_a?( type ) ? value : nil
23
+ break unless result.nil?
24
+ end
25
+ end
26
+
27
+ if result.nil?
28
+ to.each do | type |
29
+ converter = converters[ type ]
30
+ if converter
31
+ result = converter.call( value ) rescue nil
32
+ break unless result.nil?
33
+ end
34
+ end
35
+ end
36
+
37
+ result.nil? && block ? block.call( value ) : result
38
+ end
11
39
 
40
+ private
41
+
42
+ def converters
43
+ @converters ||= default_converters.dup
44
+ end
45
+
46
+ def default_converters
47
+ {
12
48
  Array => ->( v ) { Array( v ) },
13
49
  Date => ->( v ) { v.respond_to?( :to_date ) ? v.to_date : Date.parse( v.to_s ) },
14
50
  Time => ->( v ) { v.respond_to?( :to_time ) ? v.to_time : Time.parse( v.to_s ) },
@@ -34,21 +70,8 @@ module DynamicSchema
34
70
  v.to_s.match( /\A\s*(false|no)\s*\z/i ) ? false : nil
35
71
  end
36
72
  }
37
-
38
- }
39
-
40
- def initialize
41
- self.converters = DEFAULT_CONVERTERS.dup
42
- end
43
-
44
- def convertor( klass, &block )
45
- self.converters[ klass ] = block
46
- end
47
-
48
- private
49
-
50
- attr_accessor :converters
51
-
73
+ }.freeze
52
74
  end
75
+
53
76
  end
54
77
  end
@@ -0,0 +1,60 @@
1
+ # Intentionally no requires here to avoid circular dependencies.
2
+
3
+ module DynamicSchema
4
+ module Receiver
5
+ class Base < BasicObject
6
+
7
+ def self.const_missing( name )
8
+ ::Object.const_get( name )
9
+ end
10
+
11
+ if defined?( ::PP )
12
+ include ::PP::ObjectMixin
13
+ def pretty_print( pp )
14
+ pp.pp( { values: @values, schema: @schema } )
15
+ end
16
+ end
17
+
18
+
19
+ def evaluate( &block )
20
+ self.instance_eval( &block )
21
+ self
22
+ end
23
+
24
+ def nil?
25
+ false
26
+ end
27
+
28
+ def to_s
29
+ inspect
30
+ end
31
+
32
+ def inspect
33
+ { values: @values, schema: @schema }.inspect
34
+ end
35
+
36
+ private
37
+
38
+ if defined?( ::PP )
39
+ def pp( *args )
40
+ ::PP.pp( *args )
41
+ end
42
+ end
43
+
44
+ %i[ String Integer Float Array Hash Symbol Rational Complex
45
+ raise require puts warn p ].each do | method |
46
+ define_method( method ) { | *args, &block | ::Kernel.public_send( method, *args, &block ) }
47
+ end
48
+
49
+ def fail( *args ) = ::Kernel.raise( *args )
50
+
51
+ def require_relative( path )
52
+ location = ::Kernel.caller_locations( 1, 1 ).first
53
+ base_dir = location&.absolute_path ? ::File.dirname( location.absolute_path ) : ::File.dirname( location.path )
54
+ absolute = ::File.expand_path( path, base_dir )
55
+ ::Kernel.require( absolute )
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -1,14 +1,11 @@
1
- module DynamicSchema
2
- class Receiver < BasicObject
1
+ require_relative 'base'
2
+ require_relative 'value'
3
3
 
4
- if defined?( ::PP )
5
- include ::PP::ObjectMixin
6
- def pretty_print( pp )
7
- pp.pp( { values: @values, schema: @schema } )
8
- end
9
- end
4
+ module DynamicSchema
5
+ module Receiver
6
+ class Object < Base
10
7
 
11
- def initialize( values = nil, schema:, converters: )
8
+ def initialize( values = nil, schema:, converter: )
12
9
  raise ArgumentError, 'The Receiver values must be a nil or a Hash.'\
13
10
  unless values.nil? || ( values.respond_to?( :[] ) && values.respond_to?( :key? ) )
14
11
 
@@ -16,7 +13,7 @@ module DynamicSchema
16
13
  @schema = schema
17
14
  @defaults_assigned = {}
18
15
 
19
- @converters = converters&.dup
16
+ @converter = converter
20
17
 
21
18
  @schema.each do | key, criteria |
22
19
  name = criteria[ :as ] || key
@@ -29,15 +26,6 @@ module DynamicSchema
29
26
 
30
27
  end
31
28
 
32
- def evaluate( &block )
33
- self.instance_eval( &block )
34
- self
35
- end
36
-
37
- def nil?
38
- false
39
- end
40
-
41
29
  def empty?
42
30
  @values.empty?
43
31
  end
@@ -47,7 +35,7 @@ module DynamicSchema
47
35
  case object
48
36
  when ::NilClass
49
37
  nil
50
- when ::DynamicSchema::Receiver
38
+ when ::DynamicSchema::Receiver::Object
51
39
  recursive_to_h.call( object.to_h )
52
40
  when ::Hash
53
41
  object.transform_values { | value | recursive_to_h.call( value ) }
@@ -61,20 +49,14 @@ module DynamicSchema
61
49
  recursive_to_h.call( @values )
62
50
  end
63
51
 
64
- def to_s
65
- inspect
66
- end
67
-
68
- def inspect
69
- { values: @values, schema: @schema }.inspect
70
- end
71
-
72
52
  def class
73
- ::DynamicSchema::Receiver
53
+ ::DynamicSchema::Receiver::Object
74
54
  end
75
55
 
76
56
  def is_a?( klass )
77
- klass == ::DynamicSchema::Receiver || klass == ::BasicObject
57
+ klass == ::DynamicSchema::Receiver::Object ||
58
+ klass == ::DynamicSchema::Receiver::Base ||
59
+ klass == ::BasicObject
78
60
  end
79
61
 
80
62
  alias :kind_of? :is_a?
@@ -89,14 +71,14 @@ module DynamicSchema
89
71
  if criteria[ :type ] == ::Object
90
72
  value = __object( method, args, value: value, criteria: criteria, &block )
91
73
  else
92
- value = __value( method, args, value: value, criteria: criteria )
74
+ value = __value( method, args, value: value, criteria: criteria, &block )
93
75
  end
94
76
  else
95
77
  value = @defaults_assigned[ method ] ? ::Array.new : value || ::Array.new
96
78
  if criteria[ :type ] == ::Object
97
79
  value = __object_array( method, args, value: value, criteria: criteria, &block )
98
80
  else
99
- value = __values_array( method, args, value: value, criteria: criteria )
81
+ value = __values_array( method, args, value: value, criteria: criteria, &block )
100
82
  end
101
83
  end
102
84
 
@@ -104,8 +86,8 @@ module DynamicSchema
104
86
  @values[ name ] = value
105
87
  else
106
88
  ::Kernel.raise ::NoMethodError,
107
- "There is no schema value or object '#{method}' defined in this scope which includes: " \
108
- "#{@schema.keys.join( ', ' )}."
89
+ "There is no schema value or object '#{ method }' defined in this scope which includes: " \
90
+ "#{ @schema.keys.join( ', ' ) }."
109
91
  end
110
92
  end
111
93
 
@@ -124,18 +106,21 @@ module DynamicSchema
124
106
  required_arguments = [ required_arguments ].flatten if required_arguments
125
107
  required_count = required_arguments&.length || 0
126
108
  ::Kernel.raise ::ArgumentError,
127
- "The attribute '#{name}' requires #{required_count} arguments " \
128
- "(#{required_arguments.join(', ')}) but #{count} was given." \
109
+ "The attribute '#{ name }' requires #{ required_count } arguments " \
110
+ "(#{ required_arguments.join( ', ' ) }) but #{ count } was given." \
129
111
  if count < required_count
130
112
  ::Kernel.raise ::ArgumentError,
131
- "The attribute '#{name}' should have at most #{required_count + 1} arguments but " \
132
- "#{count} was given." \
113
+ "The attribute '#{ name }' should have at most #{ required_count + 1 } arguments but " \
114
+ "#{ count } was given." \
133
115
  if count > required_count + 1
116
+
134
117
  result = {}
118
+
135
119
  required_arguments&.each_with_index do | name, index |
136
120
  result[ name.to_sym ] = arguments[ index ]
137
121
  end
138
-
122
+ arguments.slice!( 0, required_arguments.length ) if required_arguments
123
+
139
124
  last = arguments.last
140
125
  case last
141
126
  when ::Hash
@@ -162,7 +147,7 @@ module DynamicSchema
162
147
 
163
148
  if result.nil?
164
149
  types.each do | type |
165
- result = @converters[ type ].call( value ) rescue nil
150
+ result = @converter.convert( value, to: type ) rescue nil
166
151
  break unless result.nil?
167
152
  end
168
153
  end
@@ -170,13 +155,16 @@ module DynamicSchema
170
155
  result
171
156
  end
172
157
 
173
- def __value( method, arguments, value:, criteria: )
158
+ def __value( method, arguments, value:, criteria:, &block )
174
159
  value = arguments.first
175
160
  new_value = criteria[ :type ] ? __coerce_value( criteria[ :type ], value ) : value
176
- new_value.nil? ? value : new_value
161
+ new_value = new_value.nil? ? value : new_value
162
+ block ?
163
+ __value_block( method, value: new_value, criteria: criteria, &block ) :
164
+ new_value
177
165
  end
178
166
 
179
- def __values_array( method, arguments, value:, criteria: )
167
+ def __values_array( method, arguments, value:, criteria:, &block )
180
168
  values = [ arguments.first ].flatten
181
169
  if type = criteria[ :type ]
182
170
  values = values.map do | v |
@@ -184,6 +172,11 @@ module DynamicSchema
184
172
  new_value.nil? ? v : new_value
185
173
  end
186
174
  end
175
+ if block
176
+ values = values.map do | value |
177
+ __value_block( method, value: value, criteria: criteria, &block )
178
+ end
179
+ end
187
180
  value.concat( values )
188
181
  end
189
182
 
@@ -194,10 +187,10 @@ module DynamicSchema
194
187
  )
195
188
  if value.nil? || attributes&.any?
196
189
  value =
197
- Receiver.new(
190
+ Receiver::Object.new(
198
191
  attributes,
199
- converters: @converters,
200
- schema: criteria[ :schema ] ||= criteria[ :resolver ]._schema
192
+ converter: @converter,
193
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled
201
194
  )
202
195
  end
203
196
  value.instance_eval( &block ) if block
@@ -210,15 +203,50 @@ module DynamicSchema
210
203
  required_arguments: criteria[ :arguments ]
211
204
  )
212
205
  value.concat( [ attributes ].flatten.map { | a |
213
- receiver = Receiver.new(
206
+ receiver = Receiver::Object.new(
214
207
  a,
215
- converters: @converters,
216
- schema: criteria[ :schema ] ||= criteria[ :resolver ]._schema
208
+ converter: @converter,
209
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled
217
210
  )
218
211
  receiver.instance_eval( &block ) if block
219
212
  receiver
220
213
  } )
221
214
  end
222
215
 
216
+ def __value_block( method, value:, criteria:, &block )
217
+ if value.nil?
218
+ type = criteria[ :type ]
219
+
220
+ if type.is_a?( ::Array )
221
+ if type.length == 1
222
+ type = type.first
223
+ else
224
+ ::Kernel.raise ::TypeError,
225
+ "An explicit value for '#{method}' is required when using a block " +
226
+ "because multiple types were specified."
227
+ end
228
+ end
229
+
230
+ case type
231
+ when ::Class
232
+ begin
233
+ value = type.new
234
+ rescue => error
235
+ ::Kernel.raise ::TypeError,
236
+ "An explicit value for '#{method}' is required because '#{type}' " +
237
+ "could not be constructed: #{error.message}."
238
+ end
239
+ else
240
+ ::Kernel.raise ::TypeError,
241
+ "An explicit value for '#{method}' is required because '#{type}' is " +
242
+ "not a Class."
243
+ end
244
+
245
+ end
246
+ ::DynamicSchema::Receiver::Value.new( value ).instance_eval( &block )
247
+ value
248
+ end
249
+
250
+ end
223
251
  end
224
252
  end
@@ -0,0 +1,27 @@
1
+ require_relative 'base'
2
+
3
+ module DynamicSchema
4
+ module Receiver
5
+ class Value < Base
6
+ def initialize( target )
7
+ @target = target
8
+ end
9
+
10
+ def method_missing( method_name, *args, &block )
11
+ writer_method = :"#{ method_name }="
12
+ unless @target.respond_to?( writer_method )
13
+ ::Kernel.raise ::NoMethodError,
14
+ "The attribute '#{ method_name }' cannot be assigned because '#{ @target.class.name }' does not define '#{ writer_method }'."
15
+ end
16
+
17
+ if args.length != 1
18
+ ::Kernel.raise ::ArgumentError,
19
+ "The attribute '#{ method_name }' requires 1 argument but #{ args.length } was given."
20
+ end
21
+
22
+ @target.public_send( writer_method, args.first )
23
+ end
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,167 @@
1
+ module DynamicSchema
2
+ class Struct
3
+ include Validator
4
+ class << self
5
+
6
+ def define( inherit: nil, &block )
7
+ builder = ::DynamicSchema.define( inherit: inherit, &block )
8
+ new( builder.schema )
9
+ end
10
+
11
+ def new( *arguments, &block )
12
+ unless self == ::DynamicSchema::Struct
13
+ super
14
+ else
15
+ __schema = arguments.first
16
+ ::Kernel.raise ::ArgumentError, "A Struct requires a schema." \
17
+ unless __schema
18
+ __converter = ::DynamicSchema::Converter
19
+
20
+ __compiled_schema = __compile_schema( __schema )
21
+ __klass = ::Class.new( self ) do
22
+
23
+ class << self
24
+ def build( attributes = {}, &block )
25
+ struct = new( attributes )
26
+ block.call( struct ) if block
27
+ struct
28
+ end
29
+
30
+ def build!( attributes = {}, &block )
31
+ struct = build( attributes, &block )
32
+ struct.validate!
33
+ struct
34
+ end
35
+ end
36
+
37
+ def initialize( attributes = nil )
38
+ @attributes = attributes&.dup || {}
39
+ @converted_attributes = {}
40
+ end
41
+
42
+ __compiled_schema.each do | property, criteria |
43
+
44
+ key = ( criteria[ :as ] || property ).to_sym
45
+ type = criteria[ :type ]
46
+ default = criteria[ :default ]
47
+
48
+ if type == ::Object
49
+ define_method( property ) do
50
+ @converted_attributes.fetch( key ) do
51
+ value = @attributes[ key ]
52
+ schema = criteria[ :schema ] ||= ( criteria[ :compiler ]&.compiled )
53
+ return value unless schema
54
+ klass = criteria[ :class ] ||= ::DynamicSchema::Struct.new( schema )
55
+ @converted_attributes[ key ] =
56
+ if criteria[ :array ]
57
+ Array( value || default ).map { | v | klass.build( v || {} ) }
58
+ else
59
+ klass.build( value || default )
60
+ end
61
+ end
62
+ end
63
+ elsif type
64
+ define_method( property ) do
65
+ @converted_attributes.fetch( key ) do
66
+ value = @attributes[ key ]
67
+ @converted_attributes[ key ] = criteria[ :array ] ?
68
+ Array( value || default ).map { | v | __convert( v, to: type ) } :
69
+ __convert( value || default, to: type )
70
+ end
71
+ end
72
+ else
73
+ define_method( property ) do
74
+ @attributes[ key ] || default
75
+ end
76
+ end
77
+
78
+ define_method( :"#{ property }=" ) do | value |
79
+ @converted_attributes.delete( key )
80
+ @attributes[ key ] = value
81
+ end
82
+
83
+ end
84
+
85
+ [ :validate!, :validate, :valid? ].each do | method |
86
+ define_method( method ) { super( @attributes ) }
87
+ end
88
+
89
+ def to_h
90
+ result = {}
91
+ self.compiled_schema.each do | property, criteria |
92
+ key = criteria[ :as ] || property
93
+ value = __object_to_h( self.send( property ) )
94
+ result[ key ] = value unless value.nil?
95
+ end
96
+ result
97
+ end
98
+
99
+ private
100
+
101
+ def compiled_schema
102
+ self.class.compiled_schema
103
+ end
104
+
105
+ def __object_to_h( object )
106
+ case object
107
+ when nil
108
+ nil
109
+ when ::Hash
110
+ object.transform_values { | v | __object_to_h( v ) } unless object.empty?
111
+ when ::Array
112
+ object.map { | e | __object_to_h( e ) } unless object.empty?
113
+ else
114
+ if object.respond_to?( :to_h )
115
+ __object_to_h( object.to_h )
116
+ else
117
+ object
118
+ end
119
+ end
120
+ end
121
+
122
+ def __convert( value, to: )
123
+ self.class.converter.convert( value, to: to ) { | v | to.new( v ) rescue nil }
124
+ end
125
+
126
+ end
127
+ __klass.instance_variable_set( :@__compiled_schema, __compiled_schema.dup )
128
+ __klass.instance_variable_set( :@__converter, __converter )
129
+ __klass.class_eval( &block ) if block
130
+ __klass
131
+ end
132
+ end
133
+
134
+ def compiled_schema
135
+ @__compiled_schema ||
136
+ ( superclass.respond_to?( :compiled_schema ) && superclass.compiled_schema )
137
+ end
138
+
139
+ def converter
140
+ @__converter || ( superclass.respond_to?( :converter ) && superclass.converter )
141
+ end
142
+
143
+ private
144
+
145
+ def __compile_schema( schema )
146
+ case schema
147
+ when ::Proc
148
+ compiler = ::DynamicSchema::Compiler.new
149
+ compiler.compile( &schema )
150
+ compiler.compiled
151
+ when ::Hash
152
+ ::Kernel.raise ::ArgumentError,
153
+ "A Struct requires a schema but an empty Hash was given." \
154
+ if schema.empty?
155
+ schema
156
+ else
157
+ if schema.respond_to?( :compiled_schema, true )
158
+ schema.send( :compiled_schema )
159
+ else
160
+ ::Kernel.raise ::ArgumentError,
161
+ "A Struct requires a schema. I must be a Builder, Proc, or Hash."
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,105 @@
1
+ module DynamicSchema
2
+ module Validator
3
+ extend self
4
+
5
+ def validate!( values, schema: compiled_schema )
6
+ traverse_and_validate_values( values, schema: schema ) { | error |
7
+ raise error
8
+ }
9
+ end
10
+
11
+ def validate( values, schema: compiled_schema )
12
+ errors = []
13
+ traverse_and_validate_values( values, schema: schema ) { | error |
14
+ errors << error
15
+ }
16
+ errors
17
+ end
18
+
19
+ def valid?( values, schema: compiled_schema )
20
+ traverse_and_validate_values( values, schema: schema ) do
21
+ return false
22
+ end
23
+ true
24
+ end
25
+
26
+ protected
27
+
28
+ def compiled_schema
29
+ raise ::NotImplementedError,
30
+ "The DynamicSchema::Validator requires an includind class to implement the " +
31
+ "`compiled_schema` method."
32
+ end
33
+
34
+ def value_matches_types?( value, types )
35
+ type_match = false
36
+ Array( types ).each do | type |
37
+ type_match = value.is_a?( type )
38
+ break if type_match
39
+ end
40
+ type_match
41
+ end
42
+
43
+ def traverse_and_validate_values( values, schema:, path: nil, options: nil, &block )
44
+ path.chomp( '/' ) if path
45
+ unless values.respond_to?( :[] )
46
+ raise ArgumentError, "The values must respond_to `[]`."
47
+ end
48
+
49
+ schema.each do | key, criteria |
50
+ name = criteria[ :as ] || key
51
+ value = values[ name ]
52
+
53
+ if criteria[ :required ] && ( !value || ( value.respond_to?( :empty ) && value.empty? ) )
54
+ block.call( RequiredOptionError.new( path: path, key: key ) )
55
+ elsif criteria[ :in ]
56
+ Array( value ).each do | v |
57
+ unless criteria[ :in ].include?( v ) || v.nil?
58
+ block.call( InOptionError.new( path: path, key: key, option: criteria[ :in ], value: v ) )
59
+ end
60
+ end
61
+ elsif !criteria[ :default_assigned ] && !value.nil?
62
+ unless criteria[ :array ]
63
+ if criteria[ :type ] == Object
64
+ traverse_and_validate_values(
65
+ values[ name ],
66
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
67
+ path: "#{ ( path || '' ) + ( path ? '/' : '' ) + key.to_s }",
68
+ &block
69
+ )
70
+ else
71
+ if criteria[ :type ] && value && !criteria[ :default_assigned ]
72
+ unless value_matches_types?( value, criteria[ :type ] )
73
+ block.call( IncompatibleTypeError.new( path: path, key: key, type: criteria[ :type ], value: value ) )
74
+ end
75
+ end
76
+ end
77
+ else
78
+ if criteria[ :type ] == Object
79
+ groups = Array( value )
80
+ groups.each do | group |
81
+ traverse_and_validate_values(
82
+ group,
83
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
84
+ path: "#{ ( path || '' ) + ( path ? '/' : '' ) + key.to_s }",
85
+ &block
86
+ )
87
+ end
88
+ else
89
+ if criteria[ :type ] && !criteria[ :default_assigned ]
90
+ value_array = Array( value )
91
+ value_array.each do | v |
92
+ unless value_matches_types?( v, criteria[ :type ] )
93
+ block.call( IncompatibleTypeError.new( path: path, key: key, type: criteria[ :type ], value: v ) )
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ nil
102
+ end
103
+
104
+ end
105
+ end
@@ -1,11 +1,16 @@
1
1
  require_relative 'dynamic_schema/errors'
2
+
3
+ require_relative 'dynamic_schema/validator'
4
+ require_relative 'dynamic_schema/converter'
2
5
  require_relative 'dynamic_schema/builder'
3
6
 
4
7
  require_relative 'dynamic_schema/definable'
5
8
  require_relative 'dynamic_schema/buildable'
6
9
 
10
+ require_relative 'dynamic_schema/struct'
11
+
7
12
  module DynamicSchema
8
- def self.define( schema = {}, &block )
9
- Builder.new( schema ).define( &block )
13
+ def self.define( inherit: nil, &block )
14
+ Builder.new.define( inherit: inherit, &block )
10
15
  end
11
16
  end