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