dynamicschema 2.0.0 → 2.2.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.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do | spec |
2
2
 
3
3
  spec.name = 'dynamicschema'
4
- spec.version = '2.0.0'
4
+ spec.version = '2.2.0'
5
5
  spec.authors = [ 'Kristoph Cichocki-Romanov' ]
6
6
  spec.email = [ 'rubygems.org@kristoph.net' ]
7
7
 
@@ -38,7 +38,7 @@ Gem::Specification.new do | spec |
38
38
  spec.files = Dir[ "lib/**/*.rb", "LICENSE", "README.md", "dynamicschema.gemspec" ]
39
39
  spec.require_paths = [ "lib" ]
40
40
 
41
- spec.add_development_dependency 'rspec', '~> 3.13'
41
+ spec.add_development_dependency 'minitest', '~> 6.0'
42
42
  spec.add_development_dependency 'debug', '~> 1.9'
43
43
 
44
44
  end
@@ -29,22 +29,28 @@ module DynamicSchema
29
29
  end
30
30
  end
31
31
 
32
- def build( values = nil, &block )
33
- receiver = Receiver::Object.new( values, schema: self.compiled_schema, converter: self )
32
+ def build( values = nil, defaults: true, &block )
33
+ receiver = Receiver::Object.new( values,
34
+ schema: self.compiled_schema,
35
+ converter: self,
36
+ defaults: defaults )
34
37
  receiver.instance_eval( &block ) if block
35
38
  receiver.to_h
36
39
  end
37
40
 
38
- def build_from_bytes( bytes, filename: '(schema)', values: nil )
39
- receiver = Receiver::Object.new( values, schema: compiled_schema, converter: self )
41
+ def build_from_bytes( bytes, filename: '(schema)', values: nil, defaults: true )
42
+ receiver = Receiver::Object.new( values,
43
+ schema: compiled_schema,
44
+ converter: self,
45
+ defaults: defaults )
40
46
  receiver.instance_eval( bytes, filename, 1 )
41
47
  receiver.to_h
42
48
  end
43
49
 
44
- def build_from_file( path, values: nil )
50
+ def build_from_file( path, values: nil, defaults: true )
45
51
  self.build_from_bytes(
46
52
  File.read( path, encoding: 'UTF-8' ),
47
- filename: path, values: values
53
+ filename: path, values: values, defaults: defaults
48
54
  )
49
55
  end
50
56
 
@@ -49,10 +49,12 @@ module DynamicSchema
49
49
  ::Kernel.raise ::NameError, "The name '#{name}' is reserved and cannot be used for parameters." \
50
50
  if receiver.method_defined?( name ) || receiver.private_method_defined?( name )
51
51
 
52
+ type = options[ :type ]
53
+ options[ :type ] = ::Object unless type.is_a?( ::Array )
54
+
52
55
  @compiled_schema[ name ] = options.merge( {
53
- type: ::Object,
54
56
  compiler: Compiler.new( compiled_blocks: @compiled_blocks ).compile( &block )
55
- } )
57
+ } )
56
58
  self
57
59
  end
58
60
 
@@ -62,28 +64,31 @@ module DynamicSchema
62
64
  if args.empty?
63
65
  options = {}
64
66
  elsif first.is_a?( ::Hash )
65
- options = first
66
- elsif args.length == 1 &&
67
+ options = first
68
+ elsif args.length == 1 &&
67
69
  ( first.is_a?( ::Class ) || first.is_a?( ::Module ) || first.is_a?( ::Array ) )
68
70
  options = { type: first }
69
- elsif args.length == 2 &&
70
- ( first.is_a?( ::Class ) || first.is_a?( ::Module ) || first.is_a?( ::Array ) ) &&
71
+ elsif args.length == 2 &&
72
+ ( first.is_a?( ::Class ) || first.is_a?( ::Module ) || first.is_a?( ::Array ) ) &&
71
73
  args[ 1 ].is_a?( ::Hash )
72
74
  options = args[ 1 ]
73
75
  options[ :type ] = args[ 0 ]
74
76
  else
75
77
  ::Kernel.raise \
76
- ::ArgumentError,
78
+ ::ArgumentError,
77
79
  "A schema definition may only include the type (Class or Module) followed by options (Hash). "
78
80
  end
79
81
 
80
82
  type = options[ :type ]
81
- if type == ::Object || ( type.nil? && block )
83
+
84
+ if type.is_a?( ::Array ) && block
85
+ _object( method, options, &block )
86
+ elsif type == ::Object || ( type.nil? && block )
82
87
  _object( method, options, &block )
83
88
  else
84
89
  _value( method, options )
85
90
  end
86
-
91
+
87
92
  end
88
93
 
89
94
  def to_s
@@ -5,248 +5,305 @@ module DynamicSchema
5
5
  module Receiver
6
6
  class Object < Base
7
7
 
8
- def initialize( values = nil, schema:, converter: )
9
- raise ArgumentError, 'The Receiver values must be a nil or a Hash.'\
10
- unless values.nil? || ( values.respond_to?( :[] ) && values.respond_to?( :key? ) )
11
-
12
- @values = {}
13
- @schema = schema
14
- @defaults_assigned = {}
8
+ def initialize( values = nil, schema:, converter:, defaults: true )
9
+ raise ArgumentError, 'The Receiver values must be a nil or a Hash.'\
10
+ unless values.nil? || ( values.respond_to?( :[] ) && values.respond_to?( :key? ) )
11
+
12
+ @values = {}
13
+ @schema = schema
14
+ @defaults = defaults
15
+ @defaults_assigned = {}
15
16
 
16
- @converter = converter
17
-
18
- @schema.each do | key, criteria |
19
- name = criteria[ :as ] || key
20
- if criteria.key?( :default )
21
- self.__send__( key, criteria[ :default ] )
22
- @defaults_assigned[ key ] = true
17
+ @converter = converter
18
+
19
+ @schema.each do | key, criteria |
20
+ name = criteria[ :as ] || key
21
+ if @defaults && criteria.key?( :default )
22
+ self.__send__( key, criteria[ :default ] )
23
+ @defaults_assigned[ key ] = true
24
+ end
25
+ self.__send__( key, values[ key ] ) if values && values.key?( key )
23
26
  end
24
- self.__send__( key, values[ key ] ) if values && values.key?( key )
25
- end
26
-
27
- end
28
-
29
- def empty?
30
- @values.empty?
31
- end
32
27
 
33
- def to_h
34
- recursive_to_h = ->( object ) do
35
- case object
36
- when ::NilClass
37
- nil
38
- when ::DynamicSchema::Receiver::Object
39
- recursive_to_h.call( object.to_h )
40
- when ::Hash
41
- object.transform_values { | value | recursive_to_h.call( value ) }
42
- when ::Array
43
- object.map { | element | recursive_to_h.call( element ) }
44
- else
45
- object.respond_to?( :to_h ) ? object.to_h : object
46
- end
47
28
  end
48
29
 
49
- recursive_to_h.call( @values )
50
- end
30
+ def empty?
31
+ @values.empty?
32
+ end
51
33
 
52
- def class
53
- ::DynamicSchema::Receiver::Object
54
- end
34
+ def to_h
35
+ recursive_to_h = ->( object ) do
36
+ case object
37
+ when ::NilClass
38
+ nil
39
+ when ::DynamicSchema::Receiver::Object
40
+ recursive_to_h.call( object.to_h )
41
+ when ::Hash
42
+ object.transform_values { | value | recursive_to_h.call( value ) }
43
+ when ::Array
44
+ object.map { | element | recursive_to_h.call( element ) }
45
+ else
46
+ object.respond_to?( :to_h ) ? object.to_h : object
47
+ end
48
+ end
55
49
 
56
- def is_a?( klass )
57
- klass == ::DynamicSchema::Receiver::Object ||
58
- klass == ::DynamicSchema::Receiver::Base ||
59
- klass == ::BasicObject
60
- end
50
+ recursive_to_h.call( @values )
51
+ end
61
52
 
62
- alias :kind_of? :is_a?
53
+ def class
54
+ ::DynamicSchema::Receiver::Object
55
+ end
63
56
 
64
- def method_missing( method, *args, &block )
65
- if @schema.key?( method )
66
- criteria = @schema[ method ]
67
- name = criteria[ :as ] || method
68
- value = @values[ name ]
57
+ def is_a?( klass )
58
+ klass == ::DynamicSchema::Receiver::Object ||
59
+ klass == ::DynamicSchema::Receiver::Base ||
60
+ klass == ::BasicObject
61
+ end
69
62
 
70
- unless criteria[ :array ]
71
- if criteria[ :type ] == ::Object
72
- value = __object( method, args, value: value, criteria: criteria, &block )
73
- else
74
- value = __value( method, args, value: value, criteria: criteria, &block )
75
- end
76
- else
77
- value = @defaults_assigned[ method ] ? ::Array.new : value || ::Array.new
78
- if criteria[ :type ] == ::Object
79
- value = __object_array( method, args, value: value, criteria: criteria, &block )
63
+ alias :kind_of? :is_a?
64
+
65
+ def method_missing( method, *args, &block )
66
+ if @schema.key?( method )
67
+ criteria = @schema[ method ]
68
+ name = criteria[ :as ] || method
69
+ value = @values[ name ]
70
+
71
+ type = criteria[ :type ]
72
+ has_multiple_types = type.is_a?( ::Array ) && criteria[ :compiler ]
73
+
74
+ unless criteria[ :array ]
75
+ if has_multiple_types
76
+ value = __types( method, args, value: value, criteria: criteria, &block )
77
+ elsif type == ::Object
78
+ value = __object( method, args, value: value, criteria: criteria, &block )
79
+ else
80
+ value = __value( method, args, value: value, criteria: criteria, &block )
81
+ end
80
82
  else
81
- value = __values_array( method, args, value: value, criteria: criteria, &block )
83
+ value = @defaults_assigned[ method ] ? ::Array.new : value || ::Array.new
84
+ if has_multiple_types
85
+ value = __types_array( method, args, value: value, criteria: criteria, &block )
86
+ elsif type == ::Object
87
+ value = __object_array( method, args, value: value, criteria: criteria, &block )
88
+ else
89
+ value = __values_array( method, args, value: value, criteria: criteria, &block )
90
+ end
82
91
  end
83
- end
84
92
 
85
- @defaults_assigned[ method ] = false
86
- @values[ name ] = value
87
- else
88
- ::Kernel.raise ::NoMethodError,
89
- "There is no schema value or object '#{ method }' defined in this scope which includes: " \
90
- "#{ @schema.keys.join( ', ' ) }."
93
+ @defaults_assigned[ method ] = false
94
+ @values[ name ] = value
95
+ else
96
+ ::Kernel.raise ::NoMethodError,
97
+ "There is no schema value or object '#{ method }' defined in this scope which includes: " \
98
+ "#{ @schema.keys.join( ', ' ) }."
99
+ end
91
100
  end
92
- end
93
101
 
94
- def respond_to?( method, include_private = false )
95
- @schema.key?( method ) || self.class.instance_methods.include?( method )
96
- end
102
+ def respond_to?( method, include_private = false )
103
+ @schema.key?( method ) || self.class.instance_methods.include?( method )
104
+ end
97
105
 
98
- def respond_to_missing?( method, include_private = false )
99
- @schema.key?( method ) || self.class.instance_methods.include?( method )
100
- end
106
+ def respond_to_missing?( method, include_private = false )
107
+ @schema.key?( method ) || self.class.instance_methods.include?( method )
108
+ end
101
109
 
102
- protected
103
-
104
- def __process_arguments( name, arguments, required_arguments: )
105
- count = arguments.count
106
- required_arguments = [ required_arguments ].flatten if required_arguments
107
- required_count = required_arguments&.length || 0
108
- ::Kernel.raise ::ArgumentError,
109
- "The attribute '#{ name }' requires #{ required_count } arguments " \
110
- "(#{ required_arguments.join( ', ' ) }) but #{ count } was given." \
111
- if count < required_count
112
- ::Kernel.raise ::ArgumentError,
113
- "The attribute '#{ name }' should have at most #{ required_count + 1 } arguments but " \
114
- "#{ count } was given." \
115
- if count > required_count + 1
116
-
117
- result = {}
118
-
119
- required_arguments&.each_with_index do | name, index |
120
- result[ name.to_sym ] = arguments[ index ]
121
- end
122
- arguments.slice!( 0, required_arguments.length ) if required_arguments
123
-
124
- last = arguments.last
125
- case last
126
- when ::Hash
127
- result.merge( last )
128
- when ::Array
129
- last.map { | v | result.merge( v ) }
130
- else
131
- result
110
+ protected
111
+
112
+ def __process_arguments( name, arguments, required_arguments: )
113
+ count = arguments.count
114
+ required_arguments = [ required_arguments ].flatten if required_arguments
115
+ required_count = required_arguments&.length || 0
116
+ ::Kernel.raise ::ArgumentError,
117
+ "The attribute '#{ name }' requires #{ required_count } arguments " \
118
+ "(#{ required_arguments.join( ', ' ) }) but #{ count } was given." \
119
+ if count < required_count
120
+ ::Kernel.raise ::ArgumentError,
121
+ "The attribute '#{ name }' should have at most #{ required_count + 1 } arguments but " \
122
+ "#{ count } was given." \
123
+ if count > required_count + 1
124
+
125
+ result = {}
126
+
127
+ required_arguments&.each_with_index do | name, index |
128
+ result[ name.to_sym ] = arguments[ index ]
129
+ end
130
+ arguments.slice!( 0, required_arguments.length ) if required_arguments
131
+
132
+ last = arguments.last
133
+ case last
134
+ when ::Hash
135
+ result.merge( last )
136
+ when ::Array
137
+ last.map { | v | result.merge( v ) }
138
+ else
139
+ result
140
+ end
132
141
  end
133
- end
134
142
 
135
- def __coerce_value( types, value )
136
- return value unless types && !value.nil?
143
+ def __coerce_value( types, value )
144
+ return value unless types && !value.nil?
137
145
 
138
- types = ::Kernel.method( :Array ).call( types )
139
- result = nil
146
+ types = ::Kernel.method( :Array ).call( types )
147
+ result = nil
140
148
 
141
- if value.respond_to?( :is_a? )
142
- types.each do | type |
143
- result = value.is_a?( type ) ? value : nil
144
- break unless result.nil?
149
+ if value.respond_to?( :is_a? )
150
+ types.each do | type |
151
+ result = value.is_a?( type ) ? value : nil
152
+ break unless result.nil?
153
+ end
145
154
  end
146
- end
147
155
 
148
- if result.nil?
149
- types.each do | type |
150
- result = @converter.convert( value, to: type ) rescue nil
151
- break unless result.nil?
156
+ if result.nil?
157
+ types.each do | type |
158
+ result = @converter.convert( value, to: type ) rescue nil
159
+ break unless result.nil?
160
+ end
152
161
  end
153
- end
154
162
 
155
- result
156
- end
163
+ result
164
+ end
157
165
 
158
- def __value( method, arguments, value:, criteria:, &block )
159
- value = arguments.first
160
- new_value = criteria[ :type ] ? __coerce_value( criteria[ :type ], value ) : 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
165
- end
166
+ def __value( method, arguments, value:, criteria:, &block )
167
+ value = arguments.first
168
+ new_value = criteria[ :type ] ? __coerce_value( criteria[ :type ], value ) : value
169
+ new_value = new_value.nil? ? value : new_value
170
+ new_value = block ?
171
+ __value_block( method, value: new_value, criteria: criteria, &block ) :
172
+ new_value
173
+ __normalize( new_value, criteria )
174
+ end
166
175
 
167
- def __values_array( method, arguments, value:, criteria:, &block )
168
- values = [ arguments.first ].flatten
169
- if type = criteria[ :type ]
170
- values = values.map do | v |
171
- new_value = __coerce_value( type, v )
172
- new_value.nil? ? v : new_value
173
- end
174
- end
175
- if block
176
- values = values.map do | value |
177
- __value_block( method, value: value, criteria: criteria, &block )
176
+ def __values_array( method, arguments, value:, criteria:, &block )
177
+ values = [ arguments.first ].flatten
178
+ if type = criteria[ :type ]
179
+ values = values.map do | v |
180
+ new_value = __coerce_value( type, v )
181
+ new_value.nil? ? v : new_value
182
+ end
183
+ end
184
+ if block
185
+ values = values.map do | value |
186
+ __value_block( method, value: value, criteria: criteria, &block )
187
+ end
178
188
  end
189
+ values = values.map { | v | __normalize( v, criteria ) }
190
+ value.concat( values )
191
+ end
192
+
193
+ def __object( method, arguments, value:, criteria:, &block )
194
+ attributes = __process_arguments(
195
+ method, arguments,
196
+ required_arguments: criteria[ :arguments ]
197
+ )
198
+ if value.nil? || attributes&.any?
199
+ value =
200
+ Receiver::Object.new(
201
+ attributes,
202
+ converter: @converter,
203
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
204
+ defaults: @defaults
205
+ )
206
+ end
207
+ value.instance_eval( &block ) if block
208
+ __normalize( value, criteria )
179
209
  end
180
- value.concat( values )
181
- end
182
-
183
- def __object( method, arguments, value:, criteria:, &block )
184
- attributes = __process_arguments(
185
- method, arguments,
186
- required_arguments: criteria[ :arguments ]
187
- )
188
- if value.nil? || attributes&.any?
189
- value =
190
- Receiver::Object.new(
191
- attributes,
192
- converter: @converter,
193
- schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled
210
+
211
+ def __object_array( method, arguments, value:, criteria:, &block )
212
+ attributes = __process_arguments(
213
+ method, arguments,
214
+ required_arguments: criteria[ :arguments ]
215
+ )
216
+ value.concat( [ attributes ].flatten.map { | a |
217
+ receiver = Receiver::Object.new(
218
+ a,
219
+ converter: @converter,
220
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
221
+ defaults: @defaults
194
222
  )
223
+ receiver.instance_eval( &block ) if block
224
+ __normalize( receiver, criteria )
225
+ } )
195
226
  end
196
- value.instance_eval( &block ) if block
197
- value
198
- end
199
227
 
200
- def __object_array( method, arguments, value:, criteria:, &block )
201
- attributes = __process_arguments(
202
- method, arguments,
203
- required_arguments: criteria[ :arguments ]
204
- )
205
- value.concat( [ attributes ].flatten.map { | a |
206
- receiver = Receiver::Object.new(
207
- a,
208
- converter: @converter,
209
- schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled
210
- )
211
- receiver.instance_eval( &block ) if block
212
- receiver
213
- } )
214
- end
228
+ def __types( method, arguments, value:, criteria:, &block )
229
+ argument = arguments.first
230
+ if block || argument.is_a?( ::Hash )
231
+ if block && !value.nil? && !value.is_a?( ::DynamicSchema::Receiver::Object )
232
+ value = nil
233
+ end
234
+ __object( method, arguments, value: value, criteria: criteria, &block )
235
+ else
236
+ scalar_criteria = criteria.merge( type: __non_object_types( criteria[ :type ] ) )
237
+ __value( method, arguments, value: value, criteria: scalar_criteria, &block )
238
+ end
239
+ end
240
+
241
+ def __types_array( method, arguments, value:, criteria:, &block )
242
+ items = [ arguments.first ].flatten
243
+ if block
244
+ __object_array( method, arguments, value: value, criteria: criteria, &block )
245
+ else
246
+ items.each do | item |
247
+ if item.is_a?( ::Hash )
248
+ __object_array( method, [ item ], value: value, criteria: criteria )
249
+ else
250
+ scalar_criteria = criteria.merge( type: __non_object_types( criteria[ :type ] ) )
251
+ __values_array( method, [ item ], value: value, criteria: scalar_criteria )
252
+ end
253
+ end
254
+ value
255
+ end
256
+ end
257
+
258
+ def __non_object_types( types )
259
+ types_array = types.is_a?( ::Array ) ? types : [ types ]
260
+ result = types_array.reject { | type | type == ::Object }
261
+ result.length == 1 ? result.first : result
262
+ end
215
263
 
216
- def __value_block( method, value:, criteria:, &block )
217
- if value.nil?
264
+ def __value_block( method, value:, criteria:, &block )
218
265
  type = criteria[ :type ]
266
+
267
+ # if the value type is a Proc the block is the value
268
+ return block if type == ::Proc
219
269
 
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."
270
+ if value.nil?
271
+ if type.is_a?( ::Array )
272
+ if type.length == 1
273
+ type = type.first
274
+ else
275
+ ::Kernel.raise ::TypeError,
276
+ "An explicit value for '#{method}' is required when using a block " +
277
+ "because multiple types were specified."
278
+ end
227
279
  end
228
- end
229
280
 
230
- case type
231
- when ::Class
232
- begin
233
- value = type.new
234
- rescue => error
281
+ case type
282
+ when ::Class
283
+ begin
284
+ value = type.new
285
+ rescue => error
286
+ ::Kernel.raise ::TypeError,
287
+ "An explicit value for '#{method}' is required because '#{type}' " +
288
+ "could not be constructed: #{error.message}."
289
+ end
290
+ else
235
291
  ::Kernel.raise ::TypeError,
236
- "An explicit value for '#{method}' is required because '#{type}' " +
237
- "could not be constructed: #{error.message}."
292
+ "An explicit value for '#{method}' is required because '#{type}' is " +
293
+ "not a Class."
238
294
  end
239
- else
240
- ::Kernel.raise ::TypeError,
241
- "An explicit value for '#{method}' is required because '#{type}' is " +
242
- "not a Class."
295
+
243
296
  end
297
+ ::DynamicSchema::Receiver::Value.new( value ).instance_eval( &block )
298
+ value
299
+ end
244
300
 
301
+ def __normalize( value, criteria )
302
+ normalize = criteria[ :normalize ]
303
+ return value unless normalize && !value.nil?
304
+ normalize.call( value )
245
305
  end
246
- ::DynamicSchema::Receiver::Value.new( value ).instance_eval( &block )
247
- value
248
- end
249
306
 
250
- end
307
+ end
251
308
  end
252
309
  end