parametric 0.2.7 → 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63dc92f4d7d45a1233ee42749f2a6511dfe0de3efa8fbc2f7fc2dd54e2832f70
4
- data.tar.gz: c2540792da112e082f610af8801a3f3609f1baee808971cf46ae1c1b0fccb9fc
3
+ metadata.gz: c773dd859f56f8d526384567daf66c1c259a7e5d859c9fe40ecc620c0ee73253
4
+ data.tar.gz: 9b056fd197e9fe84a2b14b16134d860ae3a7c49313f370fd9de82fb918c7bc3b
5
5
  SHA512:
6
- metadata.gz: 5acd3bb1c560f340f42b82612adf14e8696293c1f053407ee67cdfba991a9745c6c5db770fe499c0a87b94e118e1f13c442d4063d9ffa2ee63a10c358c796573
7
- data.tar.gz: 1c56887e85b884dad8211b27faf748b3be1352eda48b15a19c76160e81f3cc359cc3f1cdcc4c1462efdcf778c01ba51bda3bf5f74de5a412aed1c0a7e28168ce
6
+ metadata.gz: 9d068ab0c811e30db11d3b6eea2e38937557d2abb36df07623f419b669e7186a86af8de645575cacdb5efe7a51c0dbbe40048aa3fe5417296ee3c5810fd477bf
7
+ data.tar.gz: da9c1a990c6a0c2b85cf27d3f8ea7dd78e9b2e031548e240a79a43b85de9538e496ed491f3a5ef023ae6939d0f706b74274dd0519a7ae48d6452db65eed0d3ce
data/Gemfile CHANGED
@@ -2,3 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in parametric.gemspec
4
4
  gemspec
5
+
6
+ group :development do
7
+ gem 'benchmark-ips'
8
+ end
data/README.md CHANGED
@@ -98,7 +98,7 @@ results.errors # => {"$.friends[0].name" => "is required"}
98
98
  You can optionally use an existing schema instance as a nested schema:
99
99
 
100
100
  ```ruby
101
- friends_schema = Parametric::Schema.new do
101
+ FRIENDS_SCHEMA = Parametric::Schema.new do
102
102
  field(:friends).type(:array).schema do
103
103
  field(:name).type(:string).required
104
104
  field(:email).policy(:email)
@@ -109,10 +109,24 @@ person_schema = Parametric::Schema.new do
109
109
  field(:name).type(:string).required
110
110
  field(:age).type(:integer)
111
111
  # Nest friends_schema
112
- field(:friends).type(:array).schema(friends_schema)
112
+ field(:friends).type(:array).schema(FRIENDS_SCHEMA)
113
113
  end
114
114
  ```
115
115
 
116
+ Note that _person_schema_'s definition has access to `FRIENDS_SCHEMA` because it's a constant.
117
+ Definition blocks are run in the context of the defining schema instance by default.
118
+
119
+ To preserve the original block's context, declare two arguments in your block, the defining schema `sc` and options has.
120
+
121
+ ```ruby
122
+ person_schema = Parametric::Schema.new do |sc, options|
123
+ # this block now preserves its context. Call `sc.field` to add fields to the current schema.
124
+ sc.field(:name).type(:string).required
125
+ sc.field(:age).type(:integer)
126
+ # We now have access to local variables
127
+ sc.field(:friends).type(:array).schema(friends_schema)
128
+ end
129
+ ```
116
130
  ## Built-in policies
117
131
 
118
132
  Type coercions (the `type` method) and validations (the `validate` method) are all _policies_.
@@ -835,6 +849,130 @@ results.errors["$.Weight"] # => ["is required and value must be present"]
835
849
 
836
850
  NOTES: dynamically expanded field names are not included in `Schema#structure` metadata, and they are only processes if fields with the given expressions are present in the payload. This means that validations applied to those fields only run if keys are present in the first place.
837
851
 
852
+ ## Before and after resolve hooks
853
+
854
+ `Schema#before_resolve` can be used to register blocks to modify the entire input payload _before_ individual fields are validated and coerced.
855
+ This can be useful when you need to pre-populate fields relative to other fields' values, or fetch extra data from other sources.
856
+
857
+ ```ruby
858
+ # This example computes the value of the :slug field based on :name
859
+ schema = Parametric::Schema.new do
860
+ # Note1: These blocks run before field validations, so :name might be blank or invalid at this point.
861
+ # Note2: Before hooks _must_ return a payload hash.
862
+ before_resolve do |payload, context|
863
+ payload.merge(
864
+ slug: payload[:name].to_s.downcase.gsub(/\s+/, '-')
865
+ )
866
+ end
867
+
868
+ # You still need to define the fields you want
869
+ field(:name).type(:string).present
870
+ field(:slug).type(:string).present
871
+ end
872
+
873
+ result = schema.resolve( name: 'Joe Bloggs' )
874
+ result.output # => { name: 'Joe Bloggs', slug: 'joe-bloggs' }
875
+ ```
876
+
877
+ Before hooks can be added to nested schemas, too:
878
+
879
+ ```ruby
880
+ schema = Parametric::Schema.new do
881
+ field(:friends).type(:array).schema do
882
+ before_resolve do |friend_payload, context|
883
+ friend_payload.merge(title: "Mr/Ms #{friend_payload[:name]}")
884
+ end
885
+
886
+ field(:name).type(:string)
887
+ field(:title).type(:string)
888
+ end
889
+ end
890
+ ```
891
+
892
+ You can use inline blocks, but anything that responds to `#call(payload, context)` will work, too:
893
+
894
+ ```ruby
895
+ class SlugMaker
896
+ def initialize(slug_field, from:)
897
+ @slug_field, @from = slug_field, from
898
+ end
899
+
900
+ def call(payload, context)
901
+ payload.merge(
902
+ @slug_field => payload[@from].to_s.downcase.gsub(/\s+/, '-')
903
+ )
904
+ end
905
+ end
906
+
907
+ schema = Parametric::Schema.new do
908
+ before_resolve SlugMaker.new(:slug, from: :name)
909
+
910
+ field(:name).type(:string)
911
+ field(:slug).type(:slug)
912
+ end
913
+ ```
914
+
915
+ The `context` argument can be used to add custom validation errors in a before hook block.
916
+
917
+ ```ruby
918
+ schema = Parametric::Schema.new do
919
+ before_resolve do |payload, context|
920
+ # validate that there's no duplicate friend names
921
+ friends = payload[:friends] || []
922
+ if friends.any? && friends.map{ |fr| fr[:name] }.uniq.size < friends.size
923
+ context.add_error 'friend names must be unique'
924
+ end
925
+
926
+ # don't forget to return the payload
927
+ payload
928
+ end
929
+
930
+ field(:friends).type(:array).schema do
931
+ field(:name).type(:string)
932
+ end
933
+ end
934
+
935
+ result = schema.resolve(
936
+ friends: [
937
+ {name: 'Joe Bloggs'},
938
+ {name: 'Joan Bloggs'},
939
+ {name: 'Joe Bloggs'}
940
+ ]
941
+ )
942
+
943
+ result.valid? # => false
944
+ result.errors # => {'$' => ['friend names must be unique']}
945
+ ```
946
+
947
+ In most cases you should be validating individual fields using field policies. Only validate in before hooks in cases you have dependencies between fields.
948
+
949
+ `Schema#after_resolve` takes the sanitized input hash, and can be used to further validate fields that depend on eachother.
950
+
951
+ ```ruby
952
+ schema = Parametric::Schema.new do
953
+ after_resolve do |payload, ctx|
954
+ # Add a top level error using an arbitrary key name
955
+ ctx.add_base_error('deposit', 'cannot be greater than house price') if payload[:deposit] > payload[:house_price]
956
+ # Or add an error keyed after the current position in the schema
957
+ # ctx.add_error('some error') if some_condition
958
+ # after_resolve hooks must also return the payload, or a modified copy of it
959
+ # note that any changes added here won't be validated.
960
+ payload.merge(desc: 'hello')
961
+ end
962
+
963
+ field(:deposit).policy(:integer).present
964
+ field(:house_price).policy(:integer).present
965
+ field(:desc).policy(:string)
966
+ end
967
+
968
+ result = schema.resolve({ deposit: 1100, house_price: 1000 })
969
+ result.valid? # false
970
+ result.errors[:deposit] # ['cannot be greater than house price']
971
+ result.output[:deposit] # 1100
972
+ result.output[:house_price] # 1000
973
+ result.output[:desc] # 'hello'
974
+ ```
975
+
838
976
  ## Structs
839
977
 
840
978
  Structs turn schema definitions into objects graphs with attribute readers.
@@ -0,0 +1,53 @@
1
+ require 'benchmark/ips'
2
+ require 'parametric/struct'
3
+
4
+ StructAccount = Struct.new(:id, :email, keyword_init: true)
5
+ StructFriend = Struct.new(:name, keyword_init: true)
6
+ StructUser = Struct.new(:name, :age, :friends, :account, keyword_init: true)
7
+
8
+ class ParametricAccount
9
+ include Parametric::Struct
10
+ schema do
11
+ field(:id).type(:integer).present
12
+ field(:email).type(:string)
13
+ end
14
+ end
15
+
16
+ class ParametricUser
17
+ include Parametric::Struct
18
+ schema do
19
+ field(:name).type(:string).present
20
+ field(:age).type(:integer).default(42)
21
+ field(:friends).type(:array).schema do
22
+ field(:name).type(:string).present
23
+ end
24
+ field(:account).type(:object).schema ParametricAccount
25
+ end
26
+ end
27
+
28
+ Benchmark.ips do |x|
29
+ x.report("Struct") {
30
+ StructUser.new(
31
+ name: 'Ismael',
32
+ age: 42,
33
+ friends: [
34
+ StructFriend.new(name: 'Joe'),
35
+ StructFriend.new(name: 'Joan'),
36
+ ],
37
+ account: StructAccount.new(id: 123, email: 'my@account.com')
38
+ )
39
+ }
40
+ x.report("Parametric::Struct") {
41
+ ParametricUser.new!(
42
+ name: 'Ismael',
43
+ age: 42,
44
+ friends: [
45
+ { name: 'Joe' },
46
+ { name: 'Joan' }
47
+ ],
48
+ account: { id: 123, email: 'my@account.com' }
49
+ )
50
+ }
51
+ x.compare!
52
+ end
53
+
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "parametric/version"
2
4
  require "parametric/registry"
3
5
  require "parametric/field"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parametric
2
4
  class BlockValidator
3
5
  def self.build(meth, &block)
@@ -8,12 +10,12 @@ module Parametric
8
10
 
9
11
  def self.message(&block)
10
12
  @message_block = block if block_given?
11
- @message_block
13
+ @message_block if instance_variable_defined?('@message_block')
12
14
  end
13
15
 
14
16
  def self.validate(&validate_block)
15
17
  @validate_block = validate_block if block_given?
16
- @validate_block
18
+ @validate_block if instance_variable_defined?('@validate_block')
17
19
  end
18
20
 
19
21
  def self.coerce(&coerce_block)
@@ -23,12 +25,12 @@ module Parametric
23
25
 
24
26
  def self.eligible(&block)
25
27
  @eligible_block = block if block_given?
26
- @eligible_block
28
+ @eligible_block if instance_variable_defined?('@eligible_block')
27
29
  end
28
30
 
29
31
  def self.meta_data(&block)
30
32
  @meta_data_block = block if block_given?
31
- @meta_data_block
33
+ @meta_data_block if instance_variable_defined?('@meta_data_block')
32
34
  end
33
35
 
34
36
  attr_reader :message
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parametric
2
4
  class Top
3
5
  attr_reader :errors
@@ -26,6 +28,10 @@ module Parametric
26
28
  top.add_error(string_path, msg)
27
29
  end
28
30
 
31
+ def add_base_error(key, msg)
32
+ top.add_error(key, msg)
33
+ end
34
+
29
35
  def sub(key)
30
36
  self.class.new(path + [key], top)
31
37
  end
@@ -40,5 +46,4 @@ module Parametric
40
46
  end.join
41
47
  end
42
48
  end
43
-
44
49
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "date"
2
4
 
3
5
  module Parametric
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "parametric"
2
4
 
3
5
  module Parametric
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "parametric/field_dsl"
2
4
 
3
5
  module Parametric
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parametric
2
4
  # Field DSL
3
5
  # host instance must implement:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parametric
2
4
  module Policies
3
5
  class Format
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'parametric/block_validator'
2
4
 
3
5
  module Parametric
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parametric
2
4
  class Results
3
5
  attr_reader :output, :errors
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "parametric/context"
2
4
  require "parametric/results"
3
5
  require "parametric/field"
@@ -12,6 +14,8 @@ module Parametric
12
14
  @default_field_policies = []
13
15
  @ignored_field_keys = []
14
16
  @expansions = {}
17
+ @before_hooks = []
18
+ @after_hooks = []
15
19
  end
16
20
 
17
21
  def schema
@@ -44,11 +48,16 @@ module Parametric
44
48
  copy_into instance
45
49
  end
46
50
 
47
- def merge(other_schema)
48
- instance = self.class.new(options.merge(other_schema.options))
51
+ def merge(other_schema = nil, &block)
52
+ raise ArgumentError, '#merge takes either a schema instance or a block' if other_schema.nil? && !block_given?
49
53
 
50
- copy_into(instance)
51
- other_schema.copy_into(instance)
54
+ if other_schema
55
+ instance = self.class.new(options.merge(other_schema.options))
56
+ copy_into(instance)
57
+ other_schema.copy_into(instance)
58
+ else
59
+ merge(self.class.new(&block))
60
+ end
52
61
  end
53
62
 
54
63
  def copy_into(instance)
@@ -85,6 +94,20 @@ module Parametric
85
94
  end
86
95
  end
87
96
 
97
+ def before_resolve(klass = nil, &block)
98
+ raise ArgumentError, '#before_resolve expects a callable object, or a block' if !klass && !block_given?
99
+ callable = klass || block
100
+ before_hooks << callable
101
+ self
102
+ end
103
+
104
+ def after_resolve(klass = nil, &block)
105
+ raise ArgumentError, '#after_resolve expects a callable object, or a block' if !klass && !block_given?
106
+ callable = klass || block
107
+ after_hooks << callable
108
+ self
109
+ end
110
+
88
111
  def expand(exp, &block)
89
112
  expansions[exp] = block
90
113
  self
@@ -134,19 +157,35 @@ module Parametric
134
157
 
135
158
  protected
136
159
 
137
- attr_reader :definitions, :options
160
+ attr_reader :definitions, :options, :before_hooks, :after_hooks
138
161
 
139
162
  private
140
163
 
141
164
  attr_reader :default_field_policies, :ignored_field_keys, :expansions
142
165
 
143
166
  def coerce_one(val, context, flds: fields)
144
- flds.each_with_object({}) do |(_, field), m|
167
+ val = run_before_hooks(val, context)
168
+
169
+ out = flds.each_with_object({}) do |(_, field), m|
145
170
  r = field.resolve(val, context.sub(field.key))
146
171
  if r.eligible?
147
172
  m[field.key] = r.value
148
173
  end
149
174
  end
175
+
176
+ run_after_hooks(out, context)
177
+ end
178
+
179
+ def run_before_hooks(val, context)
180
+ before_hooks.reduce(val) do |value, callable|
181
+ callable.call(value, context)
182
+ end
183
+ end
184
+
185
+ def run_after_hooks(val, context)
186
+ after_hooks.reduce(val) do |value, callable|
187
+ callable.call(value, context)
188
+ end
150
189
  end
151
190
 
152
191
  class MatchContext
@@ -177,7 +216,11 @@ module Parametric
177
216
  def apply!
178
217
  return if @applied
179
218
  definitions.each do |d|
180
- self.instance_exec(options, &d)
219
+ if d.arity == 2 # pass schema instance and options, preserve block context
220
+ d.call(self, options)
221
+ else # run block in context of current instance
222
+ self.instance_exec(options, &d)
223
+ end
181
224
  end
182
225
  @applied = true
183
226
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'parametric/dsl'
2
4
 
3
5
  module Parametric
@@ -56,10 +58,24 @@ module Parametric
56
58
 
57
59
  # this hook is called after schema definition in DSL module
58
60
  def parametric_after_define_schema(schema)
59
- schema.fields.keys.each do |key|
60
- define_method key do
61
- _graph[key]
61
+ schema.fields.values.each do |field|
62
+ if field.meta_data[:schema]
63
+ if field.meta_data[:schema].is_a?(Parametric::Schema)
64
+ klass = Class.new do
65
+ include Struct
66
+ end
67
+ klass.schema = field.meta_data[:schema]
68
+ self.const_set(__class_name(field.key), klass)
69
+ klass.parametric_after_define_schema(field.meta_data[:schema])
70
+ else
71
+ self.const_set(__class_name(field.key), field.meta_data[:schema])
72
+ end
62
73
  end
74
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
75
+ def #{field.key}
76
+ _graph[:#{field.key}]
77
+ end
78
+ RUBY
63
79
  end
64
80
  end
65
81
 
@@ -69,27 +85,11 @@ module Parametric
69
85
  end
70
86
  end
71
87
 
72
- def parametric_build_class_for_child(key, child_schema)
73
- klass = Class.new do
74
- include Struct
75
- end
76
- klass.schema = child_schema
77
- klass
78
- end
79
-
80
88
  def wrap(key, value)
81
- field = schema.fields[key]
82
- return value unless field
83
-
84
89
  case value
85
90
  when Hash
86
91
  # find constructor for field
87
- cons = field.meta_data[:schema]
88
- if cons.kind_of?(Parametric::Schema)
89
- klass = parametric_build_class_for_child(key, cons)
90
- klass.parametric_after_define_schema(cons)
91
- cons = klass
92
- end
92
+ cons = self.const_get(__class_name(key))
93
93
  cons ? cons.new(value) : value.freeze
94
94
  when Array
95
95
  value.map{|v| wrap(key, v) }.freeze
@@ -97,6 +97,12 @@ module Parametric
97
97
  value.freeze
98
98
  end
99
99
  end
100
+
101
+ PLURAL_END = /s$/.freeze
102
+
103
+ def __class_name(key)
104
+ key.to_s.split('_').map(&:capitalize).join.sub(PLURAL_END, '')
105
+ end
100
106
  end
101
107
  end
102
108
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parametric
2
- VERSION = "0.2.7"
4
+ VERSION = "0.2.12"
3
5
  end
@@ -17,7 +17,6 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_development_dependency "bundler", "~> 1.5"
21
20
  spec.add_development_dependency "rake"
22
21
  spec.add_development_dependency "rspec", '3.4.0'
23
22
  spec.add_development_dependency "byebug"
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe 'default coercions' do
4
4
  def test_coercion(key, value, expected)
5
- coercion = Parametric.registry.coercions[key]
5
+ coercion = Parametric.registry.policies[key]
6
6
  expect(coercion.new.coerce(value, nil, nil)).to eq expected
7
7
  end
8
8
 
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parametric::Schema do
4
+ describe '#before_resolve' do
5
+ it 'passes payload through before_resolve block, if defined' do
6
+ schema = described_class.new do
7
+ before_resolve do |payload, _context|
8
+ payload[:slug] = payload[:name].to_s.downcase.gsub(/\s+/, '-') unless payload[:slug]
9
+ payload
10
+ end
11
+
12
+ field(:name).policy(:string).present
13
+ field(:slug).policy(:string).present
14
+ field(:variants).policy(:array).schema do
15
+ before_resolve do |payload, _context|
16
+ payload[:slug] = "v: #{payload[:name].to_s.downcase}"
17
+ payload
18
+ end
19
+ field(:name).policy(:string).present
20
+ field(:slug).type(:string).present
21
+ end
22
+ end
23
+
24
+ result = schema.resolve({ name: 'A name', variants: [{ name: 'A variant' }] })
25
+ expect(result.valid?).to be true
26
+ expect(result.output[:slug]).to eq 'a-name'
27
+ expect(result.output[:variants].first[:slug]).to eq 'v: a variant'
28
+ end
29
+
30
+ it 'collects errors added in before_resolve blocks' do
31
+ schema = described_class.new do
32
+ field(:variants).type(:array).schema do
33
+ before_resolve do |payload, context|
34
+ context.add_error 'nope!' if payload[:name] == 'with errors'
35
+ payload
36
+ end
37
+ field(:name).type(:string)
38
+ end
39
+ end
40
+
41
+ results = schema.resolve({ variants: [ {name: 'no errors'}, {name: 'with errors'}]})
42
+ expect(results.valid?).to be false
43
+ expect(results.errors['$.variants[1]']).to eq ['nope!']
44
+ end
45
+
46
+ it 'copies before_resolve hooks to merged schemas' do
47
+ schema1 = described_class.new do
48
+ before_resolve do |payload, _context|
49
+ payload[:slug] = payload[:name].to_s.downcase.gsub(/\s+/, '-') unless payload[:slug]
50
+ payload
51
+ end
52
+ field(:name).present.type(:string)
53
+ field(:slug).present.type(:string)
54
+ end
55
+
56
+ schema2 = described_class.new do
57
+ before_resolve do |payload, _context|
58
+ payload[:slug] = "slug-#{payload[:slug]}" if payload[:slug]
59
+ payload
60
+ end
61
+
62
+ field(:age).type(:integer)
63
+ end
64
+
65
+ schema3 = schema1.merge(schema2)
66
+
67
+ results = schema3.resolve({ name: 'Ismael Celis', age: 41 })
68
+ expect(results.output[:slug]).to eq 'slug-ismael-celis'
69
+ end
70
+
71
+ it 'works with any callable' do
72
+ slug_maker = Class.new do
73
+ def initialize(slug_field, from:)
74
+ @slug_field, @from = slug_field, from
75
+ end
76
+
77
+ def call(payload, _context)
78
+ payload.merge(
79
+ @slug_field => payload[@from].to_s.downcase.gsub(/\s+/, '-')
80
+ )
81
+ end
82
+ end
83
+
84
+ schema = described_class.new do |sc, _opts|
85
+ sc.before_resolve slug_maker.new(:slug, from: :name)
86
+
87
+ sc.field(:name).type(:string)
88
+ sc.field(:slug).type(:string)
89
+ end
90
+
91
+ results = schema.resolve(name: 'Ismael Celis')
92
+ expect(results.output[:slug]).to eq 'ismael-celis'
93
+ end
94
+ end
95
+
96
+ describe '#after_resolve' do
97
+ let!(:schema) do
98
+ described_class.new do
99
+ after_resolve do |payload, ctx|
100
+ ctx.add_base_error('deposit', 'cannot be greater than house price') if payload[:deposit] > payload[:house_price]
101
+ payload.merge(desc: 'hello')
102
+ end
103
+
104
+ field(:deposit).policy(:integer).present
105
+ field(:house_price).policy(:integer).present
106
+ field(:desc).policy(:string)
107
+ end
108
+ end
109
+
110
+ it 'passes payload through after_resolve block, if defined' do
111
+ result = schema.resolve({ deposit: 1100, house_price: 1000 })
112
+ expect(result.valid?).to be false
113
+ expect(result.output[:deposit]).to eq 1100
114
+ expect(result.output[:house_price]).to eq 1000
115
+ expect(result.output[:desc]).to eq 'hello'
116
+ end
117
+
118
+ it 'copies after hooks when merging schemas' do
119
+ child_schema = described_class.new do
120
+ field(:name).type(:string)
121
+ end
122
+
123
+ union = schema.merge(child_schema)
124
+
125
+ result = union.resolve({ name: 'Joe', deposit: 1100, house_price: 1000 })
126
+ expect(result.valid?).to be false
127
+ expect(result.output[:deposit]).to eq 1100
128
+ expect(result.output[:house_price]).to eq 1000
129
+ expect(result.output[:desc]).to eq 'hello'
130
+ expect(result.output[:name]).to eq 'Joe'
131
+ end
132
+ end
133
+ end
@@ -169,6 +169,17 @@ describe Parametric::Schema do
169
169
  expect(new_schema.fields[:title].meta_data[:type]).to eq :string
170
170
  expect(new_schema.fields[:price].meta_data[:type]).to eq :string
171
171
  end
172
+
173
+ it 'can merge from a block' do
174
+ new_schema = schema1.merge do
175
+ field(:price).policy(:string)
176
+ field(:description).policy(:string)
177
+ end
178
+
179
+ expect(schema1.fields[:price].meta_data[:type]).to eq :integer
180
+ expect(new_schema.fields[:title].meta_data[:type]).to eq :string
181
+ expect(new_schema.fields[:price].meta_data[:type]).to eq :string
182
+ end
172
183
  end
173
184
 
174
185
  context "with options" do
@@ -226,6 +237,20 @@ describe Parametric::Schema do
226
237
  end
227
238
  end
228
239
 
240
+ context 'yielding schema to definition, to preserve outer context' do
241
+ it 'yields schema instance and options to definition block, can access outer context' do
242
+ schema1 = described_class.new do
243
+ field(:name).type(:string)
244
+ end
245
+ schema2 = described_class.new do |sc, _opts|
246
+ sc.field(:user).schema schema1
247
+ end
248
+
249
+ out = schema2.resolve(user: { name: 'Joe' }).output
250
+ expect(out[:user][:name]).to eq 'Joe'
251
+ end
252
+ end
253
+
229
254
  describe "#ignore" do
230
255
  it "ignores fields" do
231
256
  s1 = described_class.new.ignore(:title, :status) do
@@ -99,32 +99,6 @@ describe Parametric::Struct do
99
99
  expect(instance.friends.first.age).to eq 10
100
100
  end
101
101
 
102
- it 'wraps nested schemas in custom class' do
103
- klass = Class.new do
104
- include Parametric::Struct
105
-
106
- def self.parametric_build_class_for_child(key, child_schema)
107
- Class.new do
108
- include Parametric::Struct
109
- schema child_schema
110
- def salutation
111
- "my age is #{age}"
112
- end
113
- end
114
- end
115
-
116
- schema do
117
- field(:name).type(:string).present
118
- field(:friends).type(:array).schema do
119
- field(:age).type(:integer)
120
- end
121
- end
122
- end
123
-
124
- user = klass.new(name: 'Ismael', friends: [{age: 43}])
125
- expect(user.friends.first.salutation).to eq 'my age is 43'
126
- end
127
-
128
102
  it "wraps regular schemas in structs" do
129
103
  friend_schema = Parametric::Schema.new do
130
104
  field(:name)
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parametric
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-05 00:00:00.000000000 Z
11
+ date: 2020-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.5'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.5'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rake
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -81,6 +67,7 @@ files:
81
67
  - LICENSE.txt
82
68
  - README.md
83
69
  - Rakefile
70
+ - bench/struct_bench.rb
84
71
  - bin/console
85
72
  - lib/parametric.rb
86
73
  - lib/parametric/block_validator.rb
@@ -101,6 +88,7 @@ files:
101
88
  - spec/expand_spec.rb
102
89
  - spec/field_spec.rb
103
90
  - spec/policies_spec.rb
91
+ - spec/schema_lifecycle_hooks_spec.rb
104
92
  - spec/schema_spec.rb
105
93
  - spec/schema_walk_spec.rb
106
94
  - spec/spec_helper.rb
@@ -110,7 +98,7 @@ homepage: ''
110
98
  licenses:
111
99
  - MIT
112
100
  metadata: {}
113
- post_install_message:
101
+ post_install_message:
114
102
  rdoc_options: []
115
103
  require_paths:
116
104
  - lib
@@ -125,9 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
113
  - !ruby/object:Gem::Version
126
114
  version: '0'
127
115
  requirements: []
128
- rubyforge_project:
129
- rubygems_version: 2.7.7
130
- signing_key:
116
+ rubygems_version: 3.0.3
117
+ signing_key:
131
118
  specification_version: 4
132
119
  summary: DSL for declaring allowed parameters with options, regexp patern and default
133
120
  values.
@@ -137,6 +124,7 @@ test_files:
137
124
  - spec/expand_spec.rb
138
125
  - spec/field_spec.rb
139
126
  - spec/policies_spec.rb
127
+ - spec/schema_lifecycle_hooks_spec.rb
140
128
  - spec/schema_spec.rb
141
129
  - spec/schema_walk_spec.rb
142
130
  - spec/spec_helper.rb