paradocs 1.0.24 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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