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.
- 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} +77 -49
- 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,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 = @
|
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
|
-
|
200
|
-
schema: criteria[ :schema ] ||= criteria[ :
|
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
|
-
|
216
|
-
schema: criteria[ :schema ] ||= criteria[ :
|
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
|
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
|