parametric 0.0.1 → 0.2.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,2 +1,3 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'byebug'
2
3
  require 'parametric'
@@ -0,0 +1,324 @@
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 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
+ it "wraps regular schemas in structs" do
129
+ friend_schema = Parametric::Schema.new do
130
+ field(:name)
131
+ end
132
+
133
+ klass = Class.new do
134
+ include Parametric::Struct
135
+
136
+ schema do
137
+ field(:title).type(:string).present
138
+ field(:friends).type(:array).schema friend_schema
139
+ end
140
+ end
141
+
142
+ instance = klass.new({
143
+ title: 'foo',
144
+ friends: [{name: 'Ismael'}]
145
+ })
146
+
147
+ expect(instance.friends.first.name).to eq 'Ismael'
148
+ end
149
+
150
+ it "#to_h" do
151
+ klass = Class.new do
152
+ include Parametric::Struct
153
+
154
+ schema do
155
+ field(:title).type(:string).present
156
+ field(:friends).type(:array).schema do
157
+ field(:name).type(:string)
158
+ field(:age).type(:integer).default(20)
159
+ end
160
+ end
161
+ end
162
+
163
+ instance = klass.new({
164
+ title: 'foo',
165
+ friends: [
166
+ {name: 'Jane'},
167
+ {name: 'Joe', age: '39'},
168
+ ]
169
+ })
170
+
171
+ expect(instance.to_h).to eq({
172
+ title: 'foo',
173
+ friends: [
174
+ {name: 'Jane', age: 20},
175
+ {name: 'Joe', age: 39},
176
+ ]
177
+ })
178
+
179
+ new_instance = klass.new(instance.to_h)
180
+ expect(new_instance.title).to eq 'foo'
181
+
182
+ # it returns a copy so we can't break things!
183
+ data = new_instance.to_h
184
+ data[:title] = 'nope'
185
+ expect(new_instance.to_h[:title]).to eq 'foo'
186
+ end
187
+
188
+ it "works with inheritance" do
189
+ klass = Class.new do
190
+ include Parametric::Struct
191
+
192
+ schema do
193
+ field(:title).type(:string).present
194
+ field(:friends).type(:array).schema do
195
+ field(:name).type(:string)
196
+ field(:age).type(:integer).default(20)
197
+ end
198
+ end
199
+ end
200
+
201
+ subclass = Class.new(klass) do
202
+ schema do
203
+ field(:email)
204
+ end
205
+ end
206
+
207
+ instance = subclass.new(
208
+ title: 'foo',
209
+ email: 'email@me.com',
210
+ friends: [
211
+ {name: 'Jane', age: 20},
212
+ {name: 'Joe', age: 39},
213
+ ]
214
+ )
215
+
216
+ expect(instance.title).to eq 'foo'
217
+ expect(instance.email).to eq 'email@me.com'
218
+ expect(instance.friends.size).to eq 2
219
+ end
220
+
221
+ it "implements deep struct equality" do
222
+ klass = Class.new do
223
+ include Parametric::Struct
224
+
225
+ schema do
226
+ field(:title).type(:string).present
227
+ field(:friends).type(:array).schema do
228
+ field(:age).type(:integer)
229
+ end
230
+ end
231
+ end
232
+
233
+ s1 = klass.new({
234
+ title: 'foo',
235
+ friends: [
236
+ {age: 10},
237
+ {age: 39},
238
+ ]
239
+ })
240
+
241
+
242
+ s2 = klass.new({
243
+ title: 'foo',
244
+ friends: [
245
+ {age: 10},
246
+ {age: 39},
247
+ ]
248
+ })
249
+
250
+ s3 = klass.new({
251
+ title: 'foo',
252
+ friends: [
253
+ {age: 11},
254
+ {age: 39},
255
+ ]
256
+ })
257
+
258
+ s4 = klass.new({
259
+ title: 'bar',
260
+ friends: [
261
+ {age: 10},
262
+ {age: 39},
263
+ ]
264
+ })
265
+
266
+ expect(s1 == s2).to be true
267
+ expect(s1 == s3).to be false
268
+ expect(s1 == s4).to be false
269
+ end
270
+
271
+ it "#merge returns a new instance" do
272
+ klass = Class.new do
273
+ include Parametric::Struct
274
+
275
+ schema do
276
+ field(:title).type(:string).present
277
+ field(:desc)
278
+ field(:friends).type(:array).schema do
279
+ field(:name).type(:string)
280
+ end
281
+ end
282
+ end
283
+
284
+ original = klass.new(
285
+ title: 'foo',
286
+ desc: 'no change',
287
+ friends: [{name: 'joe'}]
288
+ )
289
+
290
+ copy = original.merge(
291
+ title: 'bar',
292
+ friends: [{name: 'jane'}]
293
+ )
294
+
295
+ expect(original.title).to eq 'foo'
296
+ expect(original.desc).to eq 'no change'
297
+ expect(original.friends.first.name).to eq 'joe'
298
+
299
+ expect(copy.title).to eq 'bar'
300
+ expect(copy.desc).to eq 'no change'
301
+ expect(copy.friends.first.name).to eq 'jane'
302
+ end
303
+
304
+ describe '.new!' do
305
+ it 'raises a useful exception if invalid data' do
306
+ klass = Class.new do
307
+ include Parametric::Struct
308
+
309
+ schema do
310
+ field(:title).type(:string).present
311
+ end
312
+ end
313
+
314
+ begin
315
+ klass.new!(title: '')
316
+ rescue Parametric::InvalidStructError => e
317
+ expect(e.errors['$.title']).not_to be nil
318
+ end
319
+
320
+ valid = klass.new!(title: 'foo')
321
+ expect(valid.title).to eq 'foo'
322
+ end
323
+ end
324
+ end