paradocs 1.0.23 → 1.0.24

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