parametric 0.0.1 → 0.2.12
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 +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +2 -1
- data/Gemfile +4 -0
- data/README.md +1017 -96
- data/bench/struct_bench.rb +53 -0
- data/bin/console +14 -0
- data/lib/parametric/block_validator.rb +66 -0
- data/lib/parametric/context.rb +49 -0
- data/lib/parametric/default_types.rb +97 -0
- data/lib/parametric/dsl.rb +70 -0
- data/lib/parametric/field.rb +113 -0
- data/lib/parametric/field_dsl.rb +26 -0
- data/lib/parametric/policies.rb +111 -38
- data/lib/parametric/registry.rb +23 -0
- data/lib/parametric/results.rb +15 -0
- data/lib/parametric/schema.rb +228 -0
- data/lib/parametric/struct.rb +108 -0
- data/lib/parametric/version.rb +3 -1
- data/lib/parametric.rb +18 -5
- data/parametric.gemspec +2 -3
- data/spec/custom_block_validator_spec.rb +21 -0
- data/spec/dsl_spec.rb +176 -0
- data/spec/expand_spec.rb +29 -0
- data/spec/field_spec.rb +430 -0
- data/spec/policies_spec.rb +72 -0
- data/spec/schema_lifecycle_hooks_spec.rb +133 -0
- data/spec/schema_spec.rb +289 -0
- data/spec/schema_walk_spec.rb +42 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/struct_spec.rb +298 -0
- data/spec/validators_spec.rb +106 -0
- metadata +49 -23
- data/lib/parametric/hash.rb +0 -36
- data/lib/parametric/params.rb +0 -60
- data/lib/parametric/utils.rb +0 -24
- data/spec/parametric_spec.rb +0 -182
data/spec/schema_spec.rb
ADDED
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Parametric::Schema do
|
4
|
+
before do
|
5
|
+
Parametric.policy :flexible_bool do
|
6
|
+
coerce do |v, k, c|
|
7
|
+
case v
|
8
|
+
when '1', 'true', 'TRUE', true
|
9
|
+
true
|
10
|
+
else
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
subject do
|
18
|
+
described_class.new do
|
19
|
+
field(:title).policy(:string).present
|
20
|
+
field(:price).policy(:integer).meta(label: "A price")
|
21
|
+
field(:status).policy(:string).options(['visible', 'hidden'])
|
22
|
+
field(:tags).policy(:split).policy(:array)
|
23
|
+
field(:description).policy(:string)
|
24
|
+
field(:variants).policy(:array).schema do
|
25
|
+
field(:name).policy(:string).present
|
26
|
+
field(:sku)
|
27
|
+
field(:stock).policy(:integer).default(1)
|
28
|
+
field(:available_if_no_stock).policy(:boolean).policy(:flexible_bool).default(false)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#structure" do
|
34
|
+
it "represents data structure and meta data" do
|
35
|
+
sc = subject.structure
|
36
|
+
expect(sc[:title][:present]).to be true
|
37
|
+
expect(sc[:title][:type]).to eq :string
|
38
|
+
expect(sc[:price][:type]).to eq :integer
|
39
|
+
expect(sc[:price][:label]).to eq "A price"
|
40
|
+
expect(sc[:variants][:type]).to eq :array
|
41
|
+
sc[:variants][:structure].tap do |sc|
|
42
|
+
expect(sc[:name][:type]).to eq :string
|
43
|
+
expect(sc[:name][:present]).to be true
|
44
|
+
expect(sc[:stock][:default]).to eq 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve(schema, payload, &block)
|
50
|
+
yield schema.resolve(payload)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_schema(schema, payload, result)
|
54
|
+
resolve(schema, payload) do |results|
|
55
|
+
expect(results.output).to eq result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'works' do
|
60
|
+
test_schema(subject, {
|
61
|
+
title: 'iPhone 6 Plus',
|
62
|
+
price: '100.0',
|
63
|
+
status: 'visible',
|
64
|
+
tags: 'tag1, tag2',
|
65
|
+
description: 'A description',
|
66
|
+
variants: [{name: 'v1', sku: 'ABC', stock: '10', available_if_no_stock: true}]
|
67
|
+
},
|
68
|
+
{
|
69
|
+
title: 'iPhone 6 Plus',
|
70
|
+
price: 100,
|
71
|
+
status: 'visible',
|
72
|
+
tags: ['tag1', 'tag2'],
|
73
|
+
description: 'A description',
|
74
|
+
variants: [{name: 'v1', sku: 'ABC', stock: 10, available_if_no_stock: true}]
|
75
|
+
})
|
76
|
+
|
77
|
+
test_schema(subject, {
|
78
|
+
title: 'iPhone 6 Plus',
|
79
|
+
variants: [{name: 'v1', available_if_no_stock: '1'}]
|
80
|
+
},
|
81
|
+
{
|
82
|
+
title: 'iPhone 6 Plus',
|
83
|
+
variants: [{name: 'v1', stock: 1, available_if_no_stock: true}]
|
84
|
+
})
|
85
|
+
|
86
|
+
resolve(subject, {}) do |results|
|
87
|
+
expect(results.valid?).to be false
|
88
|
+
expect(results.errors['$.title']).not_to be_nil
|
89
|
+
expect(results.errors['$.variants']).to be_nil
|
90
|
+
expect(results.errors['$.status']).to be_nil
|
91
|
+
end
|
92
|
+
|
93
|
+
resolve(subject, {title: 'Foobar', variants: [{name: 'v1'}, {sku: '345'}]}) do |results|
|
94
|
+
expect(results.valid?).to be false
|
95
|
+
expect(results.errors['$.variants[1].name']).not_to be_nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "ignores nil fields if using :declared policy" do
|
100
|
+
schema = described_class.new do
|
101
|
+
field(:id).type(:integer)
|
102
|
+
field(:title).declared.type(:string)
|
103
|
+
end
|
104
|
+
|
105
|
+
resolve(schema, {id: 123}) do |results|
|
106
|
+
expect(results.output.keys).to eq [:id]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#policy" do
|
111
|
+
it "applies policy to all fields" do
|
112
|
+
subject.policy(:declared)
|
113
|
+
|
114
|
+
resolve(subject, {}) do |results|
|
115
|
+
expect(results.valid?).to be true
|
116
|
+
expect(results.errors.keys).to be_empty
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
it "replaces previous policies" do
|
121
|
+
subject.policy(:declared)
|
122
|
+
subject.policy(:present)
|
123
|
+
|
124
|
+
resolve(subject, {title: "hello"}) do |results|
|
125
|
+
expect(results.valid?).to be false
|
126
|
+
expect(results.errors.keys).to match_array(%w(
|
127
|
+
$.price
|
128
|
+
$.status
|
129
|
+
$.tags
|
130
|
+
$.description
|
131
|
+
$.variants
|
132
|
+
))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it "applies :noop policy to all fields" do
|
137
|
+
subject.policy(:noop)
|
138
|
+
|
139
|
+
resolve(subject, {}) do |results|
|
140
|
+
expect(results.valid?).to be false
|
141
|
+
expect(results.errors['$.title']).not_to be_nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#merge" do
|
147
|
+
context "no options" do
|
148
|
+
let!(:schema1) {
|
149
|
+
described_class.new do
|
150
|
+
field(:title).policy(:string).present
|
151
|
+
field(:price).policy(:integer)
|
152
|
+
end
|
153
|
+
}
|
154
|
+
|
155
|
+
let!(:schema2) {
|
156
|
+
described_class.new do
|
157
|
+
field(:price).policy(:string)
|
158
|
+
field(:description).policy(:string)
|
159
|
+
end
|
160
|
+
}
|
161
|
+
|
162
|
+
it "returns a new schema adding new fields and updating existing ones" do
|
163
|
+
new_schema = schema1.merge(schema2)
|
164
|
+
expect(new_schema.fields.keys).to match_array([:title, :price, :description])
|
165
|
+
|
166
|
+
# did not mutate original
|
167
|
+
expect(schema1.fields[:price].meta_data[:type]).to eq :integer
|
168
|
+
|
169
|
+
expect(new_schema.fields[:title].meta_data[:type]).to eq :string
|
170
|
+
expect(new_schema.fields[:price].meta_data[:type]).to eq :string
|
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
|
183
|
+
end
|
184
|
+
|
185
|
+
context "with options" do
|
186
|
+
let!(:schema1) {
|
187
|
+
described_class.new(price_type: :integer, label: "Foo") do |opts|
|
188
|
+
field(:title).policy(:string).present
|
189
|
+
field(:price).policy(opts[:price_type]).meta(label: opts[:label])
|
190
|
+
end
|
191
|
+
}
|
192
|
+
|
193
|
+
let!(:schema2) {
|
194
|
+
described_class.new(price_type: :string) do
|
195
|
+
field(:description).policy(:string)
|
196
|
+
end
|
197
|
+
}
|
198
|
+
|
199
|
+
it "inherits options" do
|
200
|
+
new_schema = schema1.merge(schema2)
|
201
|
+
expect(new_schema.fields[:price].meta_data[:type]).to eq :string
|
202
|
+
expect(new_schema.fields[:price].meta_data[:label]).to eq "Foo"
|
203
|
+
end
|
204
|
+
|
205
|
+
it "re-applies blocks with new options" do
|
206
|
+
new_schema = schema1.merge(schema2)
|
207
|
+
expect(new_schema.fields.keys).to match_array([:title, :price, :description])
|
208
|
+
|
209
|
+
# did not mutate original
|
210
|
+
expect(schema1.fields[:price].meta_data[:type]).to eq :integer
|
211
|
+
|
212
|
+
expect(new_schema.fields[:title].meta_data[:type]).to eq :string
|
213
|
+
expect(new_schema.fields[:price].meta_data[:type]).to eq :string
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe "#clone" do
|
219
|
+
let!(:schema1) {
|
220
|
+
described_class.new do |opts|
|
221
|
+
field(:id).present
|
222
|
+
field(:title).policy(:string).present
|
223
|
+
field(:price)
|
224
|
+
end
|
225
|
+
}
|
226
|
+
|
227
|
+
it "returns a copy that can be further manipulated" do
|
228
|
+
schema2 = schema1.clone.policy(:declared).ignore(:id)
|
229
|
+
expect(schema1.fields.keys).to match_array([:id, :title, :price])
|
230
|
+
expect(schema2.fields.keys).to match_array([:title, :price])
|
231
|
+
|
232
|
+
results1 = schema1.resolve(id: "abc", price: 100)
|
233
|
+
expect(results1.errors.keys).to eq ["$.title"]
|
234
|
+
|
235
|
+
results2 = schema2.resolve(id: "abc", price: 100)
|
236
|
+
expect(results2.errors.keys).to eq []
|
237
|
+
end
|
238
|
+
end
|
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
|
+
|
254
|
+
describe "#ignore" do
|
255
|
+
it "ignores fields" do
|
256
|
+
s1 = described_class.new.ignore(:title, :status) do
|
257
|
+
field(:status)
|
258
|
+
field(:title).policy(:string).present
|
259
|
+
field(:price).policy(:integer)
|
260
|
+
end
|
261
|
+
|
262
|
+
output = s1.resolve(status: "draft", title: "foo", price: "100").output
|
263
|
+
expect(output).to eq({price: 100})
|
264
|
+
end
|
265
|
+
|
266
|
+
it "ignores when merging" do
|
267
|
+
s1 = described_class.new do
|
268
|
+
field(:status)
|
269
|
+
field(:title).policy(:string).present
|
270
|
+
end
|
271
|
+
|
272
|
+
s1 = described_class.new.ignore(:title, :status) do
|
273
|
+
field(:price).policy(:integer)
|
274
|
+
end
|
275
|
+
|
276
|
+
output = s1.resolve(title: "foo", status: "draft", price: "100").output
|
277
|
+
expect(output).to eq({price: 100})
|
278
|
+
end
|
279
|
+
|
280
|
+
it "returns self so it can be chained" do
|
281
|
+
s1 = described_class.new do
|
282
|
+
field(:status)
|
283
|
+
field(:title).policy(:string).present
|
284
|
+
end
|
285
|
+
|
286
|
+
expect(s1.ignore(:status)).to eq s1
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Schema#walk' do
|
4
|
+
let(:schema) do
|
5
|
+
Parametric::Schema.new do
|
6
|
+
field(:title).meta(example: 'a title', label: 'custom title')
|
7
|
+
field(:tags).policy(:array).meta(example: ['tag1', 'tag2'], label: 'comma-separated tags')
|
8
|
+
field(:friends).policy(:array).schema do
|
9
|
+
field(:name).meta(example: 'a friend', label: 'friend full name')
|
10
|
+
field(:age).meta(example: 34, label: 'age')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "recursively walks the schema and collects meta data" do
|
16
|
+
results = schema.walk(:label)
|
17
|
+
expect(results.output).to eq({
|
18
|
+
title: 'custom title',
|
19
|
+
tags: 'comma-separated tags',
|
20
|
+
friends: [
|
21
|
+
{
|
22
|
+
name: 'friend full name',
|
23
|
+
age: 'age'
|
24
|
+
}
|
25
|
+
]
|
26
|
+
})
|
27
|
+
end
|
28
|
+
|
29
|
+
it "works with blocks" do
|
30
|
+
results = schema.walk{|field| field.meta_data[:example]}
|
31
|
+
expect(results.output).to eq({
|
32
|
+
title: 'a title',
|
33
|
+
tags: ['tag1', 'tag2'],
|
34
|
+
friends: [
|
35
|
+
{
|
36
|
+
name: 'a friend',
|
37
|
+
age: 34
|
38
|
+
}
|
39
|
+
]
|
40
|
+
})
|
41
|
+
end
|
42
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/struct_spec.rb
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'parametric/struct'
|
3
|
+
|
4
|
+
describe Parametric::Struct do
|
5
|
+
it "works" do
|
6
|
+
friend_class = Class.new do
|
7
|
+
include Parametric::Struct
|
8
|
+
|
9
|
+
schema do
|
10
|
+
field(:name).type(:string).present
|
11
|
+
field(:age).type(:integer)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
klass = Class.new do
|
16
|
+
include Parametric::Struct
|
17
|
+
|
18
|
+
schema do
|
19
|
+
field(:title).type(:string).present
|
20
|
+
field(:friends).type(:array).default([]).schema friend_class
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
new_instance = klass.new
|
25
|
+
expect(new_instance.title).to eq ''
|
26
|
+
expect(new_instance.friends).to eq []
|
27
|
+
expect(new_instance.valid?).to be false
|
28
|
+
expect(new_instance.errors['$.title']).not_to be_nil
|
29
|
+
|
30
|
+
instance = klass.new({
|
31
|
+
title: 'foo',
|
32
|
+
friends: [
|
33
|
+
{name: 'Ismael', age: 40},
|
34
|
+
{name: 'Joe', age: 39},
|
35
|
+
]
|
36
|
+
})
|
37
|
+
|
38
|
+
expect(instance.title).to eq 'foo'
|
39
|
+
expect(instance.friends.size).to eq 2
|
40
|
+
expect(instance.friends.first.name).to eq 'Ismael'
|
41
|
+
expect(instance.friends.first).to be_a friend_class
|
42
|
+
|
43
|
+
invalid_instance = klass.new({
|
44
|
+
friends: [
|
45
|
+
{name: 'Ismael', age: 40},
|
46
|
+
{age: 39},
|
47
|
+
]
|
48
|
+
})
|
49
|
+
|
50
|
+
expect(invalid_instance.valid?).to be false
|
51
|
+
expect(invalid_instance.errors['$.title']).not_to be_nil
|
52
|
+
expect(invalid_instance.errors['$.friends[1].name']).not_to be_nil
|
53
|
+
expect(invalid_instance.friends[1].errors['$.name']).not_to be_nil
|
54
|
+
end
|
55
|
+
|
56
|
+
it "is inmutable by default" do
|
57
|
+
klass = Class.new do
|
58
|
+
include Parametric::Struct
|
59
|
+
|
60
|
+
schema do
|
61
|
+
field(:title).type(:string).present
|
62
|
+
field(:friends).type(:array).default([])
|
63
|
+
field(:friend).type(:object)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
instance = klass.new
|
68
|
+
expect {
|
69
|
+
instance.title = "foo"
|
70
|
+
}.to raise_error NoMethodError
|
71
|
+
|
72
|
+
expect {
|
73
|
+
instance.friends << 1
|
74
|
+
}.to raise_error RuntimeError
|
75
|
+
end
|
76
|
+
|
77
|
+
it "works with anonymous nested schemas" do
|
78
|
+
klass = Class.new do
|
79
|
+
include Parametric::Struct
|
80
|
+
|
81
|
+
schema do
|
82
|
+
field(:title).type(:string).present
|
83
|
+
field(:friends).type(:array).schema do
|
84
|
+
field(:age).type(:integer)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
instance = klass.new({
|
90
|
+
title: 'foo',
|
91
|
+
friends: [
|
92
|
+
{age: 10},
|
93
|
+
{age: 39},
|
94
|
+
]
|
95
|
+
})
|
96
|
+
|
97
|
+
expect(instance.title).to eq 'foo'
|
98
|
+
expect(instance.friends.size).to eq 2
|
99
|
+
expect(instance.friends.first.age).to eq 10
|
100
|
+
end
|
101
|
+
|
102
|
+
it "wraps regular schemas in structs" do
|
103
|
+
friend_schema = Parametric::Schema.new do
|
104
|
+
field(:name)
|
105
|
+
end
|
106
|
+
|
107
|
+
klass = Class.new do
|
108
|
+
include Parametric::Struct
|
109
|
+
|
110
|
+
schema do
|
111
|
+
field(:title).type(:string).present
|
112
|
+
field(:friends).type(:array).schema friend_schema
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
instance = klass.new({
|
117
|
+
title: 'foo',
|
118
|
+
friends: [{name: 'Ismael'}]
|
119
|
+
})
|
120
|
+
|
121
|
+
expect(instance.friends.first.name).to eq 'Ismael'
|
122
|
+
end
|
123
|
+
|
124
|
+
it "#to_h" do
|
125
|
+
klass = Class.new do
|
126
|
+
include Parametric::Struct
|
127
|
+
|
128
|
+
schema do
|
129
|
+
field(:title).type(:string).present
|
130
|
+
field(:friends).type(:array).schema do
|
131
|
+
field(:name).type(:string)
|
132
|
+
field(:age).type(:integer).default(20)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
instance = klass.new({
|
138
|
+
title: 'foo',
|
139
|
+
friends: [
|
140
|
+
{name: 'Jane'},
|
141
|
+
{name: 'Joe', age: '39'},
|
142
|
+
]
|
143
|
+
})
|
144
|
+
|
145
|
+
expect(instance.to_h).to eq({
|
146
|
+
title: 'foo',
|
147
|
+
friends: [
|
148
|
+
{name: 'Jane', age: 20},
|
149
|
+
{name: 'Joe', age: 39},
|
150
|
+
]
|
151
|
+
})
|
152
|
+
|
153
|
+
new_instance = klass.new(instance.to_h)
|
154
|
+
expect(new_instance.title).to eq 'foo'
|
155
|
+
|
156
|
+
# it returns a copy so we can't break things!
|
157
|
+
data = new_instance.to_h
|
158
|
+
data[:title] = 'nope'
|
159
|
+
expect(new_instance.to_h[:title]).to eq 'foo'
|
160
|
+
end
|
161
|
+
|
162
|
+
it "works with inheritance" do
|
163
|
+
klass = Class.new do
|
164
|
+
include Parametric::Struct
|
165
|
+
|
166
|
+
schema do
|
167
|
+
field(:title).type(:string).present
|
168
|
+
field(:friends).type(:array).schema do
|
169
|
+
field(:name).type(:string)
|
170
|
+
field(:age).type(:integer).default(20)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
subclass = Class.new(klass) do
|
176
|
+
schema do
|
177
|
+
field(:email)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
instance = subclass.new(
|
182
|
+
title: 'foo',
|
183
|
+
email: 'email@me.com',
|
184
|
+
friends: [
|
185
|
+
{name: 'Jane', age: 20},
|
186
|
+
{name: 'Joe', age: 39},
|
187
|
+
]
|
188
|
+
)
|
189
|
+
|
190
|
+
expect(instance.title).to eq 'foo'
|
191
|
+
expect(instance.email).to eq 'email@me.com'
|
192
|
+
expect(instance.friends.size).to eq 2
|
193
|
+
end
|
194
|
+
|
195
|
+
it "implements deep struct equality" do
|
196
|
+
klass = Class.new do
|
197
|
+
include Parametric::Struct
|
198
|
+
|
199
|
+
schema do
|
200
|
+
field(:title).type(:string).present
|
201
|
+
field(:friends).type(:array).schema do
|
202
|
+
field(:age).type(:integer)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
s1 = klass.new({
|
208
|
+
title: 'foo',
|
209
|
+
friends: [
|
210
|
+
{age: 10},
|
211
|
+
{age: 39},
|
212
|
+
]
|
213
|
+
})
|
214
|
+
|
215
|
+
|
216
|
+
s2 = klass.new({
|
217
|
+
title: 'foo',
|
218
|
+
friends: [
|
219
|
+
{age: 10},
|
220
|
+
{age: 39},
|
221
|
+
]
|
222
|
+
})
|
223
|
+
|
224
|
+
s3 = klass.new({
|
225
|
+
title: 'foo',
|
226
|
+
friends: [
|
227
|
+
{age: 11},
|
228
|
+
{age: 39},
|
229
|
+
]
|
230
|
+
})
|
231
|
+
|
232
|
+
s4 = klass.new({
|
233
|
+
title: 'bar',
|
234
|
+
friends: [
|
235
|
+
{age: 10},
|
236
|
+
{age: 39},
|
237
|
+
]
|
238
|
+
})
|
239
|
+
|
240
|
+
expect(s1 == s2).to be true
|
241
|
+
expect(s1 == s3).to be false
|
242
|
+
expect(s1 == s4).to be false
|
243
|
+
end
|
244
|
+
|
245
|
+
it "#merge returns a new instance" do
|
246
|
+
klass = Class.new do
|
247
|
+
include Parametric::Struct
|
248
|
+
|
249
|
+
schema do
|
250
|
+
field(:title).type(:string).present
|
251
|
+
field(:desc)
|
252
|
+
field(:friends).type(:array).schema do
|
253
|
+
field(:name).type(:string)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
original = klass.new(
|
259
|
+
title: 'foo',
|
260
|
+
desc: 'no change',
|
261
|
+
friends: [{name: 'joe'}]
|
262
|
+
)
|
263
|
+
|
264
|
+
copy = original.merge(
|
265
|
+
title: 'bar',
|
266
|
+
friends: [{name: 'jane'}]
|
267
|
+
)
|
268
|
+
|
269
|
+
expect(original.title).to eq 'foo'
|
270
|
+
expect(original.desc).to eq 'no change'
|
271
|
+
expect(original.friends.first.name).to eq 'joe'
|
272
|
+
|
273
|
+
expect(copy.title).to eq 'bar'
|
274
|
+
expect(copy.desc).to eq 'no change'
|
275
|
+
expect(copy.friends.first.name).to eq 'jane'
|
276
|
+
end
|
277
|
+
|
278
|
+
describe '.new!' do
|
279
|
+
it 'raises a useful exception if invalid data' do
|
280
|
+
klass = Class.new do
|
281
|
+
include Parametric::Struct
|
282
|
+
|
283
|
+
schema do
|
284
|
+
field(:title).type(:string).present
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
begin
|
289
|
+
klass.new!(title: '')
|
290
|
+
rescue Parametric::InvalidStructError => e
|
291
|
+
expect(e.errors['$.title']).not_to be nil
|
292
|
+
end
|
293
|
+
|
294
|
+
valid = klass.new!(title: 'foo')
|
295
|
+
expect(valid.title).to eq 'foo'
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|