paradocs 1.0.24 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.readthedocs.yml +5 -0
- data/docs/custom_configuration.md +10 -0
- data/docs/documentation_generation.md +291 -0
- data/docs/faq.md +21 -0
- data/docs/form_objects_dsl.md +90 -0
- data/docs/index.md +106 -0
- data/docs/payload_builder.md +104 -0
- data/docs/policies.md +309 -0
- data/docs/schema.md +294 -0
- data/docs/struct.md +135 -0
- data/docs/subschema.md +29 -0
- data/lib/paradocs/extensions/payload_builder.rb +43 -0
- data/lib/paradocs/extensions/structure.rb +118 -0
- data/lib/paradocs/schema.rb +30 -9
- data/lib/paradocs/version.rb +1 -1
- data/mkdocs.yml +16 -0
- data/paradocs.gemspec +2 -2
- data/requirements.txt +1 -0
- data/spec/extensions/payload_builder_spec.rb +70 -0
- data/spec/extensions/structures_spec.rb +237 -0
- data/spec/schema_spec.rb +1 -1
- data/spec/subschema_spec.rb +7 -4
- metadata +26 -10
- data/lib/paradocs/extensions/insides.rb +0 -77
- data/spec/schema_structures_spec.rb +0 -169
@@ -0,0 +1,104 @@
|
|
1
|
+
# Generate examples from the Schema
|
2
|
+
|
3
|
+
> Schema instance provides #example_payloads method that returns example of all possible structures.
|
4
|
+
|
5
|
+
NOTE: PayloadBuilder sets nil values by default. If options are given - builder will take on of them, if default is set - builder will use it.
|
6
|
+
> PayloadBuilder#build! method takes a block as argument that may help you adding your custom rules.
|
7
|
+
|
8
|
+
#### Example schema
|
9
|
+
```ruby
|
10
|
+
schema = Paradocs::Schema.new do
|
11
|
+
field(:data).type(:object).present.schema do
|
12
|
+
field(:id).type(:integer).present.policy(:policy_with_error)
|
13
|
+
field(:name).type(:string).meta(label: "very important staff")
|
14
|
+
field(:role).type(:string).declared.options(["admin", "user"]).default("user").mutates_schema! do |*|
|
15
|
+
:test_subschema
|
16
|
+
end
|
17
|
+
field(:extra).type(:array).required.schema do
|
18
|
+
field(:extra).declared.default(false).policy(:policy_with_silent_error)
|
19
|
+
end
|
20
|
+
|
21
|
+
mutation_by!(:name) { :subschema }
|
22
|
+
|
23
|
+
subschema(:subschema) do
|
24
|
+
field(:test_field).present
|
25
|
+
end
|
26
|
+
subschema(:test_subschema) do
|
27
|
+
field(:test1).present
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## Generate payloads
|
34
|
+
|
35
|
+
```rb
|
36
|
+
Paradocs::Extensions::PayloadBuilder.new(schema).build! # =>
|
37
|
+
# or
|
38
|
+
schema.example_payloads.to_json # =>
|
39
|
+
{
|
40
|
+
"subschema": {
|
41
|
+
"data": {
|
42
|
+
"name": null,
|
43
|
+
"role": "user",
|
44
|
+
"extra": [{"extra": null}],
|
45
|
+
"test_field": null,
|
46
|
+
"id": null
|
47
|
+
}
|
48
|
+
},
|
49
|
+
"test_subschema": {
|
50
|
+
"data": {
|
51
|
+
"name": null,
|
52
|
+
"role": "user",
|
53
|
+
"extra": [{"extra": null}],
|
54
|
+
"test1": null,
|
55
|
+
"id": null
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
```
|
60
|
+
|
61
|
+
## Customize payload generation logic
|
62
|
+
PayloadBuilder#build! allows passing a block that will receive the following arguments:
|
63
|
+
|
64
|
+
- key: Field name
|
65
|
+
- meta: Field meta data (that includes (if provided) field types, presence data, policies and other meta data
|
66
|
+
- example_value: Provided by generator example value.
|
67
|
+
- skip_word: Return this argument back if you want this item to be ommitted.
|
68
|
+
|
69
|
+
```rb
|
70
|
+
block = Proc.new do |key, meta, example, skip_word|
|
71
|
+
if key.to_s == "name"
|
72
|
+
"John Smith"
|
73
|
+
elsif meta[:type] == :integer
|
74
|
+
13
|
75
|
+
elsif key.to_s.match? /test/
|
76
|
+
skip_word
|
77
|
+
else
|
78
|
+
example
|
79
|
+
end
|
80
|
+
end
|
81
|
+
schema.example_payloads(&block) # =>
|
82
|
+
# or
|
83
|
+
Paradocs::Extensions::PayloadBuilder.new(schema).build!(&block) # =>
|
84
|
+
{
|
85
|
+
"subschema": {
|
86
|
+
"data": {
|
87
|
+
"name": "John Smith", # value is changed
|
88
|
+
"role": "user", # random choice from field(:user).options
|
89
|
+
"extra": [{"extra": null}], # null are defaults
|
90
|
+
"id": 13 # value is changed
|
91
|
+
# NOTE: fields matching with /test/ are ommitted: test_field, test1
|
92
|
+
}
|
93
|
+
},
|
94
|
+
"test_subschema": {
|
95
|
+
"data": {
|
96
|
+
"name": "John Smith",
|
97
|
+
"role": "user",
|
98
|
+
"extra": [{"extra": null}],
|
99
|
+
"id": 13
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
```
|
data/docs/policies.md
ADDED
@@ -0,0 +1,309 @@
|
|
1
|
+
# Built-In Policies
|
2
|
+
Paradocs ships with a number of built-in policies.
|
3
|
+
|
4
|
+
## Type coercions
|
5
|
+
Type coercions (the `type` method) and validations (the `validate` method) are all _policies_.
|
6
|
+
|
7
|
+
### :string
|
8
|
+
|
9
|
+
Calls `:to_s` on the value
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
field(:title).type(:string)
|
13
|
+
```
|
14
|
+
|
15
|
+
### :integer
|
16
|
+
|
17
|
+
Calls `:to_i` on the value
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
field(:age).type(:integer)
|
21
|
+
```
|
22
|
+
|
23
|
+
### :number
|
24
|
+
|
25
|
+
Calls `:to_f` on the value
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
field(:price).type(:number)
|
29
|
+
```
|
30
|
+
|
31
|
+
### :boolean
|
32
|
+
|
33
|
+
Returns `true` or `false` (`nil` is converted to `false`).
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
field(:published).type(:boolean)
|
37
|
+
```
|
38
|
+
|
39
|
+
### :datetime
|
40
|
+
|
41
|
+
Attempts parsing value with [Datetime.parse](http://ruby-doc.org/stdlib-2.3.1/libdoc/date/rdoc/DateTime.html#method-c-parse). If invalid, the error will be added to the output's `errors` object.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
field(:expires_on).type(:datetime)
|
45
|
+
```
|
46
|
+
|
47
|
+
## Presence policies.
|
48
|
+
|
49
|
+
### :required
|
50
|
+
|
51
|
+
Check that the key exists in the input.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
field(:name).required
|
55
|
+
|
56
|
+
# same as
|
57
|
+
field(:name).policy(:required)
|
58
|
+
```
|
59
|
+
|
60
|
+
Note that `:required` policy does not validate that the value is not empty. Use `:present` for that.
|
61
|
+
|
62
|
+
### :present
|
63
|
+
|
64
|
+
Check that the key exists and the value is not blank.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
field(:name).present
|
68
|
+
|
69
|
+
# same as
|
70
|
+
field(:name).policy(:present)
|
71
|
+
```
|
72
|
+
|
73
|
+
If the value is a `String`, it validates that it's not blank. If an `Array`, it checks that it's not empty. Otherwise it checks that the value is not `nil`.
|
74
|
+
|
75
|
+
### :declared
|
76
|
+
|
77
|
+
Check that a key exists in the input, or stop any further validations otherwise.
|
78
|
+
This is useful when chained to other validations. For example:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
field(:name).declared.present
|
82
|
+
```
|
83
|
+
The example above will check that the value is not empty, but only if the key exists. If the key doesn't exist no validations will run.
|
84
|
+
|
85
|
+
### :default
|
86
|
+
|
87
|
+
- `:default` policy is invoked when there are no field presence policies defined or used either `:required` or `:declared` policies.
|
88
|
+
- `:default` policy is invoked when value is nil or empty
|
89
|
+
- `:default` policy can be a proc. Proc receives the following arguments: `key, the whole payload, validation context`
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
field(:role).declared.default("admin")
|
93
|
+
field(:created_at).declared.default( ->(key, payload, context) { DateTime.now })
|
94
|
+
```
|
95
|
+
|
96
|
+
## Useful built-in policies.
|
97
|
+
|
98
|
+
### :format
|
99
|
+
|
100
|
+
Check value against custom regexp
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
field(:salutation).policy(:format, /^Mr\/s/)
|
104
|
+
# optional custom error message
|
105
|
+
field(:salutation).policy(:format, /^Mr\/s\./, "must start with Mr/s.")
|
106
|
+
```
|
107
|
+
|
108
|
+
### :email
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
field(:business_email).policy(:email)
|
112
|
+
```
|
113
|
+
|
114
|
+
### :gt, :gte, :lt, :lte
|
115
|
+
|
116
|
+
Compare the value with a number.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
field(:age).policy(:gt, 35) # strictly greater than 35
|
120
|
+
field(:age1).policy(:lt, 11.1) # strictly less than 11.1
|
121
|
+
field(:age2).policy(:lte, 21) # less or equal to 21
|
122
|
+
field(:age3).policy(:gte, 11) # greater or equal to 11
|
123
|
+
```
|
124
|
+
|
125
|
+
### :options
|
126
|
+
|
127
|
+
Pass allowed values for a field
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
field(:status).options(["draft", "published"])
|
131
|
+
|
132
|
+
# Same as
|
133
|
+
field(:status).policy(:options, ["draft", "published"])
|
134
|
+
```
|
135
|
+
|
136
|
+
### :length
|
137
|
+
|
138
|
+
Specify value's length constraints. Calls #length under the hood.
|
139
|
+
- `min:` - The attribute cannot have less than the specified length.
|
140
|
+
- `max` - The attribute cannot have more than the specified length.
|
141
|
+
- `eq` - The attribute should be exactly equal to the specified length.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
field(:name).length(min: 5, max: 25)
|
145
|
+
field(:name).length(eq: 10)
|
146
|
+
```
|
147
|
+
|
148
|
+
### :split
|
149
|
+
|
150
|
+
Split comma-separated string values into an array.
|
151
|
+
Useful for parsing comma-separated query-string parameters.
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
field(:status).policy(:split) # turns "pending,confirmed" into ["pending", "confirmed"]
|
155
|
+
```
|
156
|
+
|
157
|
+
### :meta
|
158
|
+
|
159
|
+
The `#meta` field method can be used to add custom meta data to field definitions.
|
160
|
+
These meta data can be used later when instrospecting schemas (ie. to generate documentation or error notices).
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
create_user_schema = Paradocs::Schema.new do
|
164
|
+
field(:name).required.type(:string).meta(label: "User's full name")
|
165
|
+
field(:status).options(["published", "unpublished"]).default("published")
|
166
|
+
field(:age).type(:integer).meta(label: "User's age")
|
167
|
+
field(:friends).type(:array).meta(label: "User friends").schema do
|
168
|
+
field(:name).type(:string).present.meta(label: "Friend full name")
|
169
|
+
field(:email).policy(:email).meta(label: "Friend's email")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
## Custom policies
|
175
|
+
|
176
|
+
You can also register your own custom policy objects. A policy can be not inherited from `Paradocs::BasePolicy`, in this case it must implement the following methods: `#valid?`, `#coerce`, `#message`, `#meta_data`, `#policy_name`
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class MyPolicy < Paradocs::BasePolicy
|
180
|
+
# Validation error message, if invalid
|
181
|
+
def message
|
182
|
+
'is invalid'
|
183
|
+
end
|
184
|
+
|
185
|
+
# Whether or not to validate and coerce this value
|
186
|
+
# if false, no other policies will be run on the field
|
187
|
+
def eligible?(value, key, payload)
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
# Transform the value
|
192
|
+
def coerce(value, key, context)
|
193
|
+
value
|
194
|
+
end
|
195
|
+
|
196
|
+
# Is the value valid?
|
197
|
+
def validate(value, key, payload)
|
198
|
+
true
|
199
|
+
end
|
200
|
+
|
201
|
+
# merge this object into the field's meta data
|
202
|
+
def meta_data
|
203
|
+
{type: :string}
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
|
209
|
+
You can register your policy with:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
Paradocs.policy :my_policy, MyPolicy
|
213
|
+
```
|
214
|
+
And then refer to it by name when declaring your schema fields
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
field(:title).policy(:my_policy)
|
218
|
+
```
|
219
|
+
|
220
|
+
You can chain custom policies with other policies.
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
field(:title).required.policy(:my_policy)
|
224
|
+
```
|
225
|
+
|
226
|
+
Note that you can also register instances.
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
Paradocs.policy :my_policy, MyPolicy.new
|
230
|
+
```
|
231
|
+
|
232
|
+
For example, a policy that can be configured on a field-by-field basis:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
class AddJobTitle
|
236
|
+
def initialize(job_title)
|
237
|
+
@job_title = job_title
|
238
|
+
end
|
239
|
+
|
240
|
+
def message
|
241
|
+
'is invalid'
|
242
|
+
end
|
243
|
+
|
244
|
+
# Noop
|
245
|
+
def eligible?(value, key, payload)
|
246
|
+
true
|
247
|
+
end
|
248
|
+
|
249
|
+
# Add job title to value
|
250
|
+
def coerce(value, key, context)
|
251
|
+
"#{value}, #{@job_title}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# Noop
|
255
|
+
def validate(value, key, payload)
|
256
|
+
true
|
257
|
+
end
|
258
|
+
|
259
|
+
def meta_data
|
260
|
+
{}
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Register it
|
265
|
+
Paradocs.policy :job_title, AddJobTitle
|
266
|
+
```
|
267
|
+
|
268
|
+
Now you can reuse the same policy with different configuration
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
manager_schema = Paradocs::Schema.new do
|
272
|
+
field(:name).type(:string).policy(:job_title, "manager")
|
273
|
+
end
|
274
|
+
|
275
|
+
cto_schema = Paradocs::Schema.new do
|
276
|
+
field(:name).type(:string).policy(:job_title, "CTO")
|
277
|
+
end
|
278
|
+
|
279
|
+
manager_schema.resolve(name: "Joe Bloggs").output # => {name: "Joe Bloggs, manager"}
|
280
|
+
cto_schema.resolve(name: "Joe Bloggs").output # => {name: "Joe Bloggs, CTO"}
|
281
|
+
```
|
282
|
+
|
283
|
+
## Custom policies, short version
|
284
|
+
|
285
|
+
For simple policies that don't need all policy methods, you can:
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
Paradocs.policy :cto_job_title do
|
289
|
+
coerce do |value, key, context|
|
290
|
+
"#{value}, CTO"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# use it
|
295
|
+
cto_schema = Paradocs::Schema.new do
|
296
|
+
field(:name).type(:string).policy(:cto_job_title)
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
Paradocs.policy :over_21_and_under_25 do
|
302
|
+
coerce do |age, key, context|
|
303
|
+
age.to_i
|
304
|
+
end
|
305
|
+
|
306
|
+
validate do |age, key, context|
|
307
|
+
age > 21 && age < 25
|
308
|
+
end
|
309
|
+
end
|
data/docs/schema.md
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
# Schema
|
2
|
+
## Expanding fields dynamically
|
3
|
+
|
4
|
+
Sometimes you don't know the exact field names but you want to allow arbitrary fields depending on a given pattern.
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
# with this payload:
|
8
|
+
# {
|
9
|
+
# title: "A title",
|
10
|
+
# :"custom_attr_Color" => "red",
|
11
|
+
# :"custom_attr_Material" => "leather"
|
12
|
+
# }
|
13
|
+
|
14
|
+
schema = Paradocs::Schema.new do
|
15
|
+
field(:title).type(:string).present
|
16
|
+
# here we allow any field starting with /^custom_attr/
|
17
|
+
# this yields a MatchData object to the block
|
18
|
+
# where you can define a Field and validations on the fly
|
19
|
+
# https://ruby-doc.org/core-2.2.0/MatchData.html
|
20
|
+
expand(/^custom_attr_(.+)/) do |match|
|
21
|
+
field(match[1]).type(:string).present
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
results = schema.resolve({
|
26
|
+
title: "A title",
|
27
|
+
:"custom_attr_Color" => "red",
|
28
|
+
:"custom_attr_Material" => "leather",
|
29
|
+
:"custom_attr_Weight" => "",
|
30
|
+
})
|
31
|
+
|
32
|
+
results.ouput[:Color] # => "red"
|
33
|
+
results.ouput[:Material] # => "leather"
|
34
|
+
results.errors["$.Weight"] # => ["is required and value must be present"]
|
35
|
+
```
|
36
|
+
|
37
|
+
NOTES: dynamically expanded field names are not included in `Schema#structure` metadata, and they are only processes if fields with the given expressions are present in the payload. This means that validations applied to those fields only run if keys are present in the first place.
|
38
|
+
|
39
|
+
|
40
|
+
## Cloning schemas
|
41
|
+
|
42
|
+
The `#clone` method returns a new instance of a schema with all field definitions copied over.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
new_schema = original_schema.clone
|
46
|
+
```
|
47
|
+
|
48
|
+
New copies can be further manipulated without affecting the original.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# See below for #policy and #ignore
|
52
|
+
new_schema = original_schema.clone.policy(:declared).ignore(:id) do |sc|
|
53
|
+
field(:another_field).present
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
## Merging schemas
|
58
|
+
|
59
|
+
The `#merge` method will merge field definitions in two schemas and produce a new schema instance.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
basic_user_schema = Paradocs::Schema.new do
|
63
|
+
field(:name).type(:string).required
|
64
|
+
field(:age).type(:integer)
|
65
|
+
end
|
66
|
+
|
67
|
+
friends_schema = Paradocs::Schema.new do
|
68
|
+
field(:friends).type(:array).schema do
|
69
|
+
field(:name).required
|
70
|
+
field(:email).policy(:email)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
user_with_friends_schema = basic_user_schema.merge(friends_schema)
|
75
|
+
|
76
|
+
results = user_with_friends_schema.resolve(input)
|
77
|
+
```
|
78
|
+
|
79
|
+
Fields defined in the merged schema will override fields with the same name in the original schema.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
required_name_schema = Paradocs::Schema.new do
|
83
|
+
field(:name).required
|
84
|
+
field(:age)
|
85
|
+
end
|
86
|
+
|
87
|
+
optional_name_schema = Paradocs::Schema.new do
|
88
|
+
field(:name)
|
89
|
+
end
|
90
|
+
|
91
|
+
# This schema now has :name and :age fields.
|
92
|
+
# :name has been redefined to not be required.
|
93
|
+
new_schema = required_name_schema.merge(optional_name_schema)
|
94
|
+
```
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
### Reusing nested schemas
|
99
|
+
|
100
|
+
You can optionally use an existing schema instance as a nested schema:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
friends_schema = Paradocs::Schema.new do
|
104
|
+
field(:friends).type(:array).schema do
|
105
|
+
field(:name).type(:string).required
|
106
|
+
field(:email).policy(:email)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
person_schema = Paradocs::Schema.new do
|
111
|
+
field(:name).type(:string).required
|
112
|
+
field(:age).type(:integer)
|
113
|
+
# Nest friends_schema
|
114
|
+
field(:friends).type(:array).schema(friends_schema)
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
### Schema-wide policies
|
119
|
+
|
120
|
+
Sometimes it's useful to apply the same policy to all fields in a schema.
|
121
|
+
|
122
|
+
For example, fields that are _required_ when creating a record might be optional when updating the same record (ie. _PATCH_ operations in APIs).
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class UpdateUserForm < CreateUserForm
|
126
|
+
schema.policy(:declared)
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
This will prefix the `:declared` policy to all fields inherited from the parent class.
|
131
|
+
This means that only fields whose keys are present in the input will be validated.
|
132
|
+
|
133
|
+
Schemas with default policies can still define or re-define fields.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
class UpdateUserForm < CreateUserForm
|
137
|
+
schema.policy(:declared) do
|
138
|
+
# Validation will only run if key exists
|
139
|
+
field(:age).type(:integer).present
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Ignoring fields defined in the parent class
|
145
|
+
|
146
|
+
Sometimes you'll want a child class to inherit most fields from the parent, but ignoring some.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class CreateUserForm
|
150
|
+
include Paradocs::DSL
|
151
|
+
|
152
|
+
schema do
|
153
|
+
field(:uuid).present
|
154
|
+
field(:status).required.options(["inactive", "active"])
|
155
|
+
field(:name)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
The child class can use `ignore(*fields)` to ignore fields defined in the parent.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class UpdateUserForm < CreateUserForm
|
164
|
+
schema.ignore(:uuid, :status) do
|
165
|
+
# optionally add new fields here
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
### Schema options
|
171
|
+
|
172
|
+
Another way of modifying inherited schemas is by passing options.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
class CreateUserForm
|
176
|
+
include Paradocs::DSL
|
177
|
+
|
178
|
+
schema(default_policy: :noop) do |opts|
|
179
|
+
field(:name).policy(opts[:default_policy]).type(:string).required
|
180
|
+
field(:email).policy(opts[:default_policy).policy(:email).required
|
181
|
+
field(:age).type(:integer)
|
182
|
+
end
|
183
|
+
|
184
|
+
# etc
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
The `:noop` policy does nothing. The sub-class can pass its own _default_policy_.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
class UpdateUserForm < CreateUserForm
|
192
|
+
# this will only run validations keys existing in the input
|
193
|
+
schema(default_policy: :declared)
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
## A pattern: changing schema policy on the fly.
|
198
|
+
|
199
|
+
You can use a combination of `#clone` and `#policy` to change schema-wide field policies on the fly.
|
200
|
+
|
201
|
+
For example, you might have a form object that supports creating a new user and defining mandatory fields.
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
class CreateUserForm
|
205
|
+
include Paradocs::DSL
|
206
|
+
|
207
|
+
schema do
|
208
|
+
field(:name).present
|
209
|
+
field(:age).present
|
210
|
+
end
|
211
|
+
|
212
|
+
attr_reader :errors, :params
|
213
|
+
|
214
|
+
def initialize(payload: {})
|
215
|
+
results = self.class.schema.resolve(payload)
|
216
|
+
@errors = results.errors
|
217
|
+
@params = results.output
|
218
|
+
end
|
219
|
+
|
220
|
+
def run!
|
221
|
+
User.create(params)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
Now you might want to use the same form object to _update_ and existing user supporting partial updates.
|
227
|
+
In this case, however, attributes should only be validated if the attributes exist in the payload. We need to apply the `:declared` policy to all schema fields, only if a user exists.
|
228
|
+
|
229
|
+
We can do this by producing a clone of the class-level schema and applying any necessary policies on the fly.
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
class CreateUserForm
|
233
|
+
include Paradocs::DSL
|
234
|
+
|
235
|
+
schema do
|
236
|
+
field(:name).present
|
237
|
+
field(:age).present
|
238
|
+
end
|
239
|
+
|
240
|
+
attr_reader :errors, :params
|
241
|
+
|
242
|
+
def initialize(payload: {}, user: nil)
|
243
|
+
@payload = payload
|
244
|
+
@user = user
|
245
|
+
|
246
|
+
# pick a policy based on user
|
247
|
+
policy = user ? :declared : :noop
|
248
|
+
# clone original schema and apply policy
|
249
|
+
schema = self.class.schema.clone.policy(policy)
|
250
|
+
|
251
|
+
# resolve params
|
252
|
+
results = schema.resolve(params)
|
253
|
+
@errors = results.errors
|
254
|
+
@params = results.output
|
255
|
+
end
|
256
|
+
|
257
|
+
def run!
|
258
|
+
if @user
|
259
|
+
@user.update_attributes(params)
|
260
|
+
else
|
261
|
+
User.create(params)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
## Multiple schema definitions
|
268
|
+
|
269
|
+
Form objects can optionally define more than one schema by giving them names:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
class UpdateUserForm
|
273
|
+
include Paradocs::DSL
|
274
|
+
|
275
|
+
# a schema named :query
|
276
|
+
# for example for query parameters
|
277
|
+
schema(:query) do
|
278
|
+
field(:user_id).type(:integer).present
|
279
|
+
end
|
280
|
+
|
281
|
+
# a schema for PUT body parameters
|
282
|
+
schema(:payload) do
|
283
|
+
field(:name).present
|
284
|
+
field(:age).present
|
285
|
+
end
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
Named schemas are inherited and can be extended and given options in the same way as the nameless version.
|
290
|
+
|
291
|
+
Named schemas can be retrieved by name, ie. `UpdateUserForm.schema(:query)`.
|
292
|
+
|
293
|
+
If no name given, `.schema` uses `:schema` as default schema name.
|
294
|
+
|