paradocs 1.0.24 → 1.1.4
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/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/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
|
+
|
data/docs/struct.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# Structs
|
2
|
+
## Overview
|
3
|
+
Structs turn schema definitions into objects graphs with attribute readers.
|
4
|
+
|
5
|
+
Add optional `Paradocs::Struct` module to define struct-like objects with schema definitions.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
require 'parametric/struct'
|
9
|
+
|
10
|
+
class User
|
11
|
+
include Paradocs::Struct
|
12
|
+
|
13
|
+
schema do
|
14
|
+
field(:name).type(:string).present
|
15
|
+
field(:friends).type(:array).schema do
|
16
|
+
field(:name).type(:string).present
|
17
|
+
field(:age).type(:integer)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
`User` objects can be instantiated with hash data, which will be coerced and validated as per the schema definition.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
user = User.new(
|
27
|
+
name: 'Joe',
|
28
|
+
friends: [
|
29
|
+
{name: 'Jane', age: 40},
|
30
|
+
{name: 'John', age: 30},
|
31
|
+
]
|
32
|
+
)
|
33
|
+
|
34
|
+
# properties
|
35
|
+
user.name # => 'Joe'
|
36
|
+
user.friends.first.name # => 'Jane'
|
37
|
+
user.friends.last.age # => 30
|
38
|
+
```
|
39
|
+
|
40
|
+
## Errors
|
41
|
+
|
42
|
+
Both the top-level and nested instances contain error information:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
user = User.new(
|
46
|
+
name: '', # invalid
|
47
|
+
friends: [
|
48
|
+
# friend name also invalid
|
49
|
+
{name: '', age: 40},
|
50
|
+
]
|
51
|
+
)
|
52
|
+
|
53
|
+
user.valid? # false
|
54
|
+
user.errors['$.name'] # => "is required and must be present"
|
55
|
+
user.errors['$.friends[0].name'] # => "is required and must be present"
|
56
|
+
|
57
|
+
# also access error in nested instances directly
|
58
|
+
user.friends.first.valid? # false
|
59
|
+
user.friends.first.errors['$.name'] # "is required and must be valid"
|
60
|
+
```
|
61
|
+
|
62
|
+
## .new!(hash)
|
63
|
+
|
64
|
+
Instantiating structs with `.new!(hash)` will raise a `Paradocs::InvalidStructError` exception if the data is validations fail. It will return the struct instance otherwise.
|
65
|
+
|
66
|
+
`Paradocs::InvalidStructError` includes an `#errors` property to inspect the errors raised.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
begin
|
70
|
+
user = User.new!(name: '')
|
71
|
+
rescue Paradocs::InvalidStructError => e
|
72
|
+
e.errors['$.name'] # "is required and must be present"
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
## Nested structs
|
77
|
+
|
78
|
+
You can also pass separate struct classes in a nested schema definition.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class Friend
|
82
|
+
include Paradocs::Struct
|
83
|
+
|
84
|
+
schema do
|
85
|
+
field(:name).type(:string).present
|
86
|
+
field(:age).type(:integer)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class User
|
91
|
+
include Paradocs::Struct
|
92
|
+
|
93
|
+
schema do
|
94
|
+
field(:name).type(:string).present
|
95
|
+
# here we use the Friend class
|
96
|
+
field(:friends).type(:array).schema Friend
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
## Inheritance
|
102
|
+
|
103
|
+
Struct subclasses can add to inherited schemas, or override fields defined in the parent.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class AdminUser < User
|
107
|
+
# inherits User schema, and can add stuff to its own schema
|
108
|
+
schema do
|
109
|
+
field(:permissions).type(:array)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
## #to_h
|
115
|
+
|
116
|
+
`Struct#to_h` returns the ouput hash, with values coerced and any defaults populated.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
class User
|
120
|
+
include Paradocs::Struct
|
121
|
+
schema do
|
122
|
+
field(:name).type(:string)
|
123
|
+
field(:age).type(:integer).default(30)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
user = User.new(name: "Joe")
|
128
|
+
user.to_h # {name: "Joe", age: 30}
|
129
|
+
```
|
130
|
+
|
131
|
+
## Struct equality
|
132
|
+
|
133
|
+
`Paradocs::Struct` implements `#==()` to compare two structs Hash representation (same as `struct1.to_h.eql?(struct2.to_h)`.
|
134
|
+
|
135
|
+
Users can override `#==()` in their own classes to do whatever they need.
|
data/docs/subschema.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Subchemas
|
2
|
+
> When your schema can change on-the-fly.
|
3
|
+
|
4
|
+
## Subschemas and mutations.
|
5
|
+
Sometimes depending on the data the structure may vary. Most frequently used option is to use `:declared` policy (a.k.a. conditional, but below is another option:
|
6
|
+
|
7
|
+
- Mutations are blocks that are assigned to a field and called during the validation (in #resolve), block receives all the related data and should return a subschema name.
|
8
|
+
- Subschemas are conditional schemas declared inside schemas. They doesn't exist until mutation block is called and decides to invoke a subschema.
|
9
|
+
```ruby
|
10
|
+
person_schema = Paradocs::Schema.new do
|
11
|
+
field(:role).type(:string).options(["admin", "user"]).mutates_schema! do |value, key, payload, env|
|
12
|
+
value == :admin ? :admin_schema : :user_schema
|
13
|
+
end
|
14
|
+
|
15
|
+
subschema(:admin_schema) do
|
16
|
+
field(:permissions).present.type(:string).options(["superuser"])
|
17
|
+
field(:admin_field)
|
18
|
+
end
|
19
|
+
subschema(:user_schema) do
|
20
|
+
field(:permissions).present.type(:string).options(["readonly"])
|
21
|
+
field(:user_field)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
results = person_schema.resolve(name: "John", age: 20, role: :admin, permissions: "superuser")
|
26
|
+
results.output # => {name: "John", age: 20, role: :admin, permissions: "superuser", admin_field: nil}
|
27
|
+
results = person_schema.resolve(name: "John", age: 20, role: :admin, permissions: "readonly")
|
28
|
+
results.errors => {"$.permissions"=>["must be one of superuser, but got readonly"]}
|
29
|
+
```
|