paradocs 1.0.24 → 1.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b3362dbf83838f9f786222294f5438ed7d24bfc
4
- data.tar.gz: 3ac665829a9993b65f21a15d95b71e874e1029db
3
+ metadata.gz: 0df2185fac7b1e70254c9531622e135c6609f309
4
+ data.tar.gz: 701ec92673e9044bc8a8384e251276aa7863d37e
5
5
  SHA512:
6
- metadata.gz: 38a9d697a211fd51f1e92bb289de5e7397971141188c15093880033cec1fee862d2bc3a1be00b0ac81b19dd97598e60e4f4e1e6d6aaa7222809fcd11850dc170
7
- data.tar.gz: 2834c21794d059e1d1dbb48b09dceacf9379bceb83199709b8d56d92a91a36410418681ed5b0b815272bd51b5dcbc84e7b6a678f4876eb1441f0efdc9bad0af2
6
+ metadata.gz: aa14905a01be3d1d54589aabc53fbc2f14d3a17352742a9eece1e76b324f146d94f5860ae4f59d738007e809c1fb0eb3ae95579d2a8237798e80a7a6f43ff2b9
7
+ data.tar.gz: 0c5fedee64178f630d403db573b2d8a4e2e4bc44b88e65df636c88aa1d9857836267d77ed34a1236036ff97bb105e37252f4948d46fcdc600850e682a5db082d
@@ -0,0 +1,5 @@
1
+ version: 2
2
+
3
+ mkdocs:
4
+ configuration: mkdocs.yml
5
+ fail_on_warning: false
@@ -0,0 +1,10 @@
1
+ # Configuration
2
+ ```rb
3
+ Paradocs.configure do |config|
4
+ config.explicit_errors = false # set to true if you want all errors from the policies to be explicitly registered in the policy
5
+ config.whitelisted_keys = [] # enrich it with global white-listed keys if you use WhiteList feature
6
+ config.default_schema_name = :schema # this name will be set for unnamed schemas
7
+ config.meta_prefix = "_" # used in #structure and #flatten_structure methods. All the metadata will be prefixed with this prefix.
8
+ config.whitelist_coercion = nil # set up a Proc here, that receives |value, field.meta| for each whitelisted field in order to enrich the whitelisting logic.
9
+ end
10
+ ```
@@ -0,0 +1,291 @@
1
+ # Documentation Generation
2
+
3
+ A `Schema` instance has a `#structure` method that return `Paradocs::Extensions::Structure` instance that allows instrospecting schema meta data.
4
+
5
+ It's supposed to have the following schema:
6
+ ```ruby
7
+ schema = Paradocs::Schema.new do
8
+ field(:data).type(:object).present.schema do
9
+ field(:id).type(:integer).present.policy(:policy_with_error)
10
+ field(:name).type(:string).meta(label: "very important staff")
11
+ field(:role).type(:string).declared.options(["admin", "user"]).default("user").mutates_schema! do |*|
12
+ :test_subschema
13
+ end
14
+ field(:extra).type(:array).required.schema do
15
+ field(:extra).declared.default(false).policy(:policy_with_silent_error)
16
+ end
17
+
18
+ mutation_by!(:name) { :subschema }
19
+
20
+ subschema(:subschema) do
21
+ field(:test_field).present
22
+ end
23
+ subschema(:test_subschema) do
24
+ field(:test1).present
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ ## Structure#nested
31
+ > This method returns schema structure in a nested way including subschemes.
32
+ ```ruby
33
+ schema.structure.nested.to_json # =>
34
+ {
35
+ "_errors": ["ArgumentError"],
36
+ "_subschemes": {},
37
+ "data": {
38
+ "type": "object",
39
+ "required": true,
40
+ "present": true,
41
+ "json_path": "$.data",
42
+ "nested_name": "data",
43
+ "structure": {
44
+ "_subschemes": {
45
+ "subschema": {
46
+ "_errors": [],
47
+ "_subschemes": {},
48
+ "test_field": {
49
+ "required": true,
50
+ "present": true,
51
+ "json_path": "$.data.test_field",
52
+ "nested_name": "data.test_field"
53
+ }
54
+ },
55
+ "test_subschema": {
56
+ "_errors": [],
57
+ "_subschemes": {},
58
+ "test1": {
59
+ "required": true,
60
+ "present": true,
61
+ "json_path": "$.data.test1",
62
+ "nested_name": "data.test1"
63
+ }
64
+ }
65
+ },
66
+ "id": {
67
+ "type": "integer",
68
+ "required": true,
69
+ "present": true,
70
+ "policy_with_error": {"errors": ["ArgumentError"]},
71
+ "json_path": "$.data.id",
72
+ "nested_name": "data.id"
73
+ },
74
+ "name": {
75
+ "type": "string",
76
+ "label": "very important staff",
77
+ "json_path": "$.data.name",
78
+ "mutates_schema": true,
79
+ "nested_name": "data.name"
80
+ },
81
+ "role": {
82
+ "type": "string",
83
+ "options": ["admin", "user"],
84
+ "default": "user",
85
+ "json_path": "$.data.role",
86
+ "mutates_schema": true,
87
+ "nested_name": "data.role"
88
+ },
89
+ "extra": {
90
+ "type": "array",
91
+ "required": true,
92
+ "json_path": "$.data.extra[]",
93
+ "nested_name": "data.extra",
94
+ "structure": {
95
+ "_subschemes": {},
96
+ "extra": {
97
+ "default": false,
98
+ "policy_with_silent_error": {"errors": []},
99
+ "json_path": "$.data.extra[].extra",
100
+ "nested_name": "data.extra.extra"
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## Structure#flatten
110
+ > This method returns schema structure in a flatten (without deep nesting) way including subschemes.
111
+ ```rb
112
+ schema.structure.flatten.to_json # =>
113
+ {
114
+ "_errors": ["ArgumentError"],
115
+ "_subschemes": {
116
+ "subschema": {
117
+ "_errors": [],
118
+ "_subschemes": {},
119
+ "data.test_field": {
120
+ "required": true,
121
+ "present": true,
122
+ "json_path": "$.data.test_field"
123
+ }
124
+ },
125
+ "test_subschema": {
126
+ "_errors": [],
127
+ "_subschemes": {},
128
+ "data.test1": {
129
+ "required": true,
130
+ "present": true,
131
+ "json_path": "$.data.test1"
132
+ }
133
+ }
134
+ },
135
+ "data": {
136
+ "type": "object",
137
+ "required": true,
138
+ "present": true,
139
+ "json_path": "$.data"
140
+ },
141
+ "data.id": {
142
+ "type": "integer",
143
+ "required": true,
144
+ "present": true,
145
+ "policy_with_error": {"errors": ["ArgumentError"]},
146
+ "json_path": "$.data.id"
147
+ },
148
+ "data.name": {
149
+ "type": "string",
150
+ "label": "very important staff",
151
+ "json_path": "$.data.name",
152
+ "mutates_schema": true
153
+ },
154
+ "data.role": {
155
+ "type": "string",
156
+ "options": ["admin", "user"],
157
+ "default": "user",
158
+ "json_path": "$.data.role",
159
+ "mutates_schema": true
160
+ },
161
+ "data.extra": {
162
+ "type": "array",
163
+ "required": true,
164
+ "json_path": "$.data.extra[]"
165
+ },
166
+ "data.extra.extra": {
167
+ "default": false,
168
+ "policy_with_silent_error": {"errors": []},
169
+ "json_path": "$.data.extra[].extra"
170
+ }
171
+ }
172
+ ```
173
+
174
+
175
+ ## Structure#all_nested
176
+
177
+ > This method returns all available combinations of schema (built on subschemas) saving the nesting.
178
+
179
+ Will return a hash with 2 structures named by the names of declared subschemas:
180
+ ```rb
181
+ all_nested = schema.structure.all_nested
182
+ all_nested.keys # => [:subschema, :test_subschema]
183
+ all_nested[:subschema] # =>
184
+ {
185
+ _errors: [],
186
+ "data" => {
187
+ type: :object,
188
+ required: true,
189
+ present: true,
190
+ json_path: "$.data",
191
+ structure: {
192
+ "role" => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true},
193
+ "extra" => {type: :array, required: true, json_path: "$.data.extra[]", structure: {"extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra"}}},
194
+ "test_field" => {required: true, present: true, json_path: "$.data.test_field"},
195
+ "id" => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id"},
196
+ "name" => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true}
197
+ }
198
+ }
199
+ }
200
+ all_nested[:test_subschema] # =>
201
+ {
202
+ _errors: [],
203
+ "data" => {
204
+ type: :object,
205
+ required: true,
206
+ present: true,
207
+ json_path: "$.data",
208
+ structure: {
209
+ "role" => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true},
210
+ "extra" => {type: :array, required: true, json_path: "$.data.extra[]", structure: {"extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra"}}},
211
+ "test1" => {required: true, present: true, json_path: "$.data.test1"},
212
+ "id" => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id"},
213
+ "name" => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true}
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
219
+ ## Structure#all_flatten
220
+ > This method returns all available combinations of schema (built on subschema) without nesting (the same way as Structure#flatten method does)
221
+
222
+ Schema is the same as described in Structure#all_nested
223
+ ```rb
224
+ schema.structure.all_flatten # =>
225
+ {
226
+ subschema: {
227
+ _errors: [],
228
+ "data" => {type: :object, required: true, present: true, json_path: "$.data"},
229
+ "data.id" => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id"},
230
+ "data.name" => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true},
231
+ "data.role" => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true},
232
+ "data.extra" => {type: :array, required: true, json_path: "$.data.extra[]"},
233
+ "data.extra.extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra"},
234
+ "data.test_field" => {required: true, present: true, json_path: "$.data.test_field"}
235
+ },
236
+ test_subschema: {
237
+ _errors: [],
238
+ "data" => {type: :object, required: true, present: true, json_path: "$.data"},
239
+ "data.id" => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id"},
240
+ "data.name" => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true},
241
+ "data.role" => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true},
242
+ "data.extra" => {type: :array, required: true, json_path: "$.data.extra[]"},
243
+ "data.extra.extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra"},
244
+ "data.test1" => {required: true, present: true, json_path: "$.data.test1"}
245
+ }
246
+ }
247
+ ```
248
+
249
+ ## Schema#walk
250
+
251
+ The `#walk` method can recursively walk a schema definition and extract meta data or field attributes.
252
+
253
+ ```ruby
254
+ schema_documentation = create_user_schema.walk do |field|
255
+ {type: field.meta_data[:type], label: field.meta_data[:label]}
256
+ end.output
257
+
258
+ # Returns
259
+
260
+ {
261
+ name: {type: :string, label: "User's full name"},
262
+ age: {type: :integer, label: "User's age"},
263
+ status: {type: :string, label: nil},
264
+ friends: [
265
+ {
266
+ name: {type: :string, label: "Friend full name"},
267
+ email: {type: nil, label: "Friend email"}
268
+ }
269
+ ]
270
+ }
271
+ ```
272
+
273
+ When passed a _symbol_, it will collect that key from field meta data.
274
+
275
+ ```ruby
276
+ schema_labels = create_user_schema.walk(:label).output
277
+
278
+ # returns
279
+
280
+ {
281
+ name: "User's full name",
282
+ age: "User's age",
283
+ status: nil,
284
+ friends: [
285
+ {name: "Friend full name", email: "Friend email"}
286
+ ]
287
+ }
288
+ ```
289
+
290
+ Potential uses for this are generating documentation (HTML, or [JSON Schema](http://json-schema.org/), [Swagger](http://swagger.io/), or maybe even mock API endpoints with example data.
291
+
@@ -0,0 +1,21 @@
1
+ # FAQ
2
+ ## Defaults
3
+ ### Q: I need the child schema to be enriched with the specified defaults is parent key is absent.
4
+ ```rb
5
+ schema do
6
+ field(:top_level).type(:string).required.default('top_level')
7
+ field(:nested).type(:object).required.schema do
8
+ field(:start_date).type(:datetime).required.default(->(a,b,c) { Time.now })
9
+ field(:ends_after).type(:integer).required.default(5)
10
+ end
11
+ end
12
+ # usage
13
+ TestSchema.schema.resolve({}).output # => {:top_level=>"top_level", :nested=>nil, :configurations=>nil}
14
+ TestSchema.schema.resolve({nested: {}}).output # => {:top_level=>"top_level", :nested=>{:start_date=>#<DateTime: 2020-08-31T15:36:43+02:00 ((2459093j,49003s,0n),+7200s,2299161j)>, :ends_after=>5}, :configurations=>nil}
15
+ # I want resolving on {} to include the :nested structure.
16
+ ```
17
+
18
+ ### A: Set `.default({})` to your `:nested` field.
19
+ > Fields from nested schema are invoked only when the object for the schema exists.
20
+
21
+
@@ -0,0 +1,90 @@
1
+ # Form objects DSL
2
+
3
+ ## DSL
4
+ You can use schemas and fields on their own, or include the `DSL` module in your own classes to define form objects.
5
+
6
+ ```ruby
7
+ require "parametric/dsl"
8
+
9
+ class CreateUserForm
10
+ include Paradocs::DSL
11
+
12
+ schema(:test) do
13
+ field(:name).type(:string).required
14
+ field(:email).policy(:email).required
15
+ field(:age).type(:integer)
16
+ subschema_by(:age) { |age| age > 18 ? :allow : :deny }
17
+ end
18
+
19
+ subschema_for(:test, name: :allow) { field(:role).options(["sign_in"]) }
20
+ subschema_for(:test, name: :deny) { field(:role).options([]) }
21
+
22
+ attr_reader :params, :errors
23
+
24
+ def initialize(input_data)
25
+ results = self.class.schema.resolve(input_data)
26
+ @params = results.output
27
+ @errors = results.errors
28
+ end
29
+
30
+ def run!
31
+ if !valid?
32
+ raise InvalidFormError.new(errors)
33
+ end
34
+
35
+ run
36
+ end
37
+
38
+ def valid?
39
+ !errors.any?
40
+ end
41
+
42
+ private
43
+
44
+ def run
45
+ User.create!(params)
46
+ end
47
+ end
48
+ ```
49
+
50
+ Form schemas can also be defined by passing another form or schema instance. This can be useful when building form classes in runtime.
51
+
52
+ ```ruby
53
+ UserSchema = Paradocs::Schema.new do
54
+ field(:name).type(:string).present
55
+ field(:age).type(:integer)
56
+ end
57
+
58
+ class CreateUserForm
59
+ include Paradocs::DSL
60
+ # copy from UserSchema
61
+ schema UserSchema
62
+ end
63
+ ```
64
+
65
+ ## Form object inheritance
66
+
67
+ Sub classes of classes using the DSL will inherit schemas defined on the parent class.
68
+
69
+ ```ruby
70
+ class UpdateUserForm < CreateUserForm
71
+ # All field definitions in the parent are conserved.
72
+ # New fields can be defined
73
+ # or existing fields overriden
74
+ schema do
75
+ # make this field optional
76
+ field(:name).declared.present
77
+ end
78
+
79
+ def initialize(user, input_data)
80
+ super input_data
81
+ @user = user
82
+ end
83
+
84
+ private
85
+ def run
86
+ @user.update params
87
+ end
88
+ end
89
+ ```
90
+
@@ -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
+