paradocs 1.0.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,299 @@
1
+ require 'spec_helper'
2
+
3
+ describe Paradocs::Schema do
4
+ before do
5
+ Paradocs.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 "order input by schema fields' keys order" do
60
+ payload = {
61
+ tags: 'tag',
62
+ status: 'visible',
63
+ extra_field: "extra",
64
+ price: "100",
65
+ title: "title",
66
+ variants: [
67
+ {
68
+ stock: '10',
69
+ available_if_no_stock: true,
70
+ extra_field: "extra",
71
+ name: 'v1',
72
+ sku: 'ABC'
73
+ }
74
+ ]
75
+ }
76
+
77
+ output = subject.resolve(payload).output
78
+ expect(output).to eq({
79
+ title: "title",
80
+ price: 100,
81
+ status: "visible",
82
+ tags: ["tag"],
83
+ variants: [
84
+ {
85
+ name: "v1",
86
+ sku: "ABC",
87
+ stock: 10,
88
+ available_if_no_stock: true
89
+ }
90
+ ]
91
+ })
92
+ end
93
+
94
+ it 'works' do
95
+ test_schema(subject, {
96
+ title: 'iPhone 6 Plus',
97
+ price: '100.0',
98
+ status: 'visible',
99
+ tags: 'tag1, tag2',
100
+ description: 'A description',
101
+ variants: [{name: 'v1', sku: 'ABC', stock: '10', available_if_no_stock: true}]
102
+ },
103
+ {
104
+ title: 'iPhone 6 Plus',
105
+ price: 100,
106
+ status: 'visible',
107
+ tags: ['tag1', 'tag2'],
108
+ description: 'A description',
109
+ variants: [{name: 'v1', sku: 'ABC', stock: 10, available_if_no_stock: true}]
110
+ })
111
+
112
+ test_schema(subject, {
113
+ title: 'iPhone 6 Plus',
114
+ variants: [{name: 'v1', available_if_no_stock: '1'}]
115
+ },
116
+ {
117
+ title: 'iPhone 6 Plus',
118
+ variants: [{name: 'v1', stock: 1, available_if_no_stock: true}]
119
+ })
120
+
121
+ resolve(subject, {}) do |results|
122
+ expect(results.valid?).to be false
123
+ expect(results.errors['$.title']).not_to be_nil
124
+ expect(results.errors['$.variants']).to be_nil
125
+ expect(results.errors['$.status']).to be_nil
126
+ end
127
+
128
+ resolve(subject, {title: 'Foobar', variants: [{name: 'v1'}, {sku: '345'}]}) do |results|
129
+ expect(results.valid?).to be false
130
+ expect(results.errors['$.variants[1].name']).not_to be_nil
131
+ end
132
+ end
133
+
134
+ it "ignores nil fields if using :declared policy" do
135
+ schema = described_class.new do
136
+ field(:id).type(:integer)
137
+ field(:title).declared.type(:string)
138
+ end
139
+
140
+ resolve(schema, {id: 123}) do |results|
141
+ expect(results.output.keys).to eq [:id]
142
+ end
143
+ end
144
+
145
+ describe "#policy" do
146
+ it "applies policy to all fields" do
147
+ subject.policy(:declared)
148
+
149
+ resolve(subject, {}) do |results|
150
+ expect(results.valid?).to be true
151
+ expect(results.errors.keys).to be_empty
152
+ end
153
+ end
154
+
155
+ it "replaces previous policies" do
156
+ subject.policy(:declared)
157
+ subject.policy(:present)
158
+
159
+ resolve(subject, {title: "hello"}) do |results|
160
+ expect(results.valid?).to be false
161
+ expect(results.errors.keys).to match_array(%w(
162
+ $.price
163
+ $.status
164
+ $.tags
165
+ $.description
166
+ $.variants
167
+ ))
168
+ end
169
+ end
170
+
171
+ it "applies :noop policy to all fields" do
172
+ subject.policy(:noop)
173
+
174
+ resolve(subject, {}) do |results|
175
+ expect(results.valid?).to be false
176
+ expect(results.errors['$.title']).not_to be_nil
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "#merge" do
182
+ context "no options" do
183
+ let!(:schema1) {
184
+ described_class.new do
185
+ field(:title).policy(:string).present
186
+ field(:price).policy(:integer)
187
+ end
188
+ }
189
+
190
+ let!(:schema2) {
191
+ described_class.new do
192
+ field(:price).policy(:string)
193
+ field(:description).policy(:string)
194
+ end
195
+ }
196
+
197
+ it "returns a new schema adding new fields and updating existing ones" do
198
+ new_schema = schema1.merge(schema2)
199
+ expect(new_schema.fields.keys).to match_array([:title, :price, :description])
200
+
201
+ # did not mutate original
202
+ expect(schema1.fields[:price].meta_data[:type]).to eq :integer
203
+
204
+ expect(new_schema.fields[:title].meta_data[:type]).to eq :string
205
+ expect(new_schema.fields[:price].meta_data[:type]).to eq :string
206
+ end
207
+ end
208
+
209
+ context "with options" do
210
+ let!(:schema1) {
211
+ described_class.new(price_type: :integer, label: "Foo") do |opts|
212
+ field(:title).policy(:string).present
213
+ field(:price).policy(opts[:price_type]).meta(label: opts[:label])
214
+ end
215
+ }
216
+
217
+ let!(:schema2) {
218
+ described_class.new(price_type: :string) do
219
+ field(:description).policy(:string)
220
+ end
221
+ }
222
+
223
+ it "inherits options" do
224
+ new_schema = schema1.merge(schema2)
225
+ expect(new_schema.fields[:price].meta_data[:type]).to eq :string
226
+ expect(new_schema.fields[:price].meta_data[:label]).to eq "Foo"
227
+ end
228
+
229
+ it "re-applies blocks with new options" do
230
+ new_schema = schema1.merge(schema2)
231
+ expect(new_schema.fields.keys).to match_array([:title, :price, :description])
232
+
233
+ # did not mutate original
234
+ expect(schema1.fields[:price].meta_data[:type]).to eq :integer
235
+
236
+ expect(new_schema.fields[:title].meta_data[:type]).to eq :string
237
+ expect(new_schema.fields[:price].meta_data[:type]).to eq :string
238
+ end
239
+ end
240
+ end
241
+
242
+ describe "#clone" do
243
+ let!(:schema1) {
244
+ described_class.new do |opts|
245
+ field(:id).present
246
+ field(:title).policy(:string).present
247
+ field(:price)
248
+ end
249
+ }
250
+
251
+ it "returns a copy that can be further manipulated" do
252
+ schema2 = schema1.clone.policy(:declared).ignore(:id)
253
+ expect(schema1.fields.keys).to match_array([:id, :title, :price])
254
+ expect(schema2.fields.keys).to match_array([:title, :price])
255
+
256
+ results1 = schema1.resolve(id: "abc", price: 100)
257
+ expect(results1.errors.keys).to eq ["$.title"]
258
+
259
+ results2 = schema2.resolve(id: "abc", price: 100)
260
+ expect(results2.errors.keys).to eq []
261
+ end
262
+ end
263
+
264
+ describe "#ignore" do
265
+ it "ignores fields" do
266
+ s1 = described_class.new.ignore(:title, :status) do
267
+ field(:status)
268
+ field(:title).policy(:string).present
269
+ field(:price).policy(:integer)
270
+ end
271
+
272
+ output = s1.resolve(status: "draft", title: "foo", price: "100").output
273
+ expect(output).to eq({price: 100})
274
+ end
275
+
276
+ it "ignores when merging" do
277
+ s1 = described_class.new do
278
+ field(:status)
279
+ field(:title).policy(:string).present
280
+ end
281
+
282
+ s1 = described_class.new.ignore(:title, :status) do
283
+ field(:price).policy(:integer)
284
+ end
285
+
286
+ output = s1.resolve(title: "foo", status: "draft", price: "100").output
287
+ expect(output).to eq({price: 100})
288
+ end
289
+
290
+ it "returns self so it can be chained" do
291
+ s1 = described_class.new do
292
+ field(:status)
293
+ field(:title).policy(:string).present
294
+ end
295
+
296
+ expect(s1.ignore(:status)).to eq s1
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Schema structures generation" do
4
+ Paradocs.policy :policy_with_error do
5
+ register_error ArgumentError
6
+
7
+ validate do |*|
8
+ raise ArgumentError
9
+ end
10
+ end
11
+
12
+ Paradocs.policy :policy_with_silent_error do
13
+ register_silent_error RuntimeError
14
+ end
15
+
16
+ let(:schema) do
17
+ Paradocs::Schema.new do
18
+ subschema(:highest_level) { field(:test).present } # no mutations on this level -> subschema ignored
19
+
20
+ field(:data).type(:object).present.schema do
21
+ field(:id).type(:integer).present.policy(:policy_with_error)
22
+ field(:name).type(:string).meta(label: "very important staff")
23
+ field(:role).type(:string).declared.options(["admin", "user"]).default("user").mutates_schema! do |*|
24
+ :test_subschema
25
+ end
26
+ field(:extra).type(:array).required.schema do
27
+ field(:extra).declared.default(false).policy(:policy_with_silent_error)
28
+ end
29
+
30
+ mutation_by!(:name) { :subschema }
31
+
32
+ subschema(:subschema) do
33
+ field(:test_field).present
34
+ end
35
+ subschema(:test_subschema) do
36
+ field(:test1).present
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+
43
+ it "generates nested data for documentation generation" do
44
+ result = schema.structure { |k, meta| meta[:block_works] = true unless meta[:present] }
45
+ expect(result[:_subschemes]).to eq({})
46
+ expect(result[:_errors]).to eq([ArgumentError])
47
+ data_structure = result[:data].delete(:structure)
48
+ expect(result[:data]).to eq({
49
+ type: :object,
50
+ required: true,
51
+ present: true,
52
+ json_path: "$.data",
53
+ nested_name: "data",
54
+ })
55
+ expect(data_structure[:_subschemes]).to eq({
56
+ test_subschema: {
57
+ _errors: [],
58
+ _subschemes: {},
59
+ test1: {required: true, present: true, json_path: "$.data.test1", nested_name: "data.test1"}
60
+ },
61
+ subschema: {
62
+ _errors: [],
63
+ _subschemes: {},
64
+ test_field: {required: true, present: true, json_path: "$.data.test_field", nested_name: "data.test_field"}
65
+ }
66
+ })
67
+ expect(data_structure[:id]).to eq({
68
+ type: :integer,
69
+ required: true,
70
+ present: true,
71
+ policy_with_error: {errors: [ArgumentError]},
72
+ json_path: "$.data.id",
73
+ nested_name: "data.id"
74
+ })
75
+ expect(data_structure[:name]).to eq({
76
+ type: :string,
77
+ label: "very important staff",
78
+ mutates_schema: true,
79
+ block_works: true,
80
+ json_path: "$.data.name",
81
+ nested_name: "data.name"
82
+ })
83
+ expect(data_structure[:role]).to eq({
84
+ type: :string,
85
+ options: ["admin", "user"],
86
+ default: "user",
87
+ mutates_schema: true,
88
+ block_works: true,
89
+ json_path: "$.data.role",
90
+ nested_name: "data.role"
91
+ })
92
+ expect(data_structure[:extra]).to eq({
93
+ type: :array,
94
+ required: true,
95
+ block_works: true,
96
+ json_path: "$.data.extra[]",
97
+ nested_name: "data.extra",
98
+ structure: {
99
+ extra: {
100
+ default: false,
101
+ block_works: true,
102
+ json_path: "$.data.extra[].extra",
103
+ nested_name: "data.extra.extra",
104
+ policy_with_silent_error: {errors: []}
105
+ },
106
+ _subschemes: {}
107
+ }
108
+ })
109
+ end
110
+
111
+ it "generates flatten data for documentation generation" do
112
+ expect(schema.flatten_structure { |key, meta| meta[:block_works] = true if key.split(".").size == 1 }).to eq({
113
+ "data" => {
114
+ type: :object,
115
+ required: true,
116
+ present: true,
117
+ block_works: true,
118
+ json_path: "$.data"
119
+ },
120
+ "data.extra" => {
121
+ type: :array,
122
+ required: true,
123
+ json_path: "$.data.extra[]"
124
+ },
125
+ "data.extra.extra" => {
126
+ default: false,
127
+ json_path: "$.data.extra[].extra",
128
+ policy_with_silent_error: {errors: []}
129
+ },
130
+ "data.id" => {
131
+ type: :integer,
132
+ required: true,
133
+ present: true,
134
+ json_path: "$.data.id",
135
+ policy_with_error: {errors: [ArgumentError]}
136
+ },
137
+ "data.name" => {
138
+ type: :string,
139
+ json_path: "$.data.name",
140
+ label: "very important staff",
141
+ mutates_schema: true
142
+ },
143
+ "data.role" => {
144
+ type: :string,
145
+ options: ["admin", "user"],
146
+ default: "user",
147
+ json_path: "$.data.role",
148
+ mutates_schema: true
149
+ },
150
+ _errors: [ArgumentError],
151
+ _subschemes: {
152
+ test_subschema: {
153
+ _errors: [],
154
+ _subschemes: {},
155
+ "data.test1"=>{
156
+ required: true,
157
+ present: true,
158
+ json_path: "$.data.test1"
159
+ }
160
+ },
161
+ subschema: {
162
+ _errors: [],
163
+ _subschemes: {},
164
+ "data.test_field" => {required: true, present: true, json_path: "$.data.test_field"}
165
+ }
166
+ }
167
+ })
168
+ end
169
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Schema#walk' do
4
+ let(:schema) do
5
+ Paradocs::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
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'pry'
3
+ require 'paradocs'
4
+ require_relative 'helpers'