paradocs 1.0.24 → 1.1.4

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,106 @@
1
+ # Getting Started
2
+ ## Introduction
3
+ > [Paradocs](https://github.com/mtkachenk0/paradocs) = Extended [Parametric gem](https://github.com/ismasan/parametric) + Documentation Generation
4
+
5
+ ![Ruby](https://github.com/mtkachenk0/paradocs/workflows/Ruby/badge.svg)
6
+
7
+ Declaratively define data schemas in your Ruby objects, and use them to whitelist, validate or transform inputs to your programs.
8
+
9
+ Useful for building self-documeting APIs, search or form objects. Or possibly as an alternative to Rails' _strong parameters_ (it has no dependencies on Rails and can be used stand-alone).
10
+ ## Installation
11
+ ```sh
12
+ $ gem install paradocs
13
+ ```
14
+
15
+ Or with Bundler in your Gemfile.
16
+ ```rb
17
+ gem 'paradocs'
18
+ ```
19
+
20
+ ## Try it out
21
+
22
+ Define a schema
23
+
24
+ ```ruby
25
+ schema = Paradocs::Schema.new do
26
+ field(:title).type(:string).present
27
+ field(:status).options(["draft", "published"]).default("draft")
28
+ field(:tags).type(:array)
29
+ end
30
+ ```
31
+
32
+ Populate and use. Missing keys return defaults, if provided.
33
+
34
+ ```ruby
35
+ form = schema.resolve(title: "A new blog post", tags: ["tech"])
36
+
37
+ form.output # => {title: "A new blog post", tags: ["tech"], status: "draft"}
38
+ form.errors # => {}
39
+ ```
40
+
41
+ Undeclared keys are ignored.
42
+
43
+ ```ruby
44
+ form = schema.resolve(foobar: "BARFOO", title: "A new blog post", tags: ["tech"])
45
+
46
+ form.output # => {title: "A new blog post", tags: ["tech"], status: "draft"}
47
+ ```
48
+
49
+ Validations are run and errors returned
50
+
51
+
52
+ ```ruby
53
+ form = schema.resolve({})
54
+ form.errors # => {"$.title" => ["is required"]}
55
+ ```
56
+
57
+ If options are defined, it validates that value is in options
58
+
59
+ ```ruby
60
+ form = schema.resolve({title: "A new blog post", status: "foobar"})
61
+ form.errors # => {"$.status" => ["expected one of draft, published but got foobar"]}
62
+ ```
63
+
64
+ ## Nested schemas
65
+
66
+ A schema can have nested schemas, for example for defining complex forms.
67
+
68
+ ```ruby
69
+ person_schema = Paradocs::Schema.new do
70
+ field(:name).type(:string).required
71
+ field(:age).type(:integer)
72
+ field(:friends).type(:array).schema do
73
+ field(:name).type(:string).required
74
+ field(:email).policy(:email)
75
+ end
76
+ end
77
+ ```
78
+
79
+ It works as expected
80
+
81
+ ```ruby
82
+ results = person_schema.resolve(
83
+ name: "Joe",
84
+ age: "38",
85
+ friends: [
86
+ {name: "Jane", email: "jane@email.com"}
87
+ ]
88
+ )
89
+
90
+ results.output # => {name: "Joe", age: 38, friends: [{name: "Jane", email: "jane@email.com"}]}
91
+ ```
92
+
93
+ Validation errors use [JSON path](http://goessner.net/articles/JsonPath/) expressions to describe errors in nested structures
94
+
95
+ ```ruby
96
+ results = person_schema.resolve(
97
+ name: "Joe",
98
+ age: "38",
99
+ friends: [
100
+ {email: "jane@email.com"}
101
+ ]
102
+ )
103
+
104
+ results.errors # => {"$.friends[0].name" => "is required"}
105
+ ```
106
+
@@ -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