dynamicschema 1.0.1 → 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.
- checksums.yaml +4 -4
- data/README.md +456 -173
- data/dynamicschema.gemspec +7 -2
- data/lib/dynamic_schema/buildable.rb +8 -8
- data/lib/dynamic_schema/builder.rb +50 -19
- data/lib/dynamic_schema/{resolver.rb → compiler.rb} +42 -43
- data/lib/dynamic_schema/{builder_methods/conversion.rb → converter.rb} +41 -18
- data/lib/dynamic_schema/receiver/base.rb +60 -0
- data/lib/dynamic_schema/receiver/object.rb +301 -0
- data/lib/dynamic_schema/receiver/value.rb +27 -0
- data/lib/dynamic_schema/struct.rb +203 -0
- data/lib/dynamic_schema/validator.rb +139 -0
- data/lib/dynamic_schema.rb +7 -2
- metadata +18 -11
- data/lib/dynamic_schema/builder_methods/validation.rb +0 -109
- data/lib/dynamic_schema/receiver.rb +0 -227
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
require_relative 'value'
|
|
3
|
+
|
|
4
|
+
module DynamicSchema
|
|
5
|
+
module Receiver
|
|
6
|
+
class Object < Base
|
|
7
|
+
|
|
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 = {}
|
|
16
|
+
|
|
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 )
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def empty?
|
|
31
|
+
@values.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
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
|
|
49
|
+
|
|
50
|
+
recursive_to_h.call( @values )
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def class
|
|
54
|
+
::DynamicSchema::Receiver::Object
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def is_a?( klass )
|
|
58
|
+
klass == ::DynamicSchema::Receiver::Object ||
|
|
59
|
+
klass == ::DynamicSchema::Receiver::Base ||
|
|
60
|
+
klass == ::BasicObject
|
|
61
|
+
end
|
|
62
|
+
|
|
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
|
|
82
|
+
else
|
|
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
|
|
91
|
+
end
|
|
92
|
+
|
|
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
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def respond_to?( method, include_private = false )
|
|
103
|
+
@schema.key?( method ) || self.class.instance_methods.include?( method )
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def respond_to_missing?( method, include_private = false )
|
|
107
|
+
@schema.key?( method ) || self.class.instance_methods.include?( method )
|
|
108
|
+
end
|
|
109
|
+
|
|
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
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def __coerce_value( types, value )
|
|
144
|
+
return value unless types && !value.nil?
|
|
145
|
+
|
|
146
|
+
types = ::Kernel.method( :Array ).call( types )
|
|
147
|
+
result = nil
|
|
148
|
+
|
|
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
|
|
154
|
+
end
|
|
155
|
+
|
|
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
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
result
|
|
164
|
+
end
|
|
165
|
+
|
|
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
|
|
174
|
+
|
|
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
|
|
190
|
+
|
|
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
|
+
)
|
|
204
|
+
end
|
|
205
|
+
value.instance_eval( &block ) if block
|
|
206
|
+
value
|
|
207
|
+
end
|
|
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
|
|
220
|
+
)
|
|
221
|
+
receiver.instance_eval( &block ) if block
|
|
222
|
+
receiver
|
|
223
|
+
} )
|
|
224
|
+
end
|
|
225
|
+
|
|
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
|
|
261
|
+
|
|
262
|
+
def __value_block( method, value:, criteria:, &block )
|
|
263
|
+
type = criteria[ :type ]
|
|
264
|
+
|
|
265
|
+
# if the value type is a Proc the block is the value
|
|
266
|
+
return block if type == ::Proc
|
|
267
|
+
|
|
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
|
|
277
|
+
end
|
|
278
|
+
|
|
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
|
|
289
|
+
::Kernel.raise ::TypeError,
|
|
290
|
+
"An explicit value for '#{method}' is required because '#{type}' is " +
|
|
291
|
+
"not a Class."
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
end
|
|
295
|
+
::DynamicSchema::Receiver::Value.new( value ).instance_eval( &block )
|
|
296
|
+
value
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
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,203 @@
|
|
|
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
|
+
has_multiple_types = type.is_a?( ::Array ) && criteria[ :compiler ]
|
|
48
|
+
|
|
49
|
+
if has_multiple_types
|
|
50
|
+
define_method( property ) do
|
|
51
|
+
@converted_attributes.fetch( key ) do
|
|
52
|
+
value = @attributes[ key ]
|
|
53
|
+
value = default if value.nil?
|
|
54
|
+
schema = criteria[ :schema ] ||= ( criteria[ :compiler ]&.compiled )
|
|
55
|
+
klass = criteria[ :class ] ||= ( schema ? ::DynamicSchema::Struct.new( schema ) : nil )
|
|
56
|
+
@converted_attributes[ key ] =
|
|
57
|
+
if criteria[ :array ]
|
|
58
|
+
__convert_types_array( Array( value ), klass, criteria )
|
|
59
|
+
else
|
|
60
|
+
__convert_types( value, klass, criteria )
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
elsif type == ::Object
|
|
65
|
+
define_method( property ) do
|
|
66
|
+
@converted_attributes.fetch( key ) do
|
|
67
|
+
value = @attributes[ key ]
|
|
68
|
+
schema = criteria[ :schema ] ||= ( criteria[ :compiler ]&.compiled )
|
|
69
|
+
return value unless schema
|
|
70
|
+
klass = criteria[ :class ] ||= ::DynamicSchema::Struct.new( schema )
|
|
71
|
+
@converted_attributes[ key ] =
|
|
72
|
+
if criteria[ :array ]
|
|
73
|
+
Array( value || default ).map { | v | klass.build( v || {} ) }
|
|
74
|
+
else
|
|
75
|
+
klass.build( value || default )
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
elsif type
|
|
80
|
+
define_method( property ) do
|
|
81
|
+
@converted_attributes.fetch( key ) do
|
|
82
|
+
value = @attributes[ key ]
|
|
83
|
+
@converted_attributes[ key ] = criteria[ :array ] ?
|
|
84
|
+
Array( value || default ).map { | v | __convert( v, to: type ) } :
|
|
85
|
+
__convert( value || default, to: type )
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
define_method( property ) do
|
|
90
|
+
@attributes[ key ] || default
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
define_method( :"#{ property }=" ) do | value |
|
|
95
|
+
@converted_attributes.delete( key )
|
|
96
|
+
@attributes[ key ] = value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
[ :validate!, :validate, :valid? ].each do | method |
|
|
102
|
+
define_method( method ) { super( @attributes ) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_h
|
|
106
|
+
result = {}
|
|
107
|
+
self.compiled_schema.each do | property, criteria |
|
|
108
|
+
key = criteria[ :as ] || property
|
|
109
|
+
value = __object_to_h( self.send( property ) )
|
|
110
|
+
result[ key ] = value unless value.nil?
|
|
111
|
+
end
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def compiled_schema
|
|
118
|
+
self.class.compiled_schema
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def __object_to_h( object )
|
|
122
|
+
case object
|
|
123
|
+
when nil
|
|
124
|
+
nil
|
|
125
|
+
when ::Hash
|
|
126
|
+
object.transform_values { | v | __object_to_h( v ) } unless object.empty?
|
|
127
|
+
when ::Array
|
|
128
|
+
object.map { | e | __object_to_h( e ) } unless object.empty?
|
|
129
|
+
else
|
|
130
|
+
if object.respond_to?( :to_h )
|
|
131
|
+
__object_to_h( object.to_h )
|
|
132
|
+
else
|
|
133
|
+
object
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def __convert( value, to: )
|
|
139
|
+
self.class.converter.convert( value, to: to ) { | v | to.new( v ) rescue nil }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def __convert_types( value, klass, criteria )
|
|
143
|
+
return nil if value.nil?
|
|
144
|
+
if value.is_a?( ::Hash )
|
|
145
|
+
klass ? klass.build( value ) : value
|
|
146
|
+
else
|
|
147
|
+
scalar_types = __non_object_types( criteria[ :type ] )
|
|
148
|
+
__convert( value, to: scalar_types )
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def __convert_types_array( values, klass, criteria )
|
|
153
|
+
values.map { | v | __convert_types( v, klass, criteria ) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def __non_object_types( types )
|
|
157
|
+
types_array = types.is_a?( ::Array ) ? types : [ types ]
|
|
158
|
+
result = types_array.reject { | type | type == ::Object }
|
|
159
|
+
result.length == 1 ? result.first : result
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
end
|
|
163
|
+
__klass.instance_variable_set( :@__compiled_schema, __compiled_schema.dup )
|
|
164
|
+
__klass.instance_variable_set( :@__converter, __converter )
|
|
165
|
+
__klass.class_eval( &block ) if block
|
|
166
|
+
__klass
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def compiled_schema
|
|
171
|
+
@__compiled_schema ||
|
|
172
|
+
( superclass.respond_to?( :compiled_schema ) && superclass.compiled_schema )
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def converter
|
|
176
|
+
@__converter || ( superclass.respond_to?( :converter ) && superclass.converter )
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def __compile_schema( schema )
|
|
182
|
+
case schema
|
|
183
|
+
when ::Proc
|
|
184
|
+
compiler = ::DynamicSchema::Compiler.new
|
|
185
|
+
compiler.compile( &schema )
|
|
186
|
+
compiler.compiled
|
|
187
|
+
when ::Hash
|
|
188
|
+
::Kernel.raise ::ArgumentError,
|
|
189
|
+
"A Struct requires a schema but an empty Hash was given." \
|
|
190
|
+
if schema.empty?
|
|
191
|
+
schema
|
|
192
|
+
else
|
|
193
|
+
if schema.respond_to?( :compiled_schema, true )
|
|
194
|
+
schema.send( :compiled_schema )
|
|
195
|
+
else
|
|
196
|
+
::Kernel.raise ::ArgumentError,
|
|
197
|
+
"A Struct requires a schema. I must be a Builder, Proc, or Hash."
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
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 __validate_types( value, criteria, path, key, &block )
|
|
44
|
+
if value.is_a?( ::Hash )
|
|
45
|
+
traverse_and_validate_values(
|
|
46
|
+
value,
|
|
47
|
+
schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
|
|
48
|
+
path: "#{ ( path || '' ) + ( path ? '/' : '' ) + key.to_s }",
|
|
49
|
+
&block
|
|
50
|
+
)
|
|
51
|
+
else
|
|
52
|
+
scalar_types = __non_object_types( criteria[ :type ] )
|
|
53
|
+
unless value_matches_types?( value, scalar_types )
|
|
54
|
+
block.call( IncompatibleTypeError.new( path: path, key: key, type: criteria[ :type ], value: value ) )
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def __validate_types_array( values, criteria, path, key, &block )
|
|
60
|
+
values.each do | value |
|
|
61
|
+
__validate_types( value, criteria, path, key, &block )
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def __non_object_types( types )
|
|
66
|
+
types_array = types.is_a?( ::Array ) ? types : [ types ]
|
|
67
|
+
types_array.reject { | type | type == ::Object }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def traverse_and_validate_values( values, schema:, path: nil, options: nil, &block )
|
|
71
|
+
path.chomp( '/' ) if path
|
|
72
|
+
unless values.respond_to?( :[] )
|
|
73
|
+
raise ArgumentError, "The values must respond_to `[]`."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
schema.each do | key, criteria |
|
|
77
|
+
name = criteria[ :as ] || key
|
|
78
|
+
value = values[ name ]
|
|
79
|
+
|
|
80
|
+
if criteria[ :required ] && ( !value || ( value.respond_to?( :empty ) && value.empty? ) )
|
|
81
|
+
block.call( RequiredOptionError.new( path: path, key: key ) )
|
|
82
|
+
elsif criteria[ :in ]
|
|
83
|
+
Array( value ).each do | v |
|
|
84
|
+
unless criteria[ :in ].include?( v ) || v.nil?
|
|
85
|
+
block.call( InOptionError.new( path: path, key: key, option: criteria[ :in ], value: v ) )
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
elsif !criteria[ :default_assigned ] && !value.nil?
|
|
89
|
+
type = criteria[ :type ]
|
|
90
|
+
has_multiple_types = type.is_a?( ::Array ) && criteria[ :compiler ]
|
|
91
|
+
|
|
92
|
+
unless criteria[ :array ]
|
|
93
|
+
if has_multiple_types
|
|
94
|
+
__validate_types( value, criteria, path, key, &block )
|
|
95
|
+
elsif type == Object
|
|
96
|
+
traverse_and_validate_values(
|
|
97
|
+
values[ name ],
|
|
98
|
+
schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
|
|
99
|
+
path: "#{ ( path || '' ) + ( path ? '/' : '' ) + key.to_s }",
|
|
100
|
+
&block
|
|
101
|
+
)
|
|
102
|
+
else
|
|
103
|
+
if type && value && !criteria[ :default_assigned ]
|
|
104
|
+
unless value_matches_types?( value, type )
|
|
105
|
+
block.call( IncompatibleTypeError.new( path: path, key: key, type: type, value: value ) )
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
if has_multiple_types
|
|
111
|
+
__validate_types_array( Array( value ), criteria, path, key, &block )
|
|
112
|
+
elsif type == Object
|
|
113
|
+
groups = Array( value )
|
|
114
|
+
groups.each do | group |
|
|
115
|
+
traverse_and_validate_values(
|
|
116
|
+
group,
|
|
117
|
+
schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled,
|
|
118
|
+
path: "#{ ( path || '' ) + ( path ? '/' : '' ) + key.to_s }",
|
|
119
|
+
&block
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
if criteria[ :type ] && !criteria[ :default_assigned ]
|
|
124
|
+
value_array = Array( value )
|
|
125
|
+
value_array.each do | v |
|
|
126
|
+
unless value_matches_types?( v, criteria[ :type ] )
|
|
127
|
+
block.call( IncompatibleTypeError.new( path: path, key: key, type: criteria[ :type ], value: v ) )
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/dynamic_schema.rb
CHANGED
|
@@ -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(
|
|
9
|
-
Builder.new
|
|
13
|
+
def self.define( inherit: nil, &block )
|
|
14
|
+
Builder.new.define( inherit: inherit, &block )
|
|
10
15
|
end
|
|
11
16
|
end
|