dynamicschema 1.0.1 → 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.
- checksums.yaml +4 -4
- data/README.md +195 -35
- data/dynamicschema.gemspec +6 -1
- data/lib/dynamic_schema/buildable.rb +8 -8
- data/lib/dynamic_schema/builder.rb +43 -18
- data/lib/dynamic_schema/{resolver.rb → compiler.rb} +29 -35
- 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.rb → receiver/object.rb} +73 -48
- data/lib/dynamic_schema/receiver/value.rb +27 -0
- data/lib/dynamic_schema/struct.rb +167 -0
- data/lib/dynamic_schema/validator.rb +105 -0
- data/lib/dynamic_schema.rb +7 -2
- metadata +18 -8
- data/lib/dynamic_schema/builder_methods/validation.rb +0 -109
@@ -4,11 +4,47 @@ require 'date'
|
|
4
4
|
require 'uri'
|
5
5
|
|
6
6
|
module DynamicSchema
|
7
|
-
module
|
8
|
-
|
7
|
+
module Converter
|
8
|
+
extend self
|
9
9
|
|
10
|
-
|
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
|
-
|
2
|
-
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative 'value'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
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:,
|
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
|
-
@
|
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 ||
|
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,12 +106,12 @@ 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
|
134
116
|
|
135
117
|
result = {}
|
@@ -165,7 +147,7 @@ module DynamicSchema
|
|
165
147
|
|
166
148
|
if result.nil?
|
167
149
|
types.each do | type |
|
168
|
-
result = @
|
150
|
+
result = @converter.convert( value, to: type ) rescue nil
|
169
151
|
break unless result.nil?
|
170
152
|
end
|
171
153
|
end
|
@@ -173,13 +155,16 @@ module DynamicSchema
|
|
173
155
|
result
|
174
156
|
end
|
175
157
|
|
176
|
-
def __value( method, arguments, value:, criteria
|
158
|
+
def __value( method, arguments, value:, criteria:, &block )
|
177
159
|
value = arguments.first
|
178
160
|
new_value = criteria[ :type ] ? __coerce_value( criteria[ :type ], value ) : value
|
179
|
-
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
|
180
165
|
end
|
181
166
|
|
182
|
-
def __values_array( method, arguments, value:, criteria
|
167
|
+
def __values_array( method, arguments, value:, criteria:, &block )
|
183
168
|
values = [ arguments.first ].flatten
|
184
169
|
if type = criteria[ :type ]
|
185
170
|
values = values.map do | v |
|
@@ -187,6 +172,11 @@ module DynamicSchema
|
|
187
172
|
new_value.nil? ? v : new_value
|
188
173
|
end
|
189
174
|
end
|
175
|
+
if block
|
176
|
+
values = values.map do | value |
|
177
|
+
__value_block( method, value: value, criteria: criteria, &block )
|
178
|
+
end
|
179
|
+
end
|
190
180
|
value.concat( values )
|
191
181
|
end
|
192
182
|
|
@@ -197,10 +187,10 @@ module DynamicSchema
|
|
197
187
|
)
|
198
188
|
if value.nil? || attributes&.any?
|
199
189
|
value =
|
200
|
-
Receiver.new(
|
190
|
+
Receiver::Object.new(
|
201
191
|
attributes,
|
202
|
-
|
203
|
-
schema: criteria[ :schema ] ||= criteria[ :
|
192
|
+
converter: @converter,
|
193
|
+
schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled
|
204
194
|
)
|
205
195
|
end
|
206
196
|
value.instance_eval( &block ) if block
|
@@ -213,15 +203,50 @@ module DynamicSchema
|
|
213
203
|
required_arguments: criteria[ :arguments ]
|
214
204
|
)
|
215
205
|
value.concat( [ attributes ].flatten.map { | a |
|
216
|
-
receiver = Receiver.new(
|
206
|
+
receiver = Receiver::Object.new(
|
217
207
|
a,
|
218
|
-
|
219
|
-
schema: criteria[ :schema ] ||= criteria[ :
|
208
|
+
converter: @converter,
|
209
|
+
schema: criteria[ :schema ] ||= criteria[ :compiler ].compiled
|
220
210
|
)
|
221
211
|
receiver.instance_eval( &block ) if block
|
222
212
|
receiver
|
223
213
|
} )
|
224
214
|
end
|
225
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
|
226
251
|
end
|
227
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
|
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
|