parametric 0.0.1 → 0.2.12

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,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