dynamicschema 2.0.0 → 2.1.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.1.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,297 @@ 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
162
+
163
+ result
153
164
  end
154
165
 
155
- result
156
- 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
+ block ?
171
+ __value_block( method, value: new_value, criteria: criteria, &block ) :
172
+ new_value
173
+ end
157
174
 
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
175
+ def __values_array( method, arguments, value:, criteria:, &block )
176
+ values = [ arguments.first ].flatten
177
+ if type = criteria[ :type ]
178
+ values = values.map do | v |
179
+ new_value = __coerce_value( type, v )
180
+ new_value.nil? ? v : new_value
181
+ end
182
+ end
183
+ if block
184
+ values = values.map do | value |
185
+ __value_block( method, value: value, criteria: criteria, &block )
186
+ end
187
+ end
188
+ value.concat( values )
189
+ end
166
190
 
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 )
191
+ def __object( method, arguments, value:, criteria:, &block )
192
+ attributes = __process_arguments(
193
+ method, arguments,
194
+ required_arguments: criteria[ :arguments ]
195
+ )
196
+ if value.nil? || attributes&.any?
197
+ value =
198
+ Receiver::Object.new(
199
+ attributes,
200
+ converter: @converter,
201
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
202
+ defaults: @defaults
203
+ )
178
204
  end
205
+ value.instance_eval( &block ) if block
206
+ value
179
207
  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
208
+
209
+ def __object_array( method, arguments, value:, criteria:, &block )
210
+ attributes = __process_arguments(
211
+ method, arguments,
212
+ required_arguments: criteria[ :arguments ]
213
+ )
214
+ value.concat( [ attributes ].flatten.map { | a |
215
+ receiver = Receiver::Object.new(
216
+ a,
217
+ converter: @converter,
218
+ schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
219
+ defaults: @defaults
194
220
  )
221
+ receiver.instance_eval( &block ) if block
222
+ receiver
223
+ } )
195
224
  end
196
- value.instance_eval( &block ) if block
197
- value
198
- end
199
225
 
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
226
+ def __types( method, arguments, value:, criteria:, &block )
227
+ argument = arguments.first
228
+ if block || argument.is_a?( ::Hash )
229
+ if block && !value.nil? && !value.is_a?( ::DynamicSchema::Receiver::Object )
230
+ value = nil
231
+ end
232
+ __object( method, arguments, value: value, criteria: criteria, &block )
233
+ else
234
+ scalar_criteria = criteria.merge( type: __non_object_types( criteria[ :type ] ) )
235
+ __value( method, arguments, value: value, criteria: scalar_criteria, &block )
236
+ end
237
+ end
238
+
239
+ def __types_array( method, arguments, value:, criteria:, &block )
240
+ items = [ arguments.first ].flatten
241
+ if block
242
+ __object_array( method, arguments, value: value, criteria: criteria, &block )
243
+ else
244
+ items.each do | item |
245
+ if item.is_a?( ::Hash )
246
+ __object_array( method, [ item ], value: value, criteria: criteria )
247
+ else
248
+ scalar_criteria = criteria.merge( type: __non_object_types( criteria[ :type ] ) )
249
+ __values_array( method, [ item ], value: value, criteria: scalar_criteria )
250
+ end
251
+ end
252
+ value
253
+ end
254
+ end
255
+
256
+ def __non_object_types( types )
257
+ types_array = types.is_a?( ::Array ) ? types : [ types ]
258
+ result = types_array.reject { | type | type == ::Object }
259
+ result.length == 1 ? result.first : result
260
+ end
215
261
 
216
- def __value_block( method, value:, criteria:, &block )
217
- if value.nil?
262
+ def __value_block( method, value:, criteria:, &block )
218
263
  type = criteria[ :type ]
264
+
265
+ # if the value type is a Proc the block is the value
266
+ return block if type == ::Proc
219
267
 
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."
268
+ if value.nil?
269
+ if type.is_a?( ::Array )
270
+ if type.length == 1
271
+ type = type.first
272
+ else
273
+ ::Kernel.raise ::TypeError,
274
+ "An explicit value for '#{method}' is required when using a block " +
275
+ "because multiple types were specified."
276
+ end
227
277
  end
228
- end
229
278
 
230
- case type
231
- when ::Class
232
- begin
233
- value = type.new
234
- rescue => error
279
+ case type
280
+ when ::Class
281
+ begin
282
+ value = type.new
283
+ rescue => error
284
+ ::Kernel.raise ::TypeError,
285
+ "An explicit value for '#{method}' is required because '#{type}' " +
286
+ "could not be constructed: #{error.message}."
287
+ end
288
+ else
235
289
  ::Kernel.raise ::TypeError,
236
- "An explicit value for '#{method}' is required because '#{type}' " +
237
- "could not be constructed: #{error.message}."
290
+ "An explicit value for '#{method}' is required because '#{type}' is " +
291
+ "not a Class."
238
292
  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
293
 
294
+ end
295
+ ::DynamicSchema::Receiver::Value.new( value ).instance_eval( &block )
296
+ value
245
297
  end
246
- ::DynamicSchema::Receiver::Value.new( value ).instance_eval( &block )
247
- value
248
- end
249
298
 
250
- end
299
+ end
251
300
  end
252
301
  end