parametric 0.0.1 → 0.2.10

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.
@@ -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