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
data/dynamicschema.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Gem::Specification.new do | spec |
|
|
2
2
|
|
|
3
3
|
spec.name = 'dynamicschema'
|
|
4
|
-
spec.version = '1.0
|
|
4
|
+
spec.version = '2.1.0'
|
|
5
5
|
spec.authors = [ 'Kristoph Cichocki-Romanov' ]
|
|
6
6
|
spec.email = [ 'rubygems.org@kristoph.net' ]
|
|
7
7
|
|
|
@@ -19,6 +19,11 @@ Gem::Specification.new do | spec |
|
|
|
19
19
|
and validations. By allowing default values, type constraints, nested schemas, and
|
|
20
20
|
transformations, DynamicSchema ensures that your data structures are both robust and
|
|
21
21
|
flexible.
|
|
22
|
+
|
|
23
|
+
New in 2.0, DynamicSchema adds DynamicSchema::Struct which faciliates effortless definition
|
|
24
|
+
and construction of complex object hierarchies, with optional type coersion and validation.
|
|
25
|
+
Where DynamicSchema simplified configuration and API payload construction,
|
|
26
|
+
DynamicSchema::Struct simplifies construction of complex API reponses.
|
|
22
27
|
TEXT
|
|
23
28
|
|
|
24
29
|
spec.license = 'MIT'
|
|
@@ -33,7 +38,7 @@ Gem::Specification.new do | spec |
|
|
|
33
38
|
spec.files = Dir[ "lib/**/*.rb", "LICENSE", "README.md", "dynamicschema.gemspec" ]
|
|
34
39
|
spec.require_paths = [ "lib" ]
|
|
35
40
|
|
|
36
|
-
spec.add_development_dependency '
|
|
41
|
+
spec.add_development_dependency 'minitest', '~> 6.0'
|
|
37
42
|
spec.add_development_dependency 'debug', '~> 1.9'
|
|
38
43
|
|
|
39
44
|
end
|
|
@@ -6,15 +6,15 @@ module DynamicSchema
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
module ClassMethods
|
|
9
|
+
[ :build, :build_from_bytes, :build_from_file ].each do | name |
|
|
10
|
+
define_method( name ) do | *args, **kwargs, &block |
|
|
11
|
+
new( builder.public_send( name, *args, **kwargs, &block ) )
|
|
12
|
+
end
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def build!( attributes = nil, &block )
|
|
15
|
-
new( builder.build!( attributes, &block ) )
|
|
16
|
-
end
|
|
17
|
-
|
|
14
|
+
define_method( :"#{name}!" ) do | *args, **kwargs, &block |
|
|
15
|
+
new( builder.public_send( :"#{name}!", *args, **kwargs, &block ) )
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
end
|
|
@@ -1,38 +1,69 @@
|
|
|
1
|
-
require_relative '
|
|
2
|
-
require_relative '
|
|
3
|
-
require_relative 'resolver'
|
|
4
|
-
require_relative 'receiver'
|
|
1
|
+
require_relative 'compiler'
|
|
2
|
+
require_relative 'receiver/object'
|
|
5
3
|
|
|
6
4
|
module DynamicSchema
|
|
7
5
|
class Builder
|
|
8
|
-
|
|
9
|
-
include
|
|
10
|
-
include BuilderMethods::Conversion
|
|
6
|
+
include Validator
|
|
7
|
+
include Converter
|
|
11
8
|
|
|
12
|
-
def initialize
|
|
13
|
-
self.
|
|
14
|
-
|
|
9
|
+
def initialize
|
|
10
|
+
self.compiled_schema = nil
|
|
11
|
+
@schema_blocks = []
|
|
15
12
|
end
|
|
16
13
|
|
|
17
|
-
def define( &block )
|
|
18
|
-
|
|
14
|
+
def define( inherit: nil, &block )
|
|
15
|
+
@schema_blocks << inherit if inherit
|
|
16
|
+
@schema_blocks << block if block
|
|
17
|
+
|
|
18
|
+
compiler = Compiler.new( self.compiled_schema )
|
|
19
|
+
compiler.compile( &inherit ) if inherit
|
|
20
|
+
compiler.compile( &block ) if block
|
|
21
|
+
self.compiled_schema = compiler.compiled
|
|
19
22
|
self
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
def
|
|
23
|
-
|
|
25
|
+
def schema
|
|
26
|
+
blocks = @schema_blocks.dup
|
|
27
|
+
proc do
|
|
28
|
+
blocks.each { | block | instance_eval( &block ) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
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 )
|
|
24
37
|
receiver.instance_eval( &block ) if block
|
|
25
38
|
receiver.to_h
|
|
26
39
|
end
|
|
27
40
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 )
|
|
46
|
+
receiver.instance_eval( bytes, filename, 1 )
|
|
47
|
+
receiver.to_h
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_from_file( path, values: nil, defaults: true )
|
|
51
|
+
self.build_from_bytes(
|
|
52
|
+
File.read( path, encoding: 'UTF-8' ),
|
|
53
|
+
filename: path, values: values, defaults: defaults
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
[ :build, :build_from_bytes, :build_from_file ].each do |name|
|
|
58
|
+
define_method( :"#{name}!" ) do |*args, **kwargs, &blk|
|
|
59
|
+
result = public_send(name, *args, **kwargs, &blk)
|
|
60
|
+
validate!(result)
|
|
61
|
+
result
|
|
62
|
+
end
|
|
32
63
|
end
|
|
33
64
|
|
|
34
65
|
private
|
|
35
|
-
attr_accessor :
|
|
66
|
+
attr_accessor :compiled_schema
|
|
36
67
|
|
|
37
68
|
end
|
|
38
69
|
end
|
|
@@ -1,57 +1,60 @@
|
|
|
1
|
-
require_relative 'receiver'
|
|
1
|
+
require_relative 'receiver/object'
|
|
2
2
|
|
|
3
3
|
module DynamicSchema
|
|
4
|
-
class
|
|
4
|
+
class Compiler < BasicObject
|
|
5
5
|
|
|
6
|
-
def initialize(
|
|
7
|
-
@
|
|
6
|
+
def initialize( compiled_schema = nil, compiled_blocks: nil )
|
|
7
|
+
@compiled_schema = compiled_schema || {}
|
|
8
8
|
|
|
9
9
|
@block = nil
|
|
10
|
-
@
|
|
11
|
-
@
|
|
10
|
+
@compiled = false
|
|
11
|
+
@compiled_blocks = compiled_blocks || []
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def compile( &block )
|
|
15
15
|
@block = block
|
|
16
|
-
@
|
|
17
|
-
unless @
|
|
18
|
-
@
|
|
16
|
+
@compiled = false
|
|
17
|
+
unless @compiled_blocks.include?( @block )
|
|
18
|
+
@compiled_blocks << @block
|
|
19
19
|
self.instance_eval( &@block )
|
|
20
|
-
@
|
|
20
|
+
@compiled = true
|
|
21
21
|
end
|
|
22
22
|
self
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@resolved_blocks << @block unless @resolved_blocks.include?( @block )
|
|
25
|
+
def compiled
|
|
26
|
+
if !@compiled && @block
|
|
27
|
+
@compiled_blocks << @block unless @compiled_blocks.include?( @block )
|
|
29
28
|
self.instance_eval( &@block )
|
|
30
|
-
@
|
|
29
|
+
@compiled = true
|
|
31
30
|
end
|
|
32
|
-
@
|
|
31
|
+
@compiled_schema
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
def _value( name, options )
|
|
36
35
|
name = name.to_sym
|
|
36
|
+
receiver = ::DynamicSchema::Receiver::Object
|
|
37
37
|
::Kernel.raise ::NameError, "The name '#{name}' is reserved and cannot be used for parameters." \
|
|
38
|
-
if
|
|
38
|
+
if receiver.method_defined?( name ) || receiver.private_method_defined?( name )
|
|
39
39
|
|
|
40
40
|
_validate_in!( name, options[ :type ], options[ :in ] ) if options[ :in ]
|
|
41
41
|
|
|
42
|
-
@
|
|
42
|
+
@compiled_schema[ name ] = options
|
|
43
43
|
self
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def _object( name, options = {}, &block )
|
|
47
47
|
name = name.to_sym
|
|
48
|
+
receiver = ::DynamicSchema::Receiver::Object
|
|
48
49
|
::Kernel.raise ::NameError, "The name '#{name}' is reserved and cannot be used for parameters." \
|
|
49
|
-
if
|
|
50
|
+
if receiver.method_defined?( name ) || receiver.private_method_defined?( name )
|
|
51
|
+
|
|
52
|
+
type = options[ :type ]
|
|
53
|
+
options[ :type ] = ::Object unless type.is_a?( ::Array )
|
|
50
54
|
|
|
51
|
-
@
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} )
|
|
55
|
+
@compiled_schema[ name ] = options.merge( {
|
|
56
|
+
compiler: Compiler.new( compiled_blocks: @compiled_blocks ).compile( &block )
|
|
57
|
+
} )
|
|
55
58
|
self
|
|
56
59
|
end
|
|
57
60
|
|
|
@@ -60,33 +63,32 @@ module DynamicSchema
|
|
|
60
63
|
options = nil
|
|
61
64
|
if args.empty?
|
|
62
65
|
options = {}
|
|
63
|
-
# when called with just options: name as: :streams
|
|
64
66
|
elsif first.is_a?( ::Hash )
|
|
65
|
-
options = first
|
|
66
|
-
|
|
67
|
-
# name [ TrueClass, FalseClass ]
|
|
68
|
-
elsif args.length == 1 &&
|
|
67
|
+
options = first
|
|
68
|
+
elsif args.length == 1 &&
|
|
69
69
|
( first.is_a?( ::Class ) || first.is_a?( ::Module ) || first.is_a?( ::Array ) )
|
|
70
70
|
options = { type: first }
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
( 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 ) ) &&
|
|
74
73
|
args[ 1 ].is_a?( ::Hash )
|
|
75
74
|
options = args[ 1 ]
|
|
76
75
|
options[ :type ] = args[ 0 ]
|
|
77
76
|
else
|
|
78
77
|
::Kernel.raise \
|
|
79
|
-
::ArgumentError,
|
|
78
|
+
::ArgumentError,
|
|
80
79
|
"A schema definition may only include the type (Class or Module) followed by options (Hash). "
|
|
81
80
|
end
|
|
82
81
|
|
|
83
82
|
type = options[ :type ]
|
|
84
|
-
|
|
83
|
+
|
|
84
|
+
if type.is_a?( ::Array ) && block
|
|
85
|
+
_object( method, options, &block )
|
|
86
|
+
elsif type == ::Object || ( type.nil? && block )
|
|
85
87
|
_object( method, options, &block )
|
|
86
|
-
else
|
|
88
|
+
else
|
|
87
89
|
_value( method, options )
|
|
88
90
|
end
|
|
89
|
-
|
|
91
|
+
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
def to_s
|
|
@@ -94,15 +96,15 @@ module DynamicSchema
|
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def inspect
|
|
97
|
-
{ schema: @
|
|
99
|
+
{ schema: @compiled_schema }.inspect
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
def class
|
|
101
|
-
::DynamicSchema::
|
|
103
|
+
::DynamicSchema::Compiler
|
|
102
104
|
end
|
|
103
105
|
|
|
104
106
|
def is_a?( klass )
|
|
105
|
-
klass == ::DynamicSchema::
|
|
107
|
+
klass == ::DynamicSchema::Compiler || klass == ::BasicObject
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
alias :kind_of? :is_a?
|
|
@@ -110,7 +112,7 @@ module DynamicSchema
|
|
|
110
112
|
if defined?( ::PP )
|
|
111
113
|
include ::PP::ObjectMixin
|
|
112
114
|
def pretty_print( pp )
|
|
113
|
-
pp.pp( { schema: @
|
|
115
|
+
pp.pp( { schema: @compiled_schema } )
|
|
114
116
|
end
|
|
115
117
|
end
|
|
116
118
|
|
|
@@ -124,6 +126,3 @@ module DynamicSchema
|
|
|
124
126
|
|
|
125
127
|
end
|
|
126
128
|
end
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
@@ -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
|