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,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
+
@@ -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.
@@ -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
+ ```