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.
- checksums.yaml +4 -4
- data/.readthedocs.yml +5 -0
- data/README.md +1 -11
- data/docs/changelog.md +17 -0
- data/docs/custom_configuration.md +10 -0
- data/docs/documentation_generation.md +304 -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 +105 -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 +45 -0
- data/lib/paradocs/extensions/structure.rb +119 -0
- data/lib/paradocs/field_dsl.rb +12 -0
- data/lib/paradocs/schema.rb +32 -10
- data/lib/paradocs/struct.rb +1 -0
- data/lib/paradocs/version.rb +1 -1
- data/mkdocs.yml +17 -0
- data/paradocs.gemspec +3 -3
- data/requirements.txt +1 -0
- data/spec/extensions/payload_builder_spec.rb +70 -0
- data/spec/extensions/structures_spec.rb +250 -0
- data/spec/field_spec.rb +1 -1
- data/spec/schema_spec.rb +7 -7
- data/spec/struct_spec.rb +8 -8
- data/spec/subschema_spec.rb +4 -4
- metadata +29 -12
- data/lib/paradocs/extensions/insides.rb +0 -77
- data/spec/schema_structures_spec.rb +0 -169
data/docs/index.md
ADDED
@@ -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
|
+

|
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
|
+
```
|
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
|