paradocs 1.0.22 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,105 @@
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
+
7
+ #### Example schema
8
+ ```ruby
9
+ schema = Paradocs::Schema.new do
10
+ field(:data).type(:object).present.schema do
11
+ field(:id).type(:integer).present.policy(:policy_with_error)
12
+ field(:name).type(:string).meta(label: "very important staff")
13
+ field(:role).type(:string).declared.options(["admin", "user"]).default("user").mutates_schema! do |*|
14
+ :test_subschema
15
+ end
16
+ field(:extra).type(:array).required.schema do
17
+ field(:extra).declared.default(false).policy(:policy_with_silent_error)
18
+ end
19
+
20
+ mutation_by!(:name) { :subschema }
21
+
22
+ subschema(:subschema) do
23
+ field(:test_field).present
24
+ end
25
+ subschema(:test_subschema) do
26
+ field(:test1).present
27
+ end
28
+ end
29
+ end
30
+ ```
31
+
32
+ ## Generate payloads
33
+
34
+ ```rb
35
+ Paradocs::Extensions::PayloadBuilder.new(schema).build! # =>
36
+ # or
37
+ schema.example_payloads.to_json # =>
38
+ {
39
+ "subschema": {
40
+ "data": {
41
+ "name": null,
42
+ "role": "user",
43
+ "extra": [{"extra": null}],
44
+ "test_field": null,
45
+ "id": null
46
+ }
47
+ },
48
+ "test_subschema": {
49
+ "data": {
50
+ "name": null,
51
+ "role": "user",
52
+ "extra": [{"extra": null}],
53
+ "test1": null,
54
+ "id": null
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## Customize payload generation logic
61
+ `PayloadBuilder#build!` arguments:
62
+
63
+ 1. `sort_by_schema: true` will try to return payload in the same way as declared in the schema.
64
+ 2. `&block` will be executed for each key receiving the following arguments:
65
+ - `key`: Field name
66
+ - `meta`: Field meta data (that includes (if provided) field types, presence data, policies and other meta data
67
+ - `example_value`: Provided by generator example value.
68
+ - `skip_word`: Return this argument back if you want this item to be ommitted.
69
+
70
+ ```rb
71
+ block = Proc.new do |key, meta, example, skip_word|
72
+ if key.to_s == "name"
73
+ "John Smith"
74
+ elsif meta[:type] == :integer
75
+ 13
76
+ elsif key.to_s.match? /test/
77
+ skip_word
78
+ else
79
+ example
80
+ end
81
+ end
82
+ schema.example_payloads(&block) # =>
83
+ # or
84
+ Paradocs::Extensions::PayloadBuilder.new(schema).build!(&block) # =>
85
+ {
86
+ "subschema": {
87
+ "data": {
88
+ "name": "John Smith", # value is changed
89
+ "role": "user", # random choice from field(:user).options
90
+ "extra": [{"extra": null}], # null are defaults
91
+ "id": 13 # value is changed
92
+ # NOTE: fields matching with /test/ are ommitted: test_field, test1
93
+ }
94
+ },
95
+ "test_subschema": {
96
+ "data": {
97
+ "name": "John Smith",
98
+ "role": "user",
99
+ "extra": [{"extra": null}],
100
+ "id": 13
101
+ }
102
+ }
103
+ }
104
+
105
+ ```
@@ -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
@@ -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
+