paradocs 1.0.22

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