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.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do | spec |
2
2
 
3
3
  spec.name = 'dynamicschema'
4
- spec.version = '1.0.1'
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 'rspec', '~> 3.13'
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
- def build( attributes = nil, &block )
11
- new( builder.build( attributes, &block ) )
12
- end
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 'builder_methods/conversion'
2
- require_relative 'builder_methods/validation'
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 BuilderMethods::Validation
10
- include BuilderMethods::Conversion
6
+ include Validator
7
+ include Converter
11
8
 
12
- def initialize( schema = nil )
13
- self.schema = schema
14
- super()
9
+ def initialize
10
+ self.compiled_schema = nil
11
+ @schema_blocks = []
15
12
  end
16
13
 
17
- def define( &block )
18
- self.schema = Resolver.new( self.schema ).resolve( &block )._schema
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 build( values = nil, &block )
23
- receiver = Receiver.new( values, schema: self.schema, converters: self.converters )
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 build!( values = nil, &block )
29
- result = self.build( values, &block )
30
- validate!( result )
31
- result
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 :schema
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 Resolver < BasicObject
4
+ class Compiler < BasicObject
5
5
 
6
- def initialize( schema = nil, resolved_blocks: nil )
7
- @schema = schema || {}
6
+ def initialize( compiled_schema = nil, compiled_blocks: nil )
7
+ @compiled_schema = compiled_schema || {}
8
8
 
9
9
  @block = nil
10
- @resolved = false
11
- @resolved_blocks = resolved_blocks || []
10
+ @compiled = false
11
+ @compiled_blocks = compiled_blocks || []
12
12
  end
13
13
 
14
- def resolve( &block )
14
+ def compile( &block )
15
15
  @block = block
16
- @resolved = false
17
- unless @resolved_blocks.include?( @block )
18
- @resolved_blocks << @block
16
+ @compiled = false
17
+ unless @compiled_blocks.include?( @block )
18
+ @compiled_blocks << @block
19
19
  self.instance_eval( &@block )
20
- @resolved = true
20
+ @compiled = true
21
21
  end
22
22
  self
23
23
  end
24
24
 
25
- def _schema
26
-
27
- if !@resolved && @block
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
- @resolved = true
29
+ @compiled = true
31
30
  end
32
- @schema
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 ::DynamicSchema::Receiver.instance_methods.include?( name )
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
- @schema[ name ] = options
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 ::DynamicSchema::Receiver.instance_methods.include?( name )
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
- @schema[ name ] = options.merge( {
52
- type: ::Object,
53
- resolver: Resolver.new( resolved_blocks: @resolved_blocks ).resolve( &block )
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
- # when called with just type: name String
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
- # when called with just type and options: name String, default: 'the default'
72
- elsif args.length == 2 &&
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
- 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 )
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: @schema }.inspect
99
+ { schema: @compiled_schema }.inspect
98
100
  end
99
101
 
100
102
  def class
101
- ::DynamicSchema::Resolver
103
+ ::DynamicSchema::Compiler
102
104
  end
103
105
 
104
106
  def is_a?( klass )
105
- klass == ::DynamicSchema::Resolver || klass == ::BasicObject
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: @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 BuilderMethods
8
- module Conversion
7
+ module Converter
8
+ extend self
9
9
 
10
- DEFAULT_CONVERTERS = {
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