paradocs 1.0.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,324 @@
1
+ require 'spec_helper'
2
+ require 'paradocs/struct'
3
+
4
+ describe Paradocs::Struct do
5
+ it "works" do
6
+ friend_class = Class.new do
7
+ include Paradocs::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 Paradocs::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 Paradocs::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 Paradocs::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 Paradocs::Struct
105
+
106
+ def self.paradocs_build_class_for_child(key, child_schema)
107
+ Class.new do
108
+ include Paradocs::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 = Paradocs::Schema.new do
130
+ field(:name)
131
+ end
132
+
133
+ klass = Class.new do
134
+ include Paradocs::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 Paradocs::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 Paradocs::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 Paradocs::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 Paradocs::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 Paradocs::Struct
308
+
309
+ schema do
310
+ field(:title).type(:string).present
311
+ end
312
+ end
313
+
314
+ begin
315
+ klass.new!(title: '')
316
+ rescue Paradocs::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
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+ require "paradocs/dsl"
3
+
4
+ describe "schemes with subschemes" do
5
+ let(:validation_class) do
6
+ Class.new do
7
+ include Paradocs::DSL
8
+
9
+ schema(:request) do
10
+ field(:action).present.options([:update, :delete]).mutates_schema! do |action, *|
11
+ action == :update ? :update_schema : :generic_schema
12
+ end
13
+
14
+ subschema(:update_schema) do
15
+ field(:event).present
16
+ end
17
+
18
+ subschema(:generic_schema) do
19
+ field(:generic_field).present
20
+ end
21
+ end
22
+
23
+ def self.validate(schema_name, data)
24
+ schema(schema_name).resolve(data)
25
+ end
26
+ end
27
+ end
28
+
29
+ let(:update_request) {
30
+ {
31
+ action: :update,
32
+ event: "test"
33
+ }
34
+ }
35
+
36
+ it "invokes necessary subschema based on condition" do
37
+ valid_result = validation_class.validate(:request, update_request)
38
+ expect(valid_result.output).to eq(update_request)
39
+ expect(valid_result.errors).to eq({})
40
+
41
+ failed_result = validation_class.validate(:request, {action: :update, generic_field: "test"})
42
+
43
+ expect(failed_result.errors).to eq({"$.event"=>["is required"]})
44
+ expect(failed_result.output).to eq({action: :update, event: nil})
45
+ end
46
+
47
+ describe "ghost fields" do
48
+ let(:schema) do
49
+ Paradocs::Schema.new do
50
+ mutation_by!(:error) do |value, key, *args|
51
+ value.nil? ? :success : :fail
52
+ end
53
+
54
+ subschema(:fail) do
55
+ field(:fail_field).present
56
+ end
57
+ subschema(:success) do
58
+ field(:success_field).present
59
+ end
60
+ end
61
+ end
62
+
63
+ it "mutates schema as expected and doesn't reflect on current schema structure" do
64
+ structure = {
65
+ _errors: [],
66
+ _subschemes: {
67
+ fail: {_errors: [], _subschemes: {}, fail_field: {required: true, present: true, json_path: "$.fail_field", nested_name: "fail_field"}},
68
+ success: {_errors: [], _subschemes: {}, success_field: {required: true, present: true, json_path: "$.success_field", nested_name: "success_field"}}
69
+ }
70
+ }
71
+ result = schema.resolve({error: :here})
72
+ expect(result.errors).to eq({"$.fail_field"=>["is required"]})
73
+ expect(result.output).to eq({error: :here, fail_field: nil})
74
+ expect(schema.structure).to eq(structure)
75
+ expect(schema.structure(ignore_transparent: false)).to eq(structure.merge(
76
+ error: {transparent: true, mutates_schema: true, json_path: "$.error", nested_name: "error"}
77
+ ))
78
+
79
+ result = schema.resolve({})
80
+ expect(result.errors).to eq({"$.success_field"=>["is required"]})
81
+ expect(result.output).to eq({success_field: nil})
82
+ expect(schema.structure).to eq(structure)
83
+ expect(schema.structure(ignore_transparent: false)).to eq(structure.merge(
84
+ error: {transparent: true, mutates_schema: true, json_path: "$.error", nested_name: "error"}
85
+ ))
86
+ end
87
+ end
88
+
89
+ describe "nested subschemes" do
90
+ let(:schema) do
91
+ Paradocs::Schema.new do
92
+ field(:action).present.options([:update, :delete]).mutates_schema! do |value, key, payload|
93
+ value == :update ? :update_schema : :generic_schema
94
+ end
95
+ field(:event).declared.type(:string)
96
+
97
+ subschema(:generic_schema) do
98
+ field(:generic_field).present
99
+ end
100
+
101
+ subschema(:update_schema) do
102
+ field(:event).present.mutates_schema! do |value, key, payload|
103
+ value == :go_deeper ? :very_deep_schema : :deep_update_schema
104
+ end
105
+ field(:update_field).present
106
+ subschema(:deep_update_schema) do
107
+ field(:field_from_deep_schema).required
108
+ end
109
+
110
+ subschema(:very_deep_schema) do
111
+ field(:a_hash).type(:object).present.schema do
112
+ field(:key).present.mutates_schema! { :draft_subschema }
113
+ subschema(:draft_subschema) do
114
+ field(:another_event).present.type(:boolean)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ context "update_schema -> deep_update_schema" do
123
+ let(:payload) {
124
+ {
125
+ action: :update,
126
+ event: :must_be_present,
127
+ update_field: 1,
128
+ field_from_deep_schema: nil
129
+ }
130
+ }
131
+
132
+ it "builds schema as expected" do
133
+ result = schema.resolve(payload)
134
+ expect(result.output).to eq(payload)
135
+ expect(result.errors).to eq({})
136
+ end
137
+
138
+ it "fails when validation fails in subschemas" do
139
+ result = schema.resolve(payload.merge(action: :delete))
140
+ expect(result.output).to eq(action: :delete, event: "must_be_present", generic_field: nil)
141
+ expect(result.errors).to eq("$.generic_field"=>["is required"])
142
+ end
143
+
144
+ it "overwrites fields: subschema field overwrites parent field" do
145
+ payload.delete(:event)
146
+ result = schema.resolve(payload)
147
+ end
148
+ end
149
+
150
+ context "update_schema -> very_deep_schema -> draft_subschema" do
151
+ let(:payload) {
152
+ {
153
+ action: :update,
154
+ event: :go_deeper,
155
+ update_field: 1,
156
+ a_hash: {
157
+ key: :value,
158
+ another_event: true
159
+ }
160
+ }
161
+ }
162
+
163
+ it "builds schema as expected" do
164
+ result = schema.resolve(payload)
165
+ expect(result.output).to eq(payload)
166
+ expect(result.errors).to eq({})
167
+ end
168
+
169
+ it "fails when payload doesn't suit just built schema" do
170
+ payload[:a_hash] = {key: :random}
171
+ result = schema.resolve(payload)
172
+ payload[:a_hash][:another_event] = nil
173
+ expect(result.output).to eq(payload)
174
+ expect(result.errors).to eq({"$.a_hash.another_event"=>["is required"]})
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'default validators' do
4
+
5
+ def test_validator(payload, key, name, eligible, valid, *args)
6
+ validator = Paradocs.registry.policies[name]
7
+ validator = validator.new(*args) if validator.respond_to?(:new)
8
+ expect(validator.eligible?(payload[key], key, payload)).to eq eligible
9
+ expect(validator.valid?(payload[key], key, payload)).to eq valid
10
+ end
11
+
12
+ describe ':format' do
13
+ it {
14
+ test_validator({key: 'Foobar'}, :key, :format, true, true, /^Foo/)
15
+ test_validator({key: 'Foobar'}, :key, :format, true, false, /^Bar/)
16
+ test_validator({foo: 'Foobar'}, :key, :format, false, true, /^Foo/)
17
+ }
18
+ end
19
+
20
+ describe ':email' do
21
+ it {
22
+ test_validator({key: 'foo@bar.com'}, :key, :email, true, true)
23
+ test_validator({key: 'foo@'}, :key, :email, true, false)
24
+ test_validator({foo: 'foo@bar.com'}, :key, :email, false, true)
25
+ }
26
+ end
27
+
28
+ describe ':required' do
29
+ it {
30
+ test_validator({key: 'foo'}, :key, :required, true, true)
31
+ test_validator({key: ''}, :key, :required, true, true)
32
+ test_validator({key: nil}, :key, :required, true, true)
33
+ test_validator({foo: 'foo'}, :key, :required, true, false)
34
+ }
35
+ end
36
+
37
+ describe ':present' do
38
+ it {
39
+ test_validator({key: 'foo'}, :key, :present, true, true)
40
+ test_validator({key: ''}, :key, :present, true, false)
41
+ test_validator({key: nil}, :key, :present, true, false)
42
+ test_validator({foo: 'foo'}, :key, :present, true, false)
43
+ }
44
+ end
45
+
46
+ describe ':declared' do
47
+ it {
48
+ test_validator({key: 'foo'}, :key, :declared, true, true)
49
+ test_validator({key: ''}, :key, :declared, true, true)
50
+ test_validator({key: nil}, :key, :declared, true, true)
51
+ test_validator({foo: 'foo'}, :key, :declared, false, true)
52
+ }
53
+ end
54
+
55
+ describe ':options' do
56
+ it {
57
+ test_validator({key: 'b'}, :key, :options, true, true, %w(a b c))
58
+ test_validator({key: 'd'}, :key, :options, true, false, %w(a b c))
59
+ test_validator({key: ['c', 'b']}, :key, :options, true, true, %w(a b c))
60
+ test_validator({key: ['c', 'b', 'd']}, :key, :options, true, false, %w(a b c))
61
+ test_validator({foo: 'b'}, :key, :options, false, true, %w(a b c))
62
+ }
63
+ end
64
+
65
+ describe ':array' do
66
+ it {
67
+ test_validator({key: ['a', 'b']}, :key, :array, true, true)
68
+ test_validator({key: []}, :key, :array, true, true)
69
+ test_validator({key: nil}, :key, :array, true, false)
70
+ test_validator({key: 'hello'}, :key, :array, true, false)
71
+ test_validator({key: 123}, :key, :array, true, false)
72
+ test_validator({foo: []}, :key, :array, true, true)
73
+ }
74
+ end
75
+
76
+ describe ':object' do
77
+ it {
78
+ test_validator({key: {'a' =>'b'}}, :key, :object, true, true)
79
+ test_validator({key: {}}, :key, :object, true, true)
80
+ test_validator({key: ['a', 'b']}, :key, :object, true, false)
81
+ test_validator({key: nil}, :key, :object, true, false)
82
+ test_validator({key: 123}, :key, :object, true, false)
83
+ test_validator({foo: {}}, :key, :object, true, true)
84
+ }
85
+ end
86
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+ require "paradocs/whitelist"
3
+ require "paradocs/dsl"
4
+
5
+ describe "classes including Whitelist module" do
6
+ class TestWhitelist
7
+ include Paradocs::DSL
8
+ include Paradocs::Whitelist
9
+
10
+ schema(:request) do
11
+ field(:data).present.type(:array).schema do
12
+ field(:id).present.type(:string).whitelisted
13
+ field(:name).present.type(:string)
14
+ field(:empty_array).type(:array).schema do
15
+ field(:id).whitelisted
16
+ end
17
+ field(:subschema_1).whitelisted.mutates_schema! { |name, *| name.to_sym }
18
+ field(:empty_hash).type(:array).schema do
19
+ field(:id).whitelisted
20
+ end
21
+ field(:extra).schema do
22
+ field(:id).present.type(:string).whitelisted
23
+ field(:name).present.type(:string)
24
+ field(:empty_string).present.type(:string)
25
+ end
26
+
27
+ subschema(:subfield_1) do
28
+ field(:subfield_1).present.type(:boolean).whitelisted
29
+ field(:subschema_2).mutates_schema! { |name, *| name.to_sym }
30
+
31
+ subschema(:subfield_2) do
32
+ field(:subfield_2).present.type(:boolean).whitelisted
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ describe ".filter!" do
40
+ let(:schema) { TestWhitelist.schema(:request) }
41
+ let(:input) {
42
+ {
43
+ "unexpected" => "test",
44
+ from_config: "whitelisted",
45
+ data: [
46
+ "id" => 5,
47
+ name: nil,
48
+ unexpected: nil,
49
+ empty_array: [],
50
+ subschema_1: "subfield_1",
51
+ subfield_1: true,
52
+ subschema_2: "subfield_2",
53
+ subfield_2: true,
54
+ empty_hash: {},
55
+ "extra" => {
56
+ id: 6,
57
+ name: "name",
58
+ unexpected: "unexpected",
59
+ empty_string: ""
60
+ }
61
+ ]
62
+ }
63
+ }
64
+
65
+ before { Paradocs.config.whitelisted_keys = [:from_config]}
66
+
67
+ it "should filter not whitelisted attributes with different key's type" do
68
+ whitelisted = TestWhitelist.new.filter!(input, schema)
69
+
70
+ expect(whitelisted).to eq(
71
+ {
72
+ unexpected: "[FILTERED]",
73
+ from_config: "whitelisted",
74
+ data: [
75
+ {
76
+ id: 5,
77
+ name: "[EMPTY]",
78
+ unexpected: "[EMPTY]",
79
+ empty_array: [],
80
+ subschema_1: "subfield_1",
81
+ subfield_1: true,
82
+ subschema_2: "[FILTERED]",
83
+ subfield_2: true,
84
+ empty_hash: {},
85
+ extra: {
86
+ id: 6,
87
+ name: "[FILTERED]",
88
+ unexpected: "[FILTERED]",
89
+ empty_string: "[EMPTY]"
90
+ }
91
+ }
92
+ ]
93
+ }
94
+ )
95
+ end
96
+ end
97
+ end