paradocs 1.0.22 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e89cffb387938580ebcb23782856781be60903c
4
- data.tar.gz: 8bb05339c65e90d579f86ec103e055d3b3965d29
3
+ metadata.gz: 5dc965cce0c862cdc35b68bbf6be48418885c69b
4
+ data.tar.gz: a0763156554745078e092b79be02d4103b906b77
5
5
  SHA512:
6
- metadata.gz: 4f1f47dce1ccdf9bd688fdc754b5759ad74051b8c976d70344ddde239badab52ce764c077b48aa0b29155ab0f81caa8645d818d5482367e3b1217bcc3230e039
7
- data.tar.gz: fb12f58e2d16b2f3471576aa76020cec0dcc0f5b994b06a66f402f8411b17e14922aa8026b6600d157a6590c3a0fadf3cd2d0687c9cf3f847104c83dcdf9b70c
6
+ metadata.gz: 6664235a1e30f28196a1e58c1826ec7ff22f33b10db0275998ea21eed1dbfb7f17d76de8b9d75f3a57ba0621bf309874a3d0bb2ba099b2527228bfe777ffb68c
7
+ data.tar.gz: 6d2658f064f28a80f148f7ababe14bb5dafdd65a46a84a3c9786cb9413d65bc6480cc4bef8c7d8cf6b8e7e52cb7f2868d53e0df40c7e1cbb265909b9f4b34876
@@ -0,0 +1,5 @@
1
+ version: 2
2
+
3
+ mkdocs:
4
+ configuration: mkdocs.yml
5
+ fail_on_warning: false
data/README.md CHANGED
@@ -6,9 +6,13 @@ Declaratively define data schemas in your Ruby objects, and use them to whitelis
6
6
 
7
7
  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).
8
8
  ## Installation
9
+ ```sh
10
+ $ gem install paradocs
11
+ ```
9
12
 
13
+ Or with Bundler in your Gemfile.
10
14
  ```rb
11
- gem 'paradocs', git: 'https://github.com/mtkachenk0/paradocs'
15
+ gem 'paradocs'
12
16
  ```
13
17
 
14
18
  ## Getting Started
@@ -32,1047 +36,5 @@ form.output # => {title: "A new blog post", tags: ["tech"], status: "draft"}
32
36
  form.errors # => {}
33
37
  ```
34
38
 
35
- Undeclared keys are ignored.
36
-
37
- ```ruby
38
- form = schema.resolve(foobar: "BARFOO", title: "A new blog post", tags: ["tech"])
39
-
40
- form.output # => {title: "A new blog post", tags: ["tech"], status: "draft"}
41
- ```
42
-
43
- Validations are run and errors returned
44
-
45
-
46
- ```ruby
47
- form = schema.resolve({})
48
- form.errors # => {"$.title" => ["is required"]}
49
- ```
50
-
51
- If options are defined, it validates that value is in options
52
-
53
- ```ruby
54
- form = schema.resolve({title: "A new blog post", status: "foobar"})
55
- form.errors # => {"$.status" => ["expected one of draft, published but got foobar"]}
56
- ```
57
-
58
- ## Nested schemas
59
-
60
- A schema can have nested schemas, for example for defining complex forms.
61
-
62
- ```ruby
63
- person_schema = Paradocs::Schema.new do
64
- field(:name).type(:string).required
65
- field(:age).type(:integer)
66
- field(:friends).type(:array).schema do
67
- field(:name).type(:string).required
68
- field(:email).policy(:email)
69
- end
70
- end
71
- ```
72
-
73
- It works as expected
74
-
75
- ```ruby
76
- results = person_schema.resolve(
77
- name: "Joe",
78
- age: "38",
79
- friends: [
80
- {name: "Jane", email: "jane@email.com"}
81
- ]
82
- )
83
-
84
- results.output # => {name: "Joe", age: 38, friends: [{name: "Jane", email: "jane@email.com"}]}
85
- ```
86
-
87
- Validation errors use [JSON path](http://goessner.net/articles/JsonPath/) expressions to describe errors in nested structures
88
-
89
- ```ruby
90
- results = person_schema.resolve(
91
- name: "Joe",
92
- age: "38",
93
- friends: [
94
- {email: "jane@email.com"}
95
- ]
96
- )
97
-
98
- results.errors # => {"$.friends[0].name" => "is required"}
99
- ```
100
-
101
- ## Built-in policies
102
-
103
- Type coercions (the `type` method) and validations (the `validate` method) are all _policies_.
104
-
105
- Paradocs ships with a number of built-in policies.
106
-
107
- ### :string
108
-
109
- Calls `:to_s` on the value
110
-
111
- ```ruby
112
- field(:title).type(:string)
113
- ```
114
-
115
- ### :integer
116
-
117
- Calls `:to_i` on the value
118
-
119
- ```ruby
120
- field(:age).type(:integer)
121
- ```
122
-
123
- ### :number
124
-
125
- Calls `:to_f` on the value
126
-
127
- ```ruby
128
- field(:price).type(:number)
129
- ```
130
-
131
- ### :boolean
132
-
133
- Returns `true` or `false` (`nil` is converted to `false`).
134
-
135
- ```ruby
136
- field(:published).type(:boolean)
137
- ```
138
-
139
- ### :datetime
140
-
141
- 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.
142
-
143
- ```ruby
144
- field(:expires_on).type(:datetime)
145
- ```
146
-
147
- ### :format
148
-
149
- Check value against custom regexp
150
-
151
- ```ruby
152
- field(:salutation).policy(:format, /^Mr\/s/)
153
- # optional custom error message
154
- field(:salutation).policy(:format, /^Mr\/s\./, "must start with Mr/s.")
155
- ```
156
-
157
- ### :email
158
-
159
- ```ruby
160
- field(:business_email).policy(:email)
161
- ```
162
-
163
- ### :required
164
-
165
- Check that the key exists in the input.
166
-
167
- ```ruby
168
- field(:name).required
169
-
170
- # same as
171
- field(:name).policy(:required)
172
- ```
173
-
174
- Note that _required_ does not validate that the value is not empty. Use _present_ for that.
175
-
176
- ### :present
177
-
178
- Check that the key exists and the value is not blank.
179
-
180
- ```ruby
181
- field(:name).present
182
-
183
- # same as
184
- field(:name).policy(:present)
185
- ```
186
-
187
- 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`.
188
-
189
- ### :declared
190
-
191
- Check that a key exists in the input, or stop any further validations otherwise.
192
- This is useful when chained to other validations. For example:
193
-
194
- ```ruby
195
- field(:name).declared.present
196
- ```
197
-
198
- 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.
199
-
200
- ### :gt
201
-
202
- Validate that the value is greater than a number
203
-
204
- ```ruby
205
- field(:age).policy(:gt, 21)
206
- ```
207
-
208
- ### :lt
209
-
210
- Validate that the value is less than a number
211
-
212
- ```ruby
213
- field(:age).policy(:lt, 21)
214
- ```
215
-
216
- ### :options
217
-
218
- Pass allowed values for a field
219
-
220
- ```ruby
221
- field(:status).options(["draft", "published"])
222
-
223
- # Same as
224
- field(:status).policy(:options, ["draft", "published"])
225
- ```
226
-
227
- ### :length
228
-
229
- Specify value's length constraints
230
- - `min:` - The attribute cannot have less than the specified length.
231
- - `max` - The attribute cannot have more than the specified length.
232
- - `eq` - The attribute should be exactly equal to the specified length.
233
-
234
- ```ruby
235
- field(:name).length(min: 5, max: 25)
236
- field(:name).length(eq: 10)
237
- ```
238
-
239
- ### :split
240
-
241
- Split comma-separated string values into an array.
242
- Useful for parsing comma-separated query-string parameters.
243
-
244
- ```ruby
245
- field(:status).policy(:split) # turns "pending,confirmed" into ["pending", "confirmed"]
246
- ```
247
-
248
- ## Custom policies
249
-
250
- 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`
251
-
252
- ```ruby
253
- class MyPolicy < Paradocs::BasePolicy
254
- # Validation error message, if invalid
255
- def message
256
- 'is invalid'
257
- end
258
-
259
- # Whether or not to validate and coerce this value
260
- # if false, no other policies will be run on the field
261
- def eligible?(value, key, payload)
262
- true
263
- end
264
-
265
- # Transform the value
266
- def coerce(value, key, context)
267
- value
268
- end
269
-
270
- # Is the value valid?
271
- def validate(value, key, payload)
272
- true
273
- end
274
-
275
- # merge this object into the field's meta data
276
- def meta_data
277
- {type: :string}
278
- end
279
- end
280
- ```
281
-
282
-
283
- You can register your policy with:
284
-
285
- ```ruby
286
- Paradocs.policy :my_policy, MyPolicy
287
- ```
288
-
289
- And then refer to it by name when declaring your schema fields
290
-
291
- ```ruby
292
- field(:title).policy(:my_policy)
293
- ```
294
-
295
- You can chain custom policies with other policies.
296
-
297
- ```ruby
298
- field(:title).required.policy(:my_policy)
299
- ```
300
-
301
- Note that you can also register instances.
302
-
303
- ```ruby
304
- Paradocs.policy :my_policy, MyPolicy.new
305
- ```
306
-
307
- For example, a policy that can be configured on a field-by-field basis:
308
-
309
- ```ruby
310
- class AddJobTitle
311
- def initialize(job_title)
312
- @job_title = job_title
313
- end
314
-
315
- def message
316
- 'is invalid'
317
- end
318
-
319
- # Noop
320
- def eligible?(value, key, payload)
321
- true
322
- end
323
-
324
- # Add job title to value
325
- def coerce(value, key, context)
326
- "#{value}, #{@job_title}"
327
- end
328
-
329
- # Noop
330
- def validate(value, key, payload)
331
- true
332
- end
333
-
334
- def meta_data
335
- {}
336
- end
337
- end
338
-
339
- # Register it
340
- Paradocs.policy :job_title, AddJobTitle
341
- ```
342
-
343
- Now you can reuse the same policy with different configuration
344
-
345
- ```ruby
346
- manager_schema = Paradocs::Schema.new do
347
- field(:name).type(:string).policy(:job_title, "manager")
348
- end
349
-
350
- cto_schema = Paradocs::Schema.new do
351
- field(:name).type(:string).policy(:job_title, "CTO")
352
- end
353
-
354
- manager_schema.resolve(name: "Joe Bloggs").output # => {name: "Joe Bloggs, manager"}
355
- cto_schema.resolve(name: "Joe Bloggs").output # => {name: "Joe Bloggs, CTO"}
356
- ```
357
-
358
- ## Custom policies, short version
359
-
360
- For simple policies that don't need all policy methods, you can:
361
-
362
- ```ruby
363
- Paradocs.policy :cto_job_title do
364
- coerce do |value, key, context|
365
- "#{value}, CTO"
366
- end
367
- end
368
-
369
- # use it
370
- cto_schema = Paradocs::Schema.new do
371
- field(:name).type(:string).policy(:cto_job_title)
372
- end
373
- ```
374
-
375
- ```ruby
376
- Paradocs.policy :over_21_and_under_25 do
377
- coerce do |age, key, context|
378
- age.to_i
379
- end
380
-
381
- validate do |age, key, context|
382
- age > 21 && age < 25
383
- end
384
- end
385
- ```
386
-
387
- ## Cloning schemas
388
-
389
- The `#clone` method returns a new instance of a schema with all field definitions copied over.
390
-
391
- ```ruby
392
- new_schema = original_schema.clone
393
- ```
394
-
395
- New copies can be further manipulated without affecting the original.
396
-
397
- ```ruby
398
- # See below for #policy and #ignore
399
- new_schema = original_schema.clone.policy(:declared).ignore(:id) do |sc|
400
- field(:another_field).present
401
- end
402
- ```
403
-
404
- ## Merging schemas
405
-
406
- The `#merge` method will merge field definitions in two schemas and produce a new schema instance.
407
-
408
- ```ruby
409
- basic_user_schema = Paradocs::Schema.new do
410
- field(:name).type(:string).required
411
- field(:age).type(:integer)
412
- end
413
-
414
- friends_schema = Paradocs::Schema.new do
415
- field(:friends).type(:array).schema do
416
- field(:name).required
417
- field(:email).policy(:email)
418
- end
419
- end
420
-
421
- user_with_friends_schema = basic_user_schema.merge(friends_schema)
422
-
423
- results = user_with_friends_schema.resolve(input)
424
- ```
425
-
426
- Fields defined in the merged schema will override fields with the same name in the original schema.
427
-
428
- ```ruby
429
- required_name_schema = Paradocs::Schema.new do
430
- field(:name).required
431
- field(:age)
432
- end
433
-
434
- optional_name_schema = Paradocs::Schema.new do
435
- field(:name)
436
- end
437
-
438
- # This schema now has :name and :age fields.
439
- # :name has been redefined to not be required.
440
- new_schema = required_name_schema.merge(optional_name_schema)
441
- ```
442
-
443
- ## #meta
444
-
445
- The `#meta` field method can be used to add custom meta data to field definitions.
446
- These meta data can be used later when instrospecting schemas (ie. to generate documentation or error notices).
447
-
448
- ```ruby
449
- create_user_schema = Paradocs::Schema.new do
450
- field(:name).required.type(:string).meta(label: "User's full name")
451
- field(:status).options(["published", "unpublished"]).default("published")
452
- field(:age).type(:integer).meta(label: "User's age")
453
- field(:friends).type(:array).meta(label: "User friends").schema do
454
- field(:name).type(:string).present.meta(label: "Friend full name")
455
- field(:email).policy(:email).meta(label: "Friend's email")
456
- end
457
- end
458
- ```
459
-
460
- ### Subschemas and mutations.
461
- 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:
462
-
463
- - 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.
464
- - Subschemas are conditional schemas declared inside schemas. They doesn't exist until mutation block is called and decides to invoke a subschema.
465
- ```ruby
466
- person_schema = Paradocs::Schema.new do
467
- field(:role).type(:string).options(["admin", "user"]).mutates_schema! do |value, key, payload, env|
468
- value == :admin ? :admin_schema : :user_schema
469
- end
470
-
471
- subschema(:admin_schema) do
472
- field(:permissions).present.type(:string).options(["superuser"])
473
- field(:admin_field)
474
- end
475
- subschema(:user_schema) do
476
- field(:permissions).present.type(:string).options(["readonly"])
477
- field(:user_field)
478
- end
479
- end
480
-
481
- results = person_schema.resolve(name: "John", age: 20, role: :admin, permissions: "superuser")
482
- results.output # => {name: "John", age: 20, role: :admin, permissions: "superuser", admin_field: nil}
483
- results = person_schema.resolve(name: "John", age: 20, role: :admin, permissions: "readonly")
484
- results.errors => {"$.permissions"=>["must be one of superuser, but got readonly"]}
485
- ```
486
-
487
- ### Reusing nested schemas
488
-
489
- You can optionally use an existing schema instance as a nested schema:
490
-
491
- ```ruby
492
- friends_schema = Paradocs::Schema.new do
493
- field(:friends).type(:array).schema do
494
- field(:name).type(:string).required
495
- field(:email).policy(:email)
496
- end
497
- end
498
-
499
- person_schema = Paradocs::Schema.new do
500
- field(:name).type(:string).required
501
- field(:age).type(:integer)
502
- # Nest friends_schema
503
- field(:friends).type(:array).schema(friends_schema)
504
- end
505
- ```
506
-
507
- ## Structures
508
- ### #structure
509
-
510
- A `Schema` instance has a `#structure` method that allows instrospecting schema meta data.
511
-
512
- ```ruby
513
- create_user_schema.structure[:name][:label] # => "User's full name"
514
- create_user_schema.structure[:age][:label] # => "User's age"
515
- create_user_schema.structure[:friends][:label] # => "User friends"
516
- # Recursive schema structures
517
- create_user_schema.structure => {
518
- _errors: [],
519
- _subschemes: {},
520
- name: {
521
- required: true,
522
- type: :string,
523
- label: "User's full name"
524
- },
525
- status: {
526
- options: ["published", "unpublished"],
527
- default: "published"
528
- },
529
- age: {
530
- type: :integer,
531
- label: "User's age"
532
- },
533
- friends: {
534
- type: :array,
535
- label: "User friends",
536
- structure: {
537
- _subschemes: {},
538
- name: {
539
- type: :string,
540
- required: true,
541
- present: true,
542
- label: "Friend full name"
543
- },
544
- email: {label: "Friend's email"}
545
- }
546
- }
547
- }
548
- ```
549
-
550
- Note that many field policies add field meta data.
551
-
552
- ```ruby
553
- create_user_schema.structure[:name][:type] # => :string
554
- create_user_schema.structure[:name][:required] # => true
555
- create_user_schema.structure[:status][:options] # => ["published", "unpublished"]
556
- create_user_schema.structure[:status][:default] # => "published"
557
- ```
558
- ## #flatten_structure
559
- A `Schema` instance also has a `#flatten_structure` method that allows instrospecting schema meta data without deep nesting.
560
- ```rb
561
- {
562
- _errors: [],
563
- _subschemes: {},
564
- "name"=>{
565
- required: true,
566
- type: :string,
567
- label: "User's full name",
568
- json_path: "$.name"
569
- },
570
- "status"=>{
571
- options: ["published", "unpublished"],
572
- default: "published",
573
- json_path: "$.status"
574
- },
575
- "age"=>{
576
- type: :integer,
577
- label: "User's age", :json_path=>"$.age"
578
- },
579
- "friends"=>{
580
- type: :array,
581
- label: "User friends",
582
- json_path: "$.friends"
583
- },
584
- "friends.name"=>{
585
- type: :string,
586
- required: true,
587
- present: true,
588
- label: "Friend full name",
589
- json_path: "$.friends.name"
590
- },
591
- "friends.email"=>{
592
- label: "Friend's email",
593
- json_path: "$.friends.email"
594
- }
595
- }
596
- ```
597
-
598
- ## #walk
599
-
600
- The `#walk` method can recursively walk a schema definition and extract meta data or field attributes.
601
-
602
- ```ruby
603
- schema_documentation = create_user_schema.walk do |field|
604
- {type: field.meta_data[:type], label: field.meta_data[:label]}
605
- end.output
606
-
607
- # Returns
608
-
609
- {
610
- name: {type: :string, label: "User's full name"},
611
- age: {type: :integer, label: "User's age"},
612
- status: {type: :string, label: nil},
613
- friends: [
614
- {
615
- name: {type: :string, label: "Friend full name"},
616
- email: {type: nil, label: "Friend email"}
617
- }
618
- ]
619
- }
620
- ```
621
-
622
- When passed a _symbol_, it will collect that key from field meta data.
623
-
624
- ```ruby
625
- schema_labels = create_user_schema.walk(:label).output
626
-
627
- # returns
628
-
629
- {
630
- name: "User's full name",
631
- age: "User's age",
632
- status: nil,
633
- friends: [
634
- {name: "Friend full name", email: "Friend email"}
635
- ]
636
- }
637
- ```
638
-
639
- 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.
640
-
641
- ## Form objects DSL
642
-
643
- You can use schemas and fields on their own, or include the `DSL` module in your own classes to define form objects.
644
-
645
- ```ruby
646
- require "parametric/dsl"
647
-
648
- class CreateUserForm
649
- include Paradocs::DSL
650
-
651
- schema(:test) do
652
- field(:name).type(:string).required
653
- field(:email).policy(:email).required
654
- field(:age).type(:integer)
655
- subschema_by(:age) { |age| age > 18 ? :allow : :deny }
656
- end
657
-
658
- subschema_for(:test, name: :allow) { field(:role).options(["sign_in"]) }
659
- subschema_for(:test, name: :deny) { field(:role).options([]) }
660
-
661
- attr_reader :params, :errors
662
-
663
- def initialize(input_data)
664
- results = self.class.schema.resolve(input_data)
665
- @params = results.output
666
- @errors = results.errors
667
- end
668
-
669
- def run!
670
- if !valid?
671
- raise InvalidFormError.new(errors)
672
- end
673
-
674
- run
675
- end
676
-
677
- def valid?
678
- !errors.any?
679
- end
680
-
681
- private
682
-
683
- def run
684
- User.create!(params)
685
- end
686
- end
687
- ```
688
-
689
- Form schemas can also be defined by passing another form or schema instance. This can be useful when building form classes in runtime.
690
-
691
- ```ruby
692
- UserSchema = Paradocs::Schema.new do
693
- field(:name).type(:string).present
694
- field(:age).type(:integer)
695
- end
696
-
697
- class CreateUserForm
698
- include Paradocs::DSL
699
- # copy from UserSchema
700
- schema UserSchema
701
- end
702
- ```
703
-
704
- ### Form object inheritance
705
-
706
- Sub classes of classes using the DSL will inherit schemas defined on the parent class.
707
-
708
- ```ruby
709
- class UpdateUserForm < CreateUserForm
710
- # All field definitions in the parent are conserved.
711
- # New fields can be defined
712
- # or existing fields overriden
713
- schema do
714
- # make this field optional
715
- field(:name).declared.present
716
- end
717
-
718
- def initialize(user, input_data)
719
- super input_data
720
- @user = user
721
- end
722
-
723
- private
724
- def run
725
- @user.update params
726
- end
727
- end
728
- ```
729
-
730
- ### Schema-wide policies
731
-
732
- Sometimes it's useful to apply the same policy to all fields in a schema.
733
-
734
- For example, fields that are _required_ when creating a record might be optional when updating the same record (ie. _PATCH_ operations in APIs).
735
-
736
- ```ruby
737
- class UpdateUserForm < CreateUserForm
738
- schema.policy(:declared)
739
- end
740
- ```
741
-
742
- This will prefix the `:declared` policy to all fields inherited from the parent class.
743
- This means that only fields whose keys are present in the input will be validated.
744
-
745
- Schemas with default policies can still define or re-define fields.
746
-
747
- ```ruby
748
- class UpdateUserForm < CreateUserForm
749
- schema.policy(:declared) do
750
- # Validation will only run if key exists
751
- field(:age).type(:integer).present
752
- end
753
- end
754
- ```
755
-
756
- ### Ignoring fields defined in the parent class
757
-
758
- Sometimes you'll want a child class to inherit most fields from the parent, but ignoring some.
759
-
760
- ```ruby
761
- class CreateUserForm
762
- include Paradocs::DSL
763
-
764
- schema do
765
- field(:uuid).present
766
- field(:status).required.options(["inactive", "active"])
767
- field(:name)
768
- end
769
- end
770
- ```
771
-
772
- The child class can use `ignore(*fields)` to ignore fields defined in the parent.
773
-
774
- ```ruby
775
- class UpdateUserForm < CreateUserForm
776
- schema.ignore(:uuid, :status) do
777
- # optionally add new fields here
778
- end
779
- end
780
- ```
781
-
782
- ## Schema options
783
-
784
- Another way of modifying inherited schemas is by passing options.
785
-
786
- ```ruby
787
- class CreateUserForm
788
- include Paradocs::DSL
789
-
790
- schema(default_policy: :noop) do |opts|
791
- field(:name).policy(opts[:default_policy]).type(:string).required
792
- field(:email).policy(opts[:default_policy).policy(:email).required
793
- field(:age).type(:integer)
794
- end
795
-
796
- # etc
797
- end
798
- ```
799
-
800
- The `:noop` policy does nothing. The sub-class can pass its own _default_policy_.
801
-
802
- ```ruby
803
- class UpdateUserForm < CreateUserForm
804
- # this will only run validations keys existing in the input
805
- schema(default_policy: :declared)
806
- end
807
- ```
808
-
809
- ## A pattern: changing schema policy on the fly.
810
-
811
- You can use a combination of `#clone` and `#policy` to change schema-wide field policies on the fly.
812
-
813
- For example, you might have a form object that supports creating a new user and defining mandatory fields.
814
-
815
- ```ruby
816
- class CreateUserForm
817
- include Paradocs::DSL
818
-
819
- schema do
820
- field(:name).present
821
- field(:age).present
822
- end
823
-
824
- attr_reader :errors, :params
825
-
826
- def initialize(payload: {})
827
- results = self.class.schema.resolve(payload)
828
- @errors = results.errors
829
- @params = results.output
830
- end
831
-
832
- def run!
833
- User.create(params)
834
- end
835
- end
836
- ```
837
-
838
- Now you might want to use the same form object to _update_ and existing user supporting partial updates.
839
- 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.
840
-
841
- We can do this by producing a clone of the class-level schema and applying any necessary policies on the fly.
842
-
843
- ```ruby
844
- class CreateUserForm
845
- include Paradocs::DSL
846
-
847
- schema do
848
- field(:name).present
849
- field(:age).present
850
- end
851
-
852
- attr_reader :errors, :params
853
-
854
- def initialize(payload: {}, user: nil)
855
- @payload = payload
856
- @user = user
857
-
858
- # pick a policy based on user
859
- policy = user ? :declared : :noop
860
- # clone original schema and apply policy
861
- schema = self.class.schema.clone.policy(policy)
862
-
863
- # resolve params
864
- results = schema.resolve(params)
865
- @errors = results.errors
866
- @params = results.output
867
- end
868
-
869
- def run!
870
- if @user
871
- @user.update_attributes(params)
872
- else
873
- User.create(params)
874
- end
875
- end
876
- end
877
- ```
878
-
879
- ## Multiple schema definitions
880
-
881
- Form objects can optionally define more than one schema by giving them names:
882
-
883
- ```ruby
884
- class UpdateUserForm
885
- include Paradocs::DSL
886
-
887
- # a schema named :query
888
- # for example for query parameters
889
- schema(:query) do
890
- field(:user_id).type(:integer).present
891
- end
892
-
893
- # a schema for PUT body parameters
894
- schema(:payload) do
895
- field(:name).present
896
- field(:age).present
897
- end
898
- end
899
- ```
900
-
901
- Named schemas are inherited and can be extended and given options in the same way as the nameless version.
902
-
903
- Named schemas can be retrieved by name, ie. `UpdateUserForm.schema(:query)`.
904
-
905
- If no name given, `.schema` uses `:schema` as default schema name.
906
-
907
- ## Expanding fields dynamically
908
-
909
- Sometimes you don't know the exact field names but you want to allow arbitrary fields depending on a given pattern.
910
-
911
- ```ruby
912
- # with this payload:
913
- # {
914
- # title: "A title",
915
- # :"custom_attr_Color" => "red",
916
- # :"custom_attr_Material" => "leather"
917
- # }
918
-
919
- schema = Paradocs::Schema.new do
920
- field(:title).type(:string).present
921
- # here we allow any field starting with /^custom_attr/
922
- # this yields a MatchData object to the block
923
- # where you can define a Field and validations on the fly
924
- # https://ruby-doc.org/core-2.2.0/MatchData.html
925
- expand(/^custom_attr_(.+)/) do |match|
926
- field(match[1]).type(:string).present
927
- end
928
- end
929
-
930
- results = schema.resolve({
931
- title: "A title",
932
- :"custom_attr_Color" => "red",
933
- :"custom_attr_Material" => "leather",
934
- :"custom_attr_Weight" => "",
935
- })
936
-
937
- results.ouput[:Color] # => "red"
938
- results.ouput[:Material] # => "leather"
939
- results.errors["$.Weight"] # => ["is required and value must be present"]
940
- ```
941
-
942
- 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.
943
-
944
- ## Structs
945
-
946
- Structs turn schema definitions into objects graphs with attribute readers.
947
-
948
- Add optional `Paradocs::Struct` module to define struct-like objects with schema definitions.
949
-
950
- ```ruby
951
- require 'parametric/struct'
952
-
953
- class User
954
- include Paradocs::Struct
955
-
956
- schema do
957
- field(:name).type(:string).present
958
- field(:friends).type(:array).schema do
959
- field(:name).type(:string).present
960
- field(:age).type(:integer)
961
- end
962
- end
963
- end
964
- ```
965
-
966
- `User` objects can be instantiated with hash data, which will be coerced and validated as per the schema definition.
967
-
968
- ```ruby
969
- user = User.new(
970
- name: 'Joe',
971
- friends: [
972
- {name: 'Jane', age: 40},
973
- {name: 'John', age: 30},
974
- ]
975
- )
976
-
977
- # properties
978
- user.name # => 'Joe'
979
- user.friends.first.name # => 'Jane'
980
- user.friends.last.age # => 30
981
- ```
982
-
983
- ### Errors
984
-
985
- Both the top-level and nested instances contain error information:
986
-
987
- ```ruby
988
- user = User.new(
989
- name: '', # invalid
990
- friends: [
991
- # friend name also invalid
992
- {name: '', age: 40},
993
- ]
994
- )
995
-
996
- user.valid? # false
997
- user.errors['$.name'] # => "is required and must be present"
998
- user.errors['$.friends[0].name'] # => "is required and must be present"
999
-
1000
- # also access error in nested instances directly
1001
- user.friends.first.valid? # false
1002
- user.friends.first.errors['$.name'] # "is required and must be valid"
1003
- ```
1004
-
1005
- ### .new!(hash)
1006
-
1007
- Instantiating structs with `.new!(hash)` will raise a `Paradocs::InvalidStructError` exception if the data is validations fail. It will return the struct instance otherwise.
1008
-
1009
- `Paradocs::InvalidStructError` includes an `#errors` property to inspect the errors raised.
1010
-
1011
- ```ruby
1012
- begin
1013
- user = User.new!(name: '')
1014
- rescue Paradocs::InvalidStructError => e
1015
- e.errors['$.name'] # "is required and must be present"
1016
- end
1017
- ```
1018
-
1019
- ### Nested structs
1020
-
1021
- You can also pass separate struct classes in a nested schema definition.
1022
-
1023
- ```ruby
1024
- class Friend
1025
- include Paradocs::Struct
1026
-
1027
- schema do
1028
- field(:name).type(:string).present
1029
- field(:age).type(:integer)
1030
- end
1031
- end
1032
-
1033
- class User
1034
- include Paradocs::Struct
1035
-
1036
- schema do
1037
- field(:name).type(:string).present
1038
- # here we use the Friend class
1039
- field(:friends).type(:array).schema Friend
1040
- end
1041
- end
1042
- ```
1043
-
1044
- ### Inheritance
1045
-
1046
- Struct subclasses can add to inherited schemas, or override fields defined in the parent.
1047
-
1048
- ```ruby
1049
- class AdminUser < User
1050
- # inherits User schema, and can add stuff to its own schema
1051
- schema do
1052
- field(:permissions).type(:array)
1053
- end
1054
- end
1055
- ```
1056
-
1057
- ### #to_h
1058
-
1059
- `Struct#to_h` returns the ouput hash, with values coerced and any defaults populated.
1060
-
1061
- ```ruby
1062
- class User
1063
- include Paradocs::Struct
1064
- schema do
1065
- field(:name).type(:string)
1066
- field(:age).type(:integer).default(30)
1067
- end
1068
- end
1069
-
1070
- user = User.new(name: "Joe")
1071
- user.to_h # {name: "Joe", age: 30}
1072
- ```
1073
-
1074
- ### Struct equality
1075
-
1076
- `Paradocs::Struct` implements `#==()` to compare two structs Hash representation (same as `struct1.to_h.eql?(struct2.to_h)`.
1077
-
1078
- Users can override `#==()` in their own classes to do whatever they need.
39
+ ## Learn more
40
+ Please read the [documentation](https://paradocs.readthedocs.io/en/latest)