typed_params 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/CONTRIBUTING.md +33 -0
  4. data/LICENSE +20 -0
  5. data/README.md +736 -0
  6. data/SECURITY.md +8 -0
  7. data/lib/typed_params/bouncer.rb +34 -0
  8. data/lib/typed_params/coercer.rb +21 -0
  9. data/lib/typed_params/configuration.rb +40 -0
  10. data/lib/typed_params/controller.rb +192 -0
  11. data/lib/typed_params/formatters/formatter.rb +20 -0
  12. data/lib/typed_params/formatters/jsonapi.rb +142 -0
  13. data/lib/typed_params/formatters/rails.rb +31 -0
  14. data/lib/typed_params/formatters.rb +20 -0
  15. data/lib/typed_params/handler.rb +24 -0
  16. data/lib/typed_params/handler_set.rb +19 -0
  17. data/lib/typed_params/mapper.rb +74 -0
  18. data/lib/typed_params/namespaced_set.rb +59 -0
  19. data/lib/typed_params/parameter.rb +100 -0
  20. data/lib/typed_params/parameterizer.rb +87 -0
  21. data/lib/typed_params/path.rb +57 -0
  22. data/lib/typed_params/pipeline.rb +13 -0
  23. data/lib/typed_params/processor.rb +27 -0
  24. data/lib/typed_params/schema.rb +290 -0
  25. data/lib/typed_params/schema_set.rb +7 -0
  26. data/lib/typed_params/transformer.rb +49 -0
  27. data/lib/typed_params/transforms/key_alias.rb +16 -0
  28. data/lib/typed_params/transforms/key_casing.rb +59 -0
  29. data/lib/typed_params/transforms/nilify_blanks.rb +16 -0
  30. data/lib/typed_params/transforms/noop.rb +11 -0
  31. data/lib/typed_params/transforms/transform.rb +11 -0
  32. data/lib/typed_params/types/array.rb +12 -0
  33. data/lib/typed_params/types/boolean.rb +33 -0
  34. data/lib/typed_params/types/date.rb +10 -0
  35. data/lib/typed_params/types/decimal.rb +10 -0
  36. data/lib/typed_params/types/float.rb +10 -0
  37. data/lib/typed_params/types/hash.rb +13 -0
  38. data/lib/typed_params/types/integer.rb +10 -0
  39. data/lib/typed_params/types/nil.rb +11 -0
  40. data/lib/typed_params/types/number.rb +10 -0
  41. data/lib/typed_params/types/string.rb +10 -0
  42. data/lib/typed_params/types/symbol.rb +10 -0
  43. data/lib/typed_params/types/time.rb +20 -0
  44. data/lib/typed_params/types/type.rb +78 -0
  45. data/lib/typed_params/types.rb +69 -0
  46. data/lib/typed_params/validations/exclusion.rb +17 -0
  47. data/lib/typed_params/validations/format.rb +19 -0
  48. data/lib/typed_params/validations/inclusion.rb +17 -0
  49. data/lib/typed_params/validations/length.rb +29 -0
  50. data/lib/typed_params/validations/validation.rb +18 -0
  51. data/lib/typed_params/validator.rb +75 -0
  52. data/lib/typed_params/version.rb +5 -0
  53. data/lib/typed_params.rb +89 -0
  54. metadata +124 -0
data/README.md ADDED
@@ -0,0 +1,736 @@
1
+ # typed_params
2
+
3
+ [![CI](https://github.com/keygen-sh/typed_params/actions/workflows/test.yml/badge.svg)](https://github.com/keygen-sh/typed_params/actions)
4
+ [![Gem Version](https://badge.fury.io/rb/typed_params.svg)](https://badge.fury.io/rb/typed_params)
5
+
6
+ `typed_params` is an alternative to Rails strong parameters for controller params,
7
+ offering an intuitive DSL for defining structured and strongly-typed controller
8
+ parameter schemas for Rails APIs.
9
+
10
+ This gem was extracted from [Keygen](https://keygen.sh) and is being used in production
11
+ to serve millions of API requests per day.
12
+
13
+ Sponsored by:
14
+
15
+ [![Keygen logo](https://github.com/keygen-sh/typed_params/assets/6979737/f2947915-2956-4415-a9c0-5411c388ea96)](https://keygen.sh)
16
+
17
+ _An open, source-available software licensing and distribution API._
18
+
19
+ Links:
20
+
21
+ - [Installing `typed_params`](#installation)
22
+ - [Supported Ruby versions](#supported-rubies)
23
+ - [RubyDoc](#documentation)
24
+ - [Usage](#usage)
25
+ - [Parameter schemas](#parameter-schemas)
26
+ - [Query schemas](#query-schemas)
27
+ - [Defining schemas](#defining-schemas)
28
+ - [Shared schemas](#shared-schemas)
29
+ - [Configuration](#configuration)
30
+ - [Unpermitted parameters](#unpermitted-parameters)
31
+ - [Invalid parameters](#invalid-parameters)
32
+ - [Parameter options](#parameter-options)
33
+ - [Shared options](#shared-options)
34
+ - [Scalar types](#scalar-types)
35
+ - [Non-scalar types](#non-scalar-types)
36
+ - [Custom types](#custom-types)
37
+ - [Contributing](#contributing)
38
+ - [License](#license)
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's `Gemfile`:
43
+
44
+ ```ruby
45
+ gem 'typed_params'
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ ```bash
51
+ $ bundle
52
+ ```
53
+
54
+ Or install it yourself as:
55
+
56
+ ```bash
57
+ $ gem install typed_params
58
+ ```
59
+
60
+ ## Supported Rubies
61
+
62
+ **`typed_params` supports Ruby 3.1 and above.** We encourage you to upgrade if you're
63
+ on an older version. Ruby 3 provides a lot of great features, like pattern matching and
64
+ a new shorthand hash syntax.
65
+
66
+ ## Documentation
67
+
68
+ You can find the documentation on [RubyDoc](https://rubydoc.info/github/keygen-sh/typed_params).
69
+
70
+ _We're working on improving the docs._
71
+
72
+ ## Features
73
+
74
+ - An intuitive DSL — a breath of fresh air coming from strong parameters.
75
+ - Define structured, strongly-typed parameter schemas for controllers.
76
+ - Reuse schemas across controllers by defining named schemas.
77
+ - Run validations on params, similar to active model validations.
78
+ - Run transforms on params before they hit your controller.
79
+
80
+ ## Usage
81
+
82
+ `typed_params` can be used to define a parameter schema per-action
83
+ on controllers.
84
+
85
+ To start, include the controller module:
86
+
87
+ ```ruby
88
+ class ApplicationController < ActionController::API
89
+ include TypedParams::Controller
90
+ end
91
+ ```
92
+
93
+ ### Parameter schemas
94
+
95
+ To define a parameter schema, you can use the `.typed_params` method.
96
+ These parameters will be pulled from the request body. It accepts a
97
+ block containing the schema definition, as well as [options](#parameter-options).
98
+
99
+ The parameters will be available inside of the controller action with
100
+ the following methods:
101
+
102
+ - `#{controller_name.singularize}_params`
103
+ - `typed_params`
104
+
105
+ ```ruby
106
+ class UsersController < ApplicationController
107
+ typed_params {
108
+ param :user, type: :hash do
109
+ param :first_name, type: :string, optional: true
110
+ param :last_name, type: :string, optional: true
111
+ param :email, type: :string
112
+ param :password, type: :string
113
+ param :roles, type: :array, if: :admin? do
114
+ items type: :string
115
+ end
116
+ end
117
+ }
118
+ def create
119
+ user = User.new(user_params)
120
+
121
+ if user.save
122
+ render_created user, location: v1_user_url(user)
123
+ else
124
+ render_unprocessable_resource user
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ ### Query schemas
131
+
132
+ To define a query schema, you can use the `.typed_query` method. These
133
+ parameters will be pulled from the request query parameters. It
134
+ accepts a block containing the schema definition.
135
+
136
+ The parameters will be available inside of the controller action with
137
+ the following methods:
138
+
139
+ - `#{controller_name.singularize}_query`
140
+ - `#typed_query`
141
+
142
+ ```ruby
143
+ class PostsController < ApplicationController
144
+ typed_query {
145
+ param :limit, type: :integer, coerce: true, allow_nil: true, optional: true
146
+ param :page, type: :integer, coerce: true, allow_nil: true, optional: true
147
+ }
148
+ def index
149
+ posts = Post.paginate(
150
+ post_query.fetch(:limit, 10),
151
+ post_query.fetch(:page, 1),
152
+ )
153
+
154
+ render_ok posts
155
+ end
156
+ end
157
+ ```
158
+
159
+ ### Defining schemas
160
+
161
+ The easiest way to define a schema is by decorating a specific controller action,
162
+ which we exemplified above. You can use `.typed_params` or `.typed_query` to
163
+ decorate a controller action.
164
+
165
+ ```ruby
166
+ class PostsController < ApplicationController
167
+ typed_params {
168
+ param :author_id, type: :integer
169
+ param :title, type: :string, length: { within: 10..80 }
170
+ param :content, type: :string, length: { minimum: 100 }
171
+ param :published_at, type: :time, optional: true, allow_nil: true
172
+ param :tag_ids, type: :array, optional: true, length: { maximum: 10 } do
173
+ items type: :integer
174
+ end
175
+ }
176
+ def create
177
+ # ...
178
+ end
179
+ end
180
+ ```
181
+
182
+ As an alternative to decorated schemas, you can define schemas after an action
183
+ has been defined.
184
+
185
+ ```ruby
186
+ class PostsController < ApplicationController
187
+ def create
188
+ # ...
189
+ end
190
+
191
+ typed_params on: :create do
192
+ param :author_id, type: :integer
193
+ param :title, type: :string, length: { within: 10..80 }
194
+ param :content, type: :string, length: { minimum: 100 }
195
+ param :published_at, type: :time, optional: true, allow_nil: true
196
+ param :tag_ids, type: :array, optional: true, length: { maximum: 10 } do
197
+ items type: :integer
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
203
+ By default, all root schemas are a [`:hash`](#hash-type) schema. This is because both
204
+ `request.request_parameters` and `request.query_parameters` are hashes. Eventually,
205
+ we'd like [to make that configurable](https://github.com/keygen-sh/typed_params/blob/67e9a34ce62c9cddbd2bd313e4e9f096f8744b83/lib/typed_params/controller.rb#L24-L27),
206
+ so that you could use a top-level array schema. You can create nested schemas via
207
+ the [`:hash`](#hash-type) and [`:array`](#array-type) types.
208
+
209
+ ### Shared schemas
210
+
211
+ If you need to share a specific schema between multiple actions, you can define
212
+ a named schema.
213
+
214
+ ```ruby
215
+ class PostsController < ApplicationController
216
+ typed_schema :post do
217
+ param :author_id, type: :integer
218
+ param :title, type: :string, length: { within: 10..80 }
219
+ param :content, type: :string, length: { minimum: 100 }
220
+ param :published_at, type: :time, optional: true, allow_nil: true
221
+ param :tag_ids, type: :array, optional: true, length: { maximum: 10 } do
222
+ items type: :integer
223
+ end
224
+ end
225
+
226
+ typed_params schema: :post
227
+ def create
228
+ # ...
229
+ end
230
+
231
+ typed_params schema: :post
232
+ def update
233
+ # ...
234
+ end
235
+ end
236
+ ```
237
+
238
+ ### Configuration
239
+
240
+ ```ruby
241
+ TypedParams.configure do |config|
242
+ # Ignore nil params that are marked optional and non-nil in the schema.
243
+ #
244
+ # For example, given the following schema:
245
+ #
246
+ # typed_params {
247
+ # param :optional_key, type: :string, optional: true
248
+ # param :required_key, type: :string
249
+ # }
250
+ #
251
+ # And the following curl request:
252
+ #
253
+ # curl -X POST http://localhost:3000 -d '{"optional_key":null,"required_key":"value"}'
254
+ #
255
+ # Within the controller, the params would be:
256
+ #
257
+ # puts typed_params # => { required_key: 'value' }
258
+ #
259
+ config.ignore_nil_optionals = true
260
+
261
+ # Key transformation applied to the parameters after validation.
262
+ #
263
+ # One of:
264
+ #
265
+ # - :underscore
266
+ # - :camel
267
+ # - :lower_camel
268
+ # - :dash
269
+ # - nil
270
+ #
271
+ # For example, given the following schema:
272
+ #
273
+ # typed_params {
274
+ # param :someKey, type: :string
275
+ # }
276
+ #
277
+ # And the following curl request:
278
+ #
279
+ # curl -X POST http://localhost:3000 -d '{"someKey":"value"}'
280
+ #
281
+ # Within the controller, the params would be:
282
+ #
283
+ # puts typed_params # => { some_key: 'value' }
284
+ #
285
+ config.key_transform = :underscore
286
+
287
+ # Path transformation applied to error paths e.g. UnpermittedParameterError.
288
+ #
289
+ # One of:
290
+ #
291
+ # - :underscore
292
+ # - :camel
293
+ # - :lower_camel
294
+ # - :dash
295
+ # - nil
296
+ #
297
+ # For example, given the following schema:
298
+ #
299
+ # typed_params {
300
+ # param :parent_key, type: :hash do
301
+ # param :child_key, type: :string
302
+ # end
303
+ # }
304
+ #
305
+ # With an invalid `child_key`, the path would be:
306
+ #
307
+ # rescue_from TypedParams::UnpermittedParameterError, err -> {
308
+ # puts err.path.to_s # => parentKey.childKey
309
+ # }
310
+ #
311
+ config.path_transform = :lower_camel
312
+ end
313
+ ```
314
+
315
+ ### Unpermitted parameters
316
+
317
+ By default, `.typed_params` is [`:strict`](#strict-parameter). This means that if any unpermitted parameters
318
+ are provided, a `TypedParams::UnpermittedParameterError` will be raised.
319
+
320
+ For `.typed_query`, the default is non-strict. This means that any unpermitted parameters
321
+ will be ignored.
322
+
323
+ You can rescue this error at the application-level like so:
324
+
325
+ ```ruby
326
+ class ApplicationController < ActionController::API
327
+ rescue_from TypedParams::UnpermittedParameterError, err -> {
328
+ render_bad_request "unpermitted parameter: #{err.path.to_jsonapi_pointer}"
329
+ }
330
+ end
331
+ ```
332
+
333
+ The `TypedParams::UnpermittedParameterError` error object has the following attributes:
334
+
335
+ - `#message` - the error message, e.g. `unpermitted parameter`.
336
+ - `#path` - a `Path` object with a pointer to the unpermitted parameter.
337
+ - `#source` - either `:params` or `:query`, depending on where the unpermitted parameter came from (i.e. request body vs URL, respectively).
338
+
339
+ ### Invalid parameters
340
+
341
+ When a parameter is provided, but it fails validation (e.g. a type mismatch), a
342
+ `TypedParams::InvalidParameterError` error will be raised.
343
+
344
+ You can rescue this error at the application-level like so:
345
+
346
+ ```ruby
347
+ class ApplicationController < ActionController::API
348
+ rescue_from TypedParams::InvalidParameterError, err -> {
349
+ render_bad_request "invalid parameter: #{err.message}", parameter: err.path.to_dot_notation
350
+ }
351
+ end
352
+ ```
353
+
354
+ The `TypedParams::InvalidParameterError` error object has the following attributes:
355
+
356
+ - `#message` - the error message, e.g. `type mismatch (received string expected integer)`.
357
+ - `#path` - a `Path` object with a pointer to the invalid parameter.
358
+ - `#source` - either `:params` or `:query`, depending on where the invalid parameter came from (i.e. request body vs URL, respectively).
359
+
360
+ ### Parameter options
361
+
362
+ Parameters can have validations, transforms, and more.
363
+
364
+ - [`:key`](#parameter-key)
365
+ - [`:type`](#parameter-type)
366
+ - [`:strict`](#strict-parameter)
367
+ - [`:optional`](#optional-parameter)
368
+ - [`:if` and `:unless`](#conditional-parameter)
369
+ - [`:as`](#alias-parameter)
370
+ - [`:noop`](#noop-parameter)
371
+ - [`:coerce`](#coerced-parameter)
372
+ - [`:allow_blank`](#allow-blank)
373
+ - [`:allow_nil`](#allow-nil)
374
+ - [`:allow_non_scalars`](#allow-non-scalars)
375
+ - [`:nilify_blanks`](#nilify-blanks)
376
+ - [`:inclusion`](#inclusion-validation)
377
+ - [`:exclusion`](#exclusion-validation)
378
+ - [`:format`](#format-validation)
379
+ - [`:length`](#length-validation)
380
+ - [`:transform`](#transform-parameter)
381
+ - [`:validate`](#validate-parameter)
382
+
383
+ #### Parameter key
384
+
385
+ The parameter's key.
386
+
387
+ ```ruby
388
+ param :foo
389
+ ```
390
+
391
+ This is required.
392
+
393
+ #### Parameter type
394
+
395
+ The parameter's type. Please see [Types](#scalar-types) for more information. Some
396
+ types may accept a block, e.g. `:hash` and `:array`.
397
+
398
+ ```ruby
399
+ param :email, type: :string
400
+ ```
401
+
402
+ This is required.
403
+
404
+ #### Strict parameter
405
+
406
+ When `true`, a `TypedParams::UnpermittedParameterError` error is raised for
407
+ unpermitted parameters. When `false`, unpermitted parameters are ignored.
408
+
409
+ ```ruby
410
+ param :user, type: :hash, strict: true do
411
+ # ...
412
+ end
413
+ ```
414
+
415
+ By default, the entire `.typed_params` schema is strict, and `.typed_query` is not.
416
+
417
+ #### Optional parameter
418
+
419
+ The parameter is optional. An invalid parameter error will not be raised in its absence.
420
+
421
+ ```ruby
422
+ param :first_name, type: :string, optional: true
423
+ ```
424
+
425
+ By default, parameters are required.
426
+
427
+ #### Conditional parameter
428
+
429
+ You can define conditional parameters using `:if` and `:unless`. The parameter will
430
+ only be evaluated when the condition to `true`.
431
+
432
+ ```ruby
433
+ param :role, type: :string, if: -> { admin? }
434
+ param :role, type: :string, if: :admin?
435
+ param :role, type: :string, unless: -> { guest? }
436
+ param :role, type: :string, unless: :guest?
437
+ ```
438
+
439
+ The lambda will be evaled within the current controller context.
440
+
441
+ #### Alias parameter
442
+
443
+ Apply a transformation that renames the parameter.
444
+
445
+ ```ruby
446
+ param :user, type: :integer, as: :user_id
447
+
448
+ typed_params # => { user_id: '...' }
449
+ ```
450
+
451
+ In this example, the parameter would be accepted as `:user`, but renamed
452
+ to `:user_id` for use inside of the controller.
453
+
454
+ #### Noop parameter
455
+
456
+ The parameter is accepted but immediately thrown out.
457
+
458
+ ```ruby
459
+ param :foo, type: :string, noop: true
460
+ ```
461
+
462
+ By default, this is `false`.
463
+
464
+ #### Coerced parameter
465
+
466
+ The parameter will be coerced if its type is coercible and the parameter has a
467
+ type mismatch. The coercion can fail, e.g. `:integer` to `:hash`, and if it does,
468
+ a `TypedParams::InvalidParameterError` will be raised.
469
+
470
+ ```ruby
471
+ param :age, type: :integer, coerce: true
472
+ ```
473
+
474
+ The default is `false`.
475
+
476
+ #### Allow blank
477
+
478
+ The parameter can be `#blank?`.
479
+
480
+ ```ruby
481
+ param :title, type: :string, allow_blank: true
482
+ ```
483
+
484
+ By default, blank params are rejected with a `TypedParams::InvalidParameterError`
485
+ error.
486
+
487
+ #### Allow nil
488
+
489
+ The parameter can be `#nil?`.
490
+
491
+ ```ruby
492
+ param :tag, type: :string, allow_nil: true
493
+ ```
494
+
495
+ By default, nil params are rejected with a `TypedParams::InvalidParameterError`
496
+ error.
497
+
498
+ #### Allow non-scalars
499
+
500
+ Only applicable to the `:hash` type and its subtypes. Allow non-scalar values in
501
+ a `:hash` parameter. Scalar types can be found under [Types](#scalar-types).
502
+
503
+ ```ruby
504
+ param :metadata, type: :hash, allow_non_scalars: true
505
+ ```
506
+
507
+ By default, non-scalar parameters are rejected with a `TypedParams::InvalidParameterError`
508
+ error.
509
+
510
+ #### Nilify blanks
511
+
512
+ Automatically convert `#blank?` values to `nil`.
513
+
514
+ ```ruby
515
+ param :phone_number, type: :string, nilify_blanks: true
516
+ ```
517
+
518
+ By default, this is disabled.
519
+
520
+ #### Inclusion validation
521
+
522
+ The parameter must be included in the array or range.
523
+
524
+ ```ruby
525
+ param :log_level, type: :string, inclusion: { in: %w[DEBUG INFO WARN ERROR FATAL] }
526
+ param :priority, type: :integer, inclusion: { in: 0..9 }
527
+ ```
528
+
529
+ #### Exclusion validation
530
+
531
+ The parameter must be excluded from the array or range.
532
+
533
+ ```ruby
534
+ param :custom_log_level, type: :string, exclusion: { in: %w[DEBUG INFO WARN ERROR FATAL] }
535
+ param :custom_priority, type: :integer, exclusion: { in: 0..9 }
536
+ ```
537
+
538
+ #### Format validation
539
+
540
+ The parameter must be a certain regular expression format.
541
+
542
+ ```ruby
543
+ param :first_name, type: :string, format: { with: /foo/ }
544
+ param :last_name, type: :string, format: { without: /bar/ }
545
+ ```
546
+
547
+ #### Length validation
548
+
549
+ The parameter must be a certain length.
550
+
551
+ ```ruby
552
+ param :content, type: :string, length: { minimum: 100 }
553
+ param :title, type: :string, length: { maximum: 10 }
554
+ param :tweet, type: :string, length: { within: ..160 }
555
+ param :odd, type: :string, length: { in: [2, 4, 6, 8] }
556
+ param :ten, type: :string, length: { is: 10 }
557
+ ```
558
+
559
+ #### Transform parameter
560
+
561
+ Transform the parameter using a lambda. This is commonly used to transform a
562
+ parameter into a nested attributes hash or array.
563
+
564
+ ```ruby
565
+ param :user, type: :string, transform: -> _key, email {
566
+ [:user_attributes, { email: }]
567
+ }
568
+ ```
569
+
570
+ The lambda must accept a key (the current parameter key), and a value (the
571
+ current parameter value).
572
+
573
+ The lamda must return a tuple with the new key and value.
574
+
575
+ #### Validate parameter
576
+
577
+ Define a custom validation for the parameter, outside of the default
578
+ validations.
579
+
580
+ ```ruby
581
+ param :user, type: :integer, validate: -> id {
582
+ User.exists?(id)
583
+ }
584
+ ```
585
+
586
+ The lambda should accept a value and return a boolean. When the boolean
587
+ evaluates to `false`, a `TypedParams::InvalidParameterError` will
588
+ be raised.
589
+
590
+ ### Shared options
591
+
592
+ You can define a set of options that will be applied to immediate
593
+ children parameters (i.e. not grandchilden).
594
+
595
+ ```ruby
596
+ with if: :admin? do
597
+ param :referrer, type: :string, optional: true
598
+ param :role, type: :string
599
+ end
600
+ ```
601
+
602
+ ### Scalar types
603
+
604
+ - [`:string`](#string-type)
605
+ - [`:boolean`](#boolean-type)
606
+ - [`:integer`](#integer-type)
607
+ - [`:float`](#float-type)
608
+ - [`:decimal`](#decimal-type)
609
+ - [`:number`](#number-type)
610
+ - [`:symbol`](#symbol-type)
611
+ - [`:time`](#time-type)
612
+ - [`:date`](#date-type)
613
+
614
+ #### String type
615
+
616
+ Defines a string parameter. Must be a `String`.
617
+
618
+ #### Boolean type
619
+
620
+ Defines a boolean parameter. Must be `TrueClass` or `FalseClass`.
621
+
622
+ #### Integer type
623
+
624
+ Defines an integer parameter. Must be an `Integer`.
625
+
626
+ #### Float type
627
+
628
+ Defines a float parameter. Must be a `Float`.
629
+
630
+ #### Decimal type
631
+
632
+ Defines a decimal parameter. Must be a `BigDecimal`.
633
+
634
+ #### Number type
635
+
636
+ Defines a number parameter. Must be either an `Integer`, a `Float`, or a `BigDecimal`.
637
+
638
+ #### Symbol type
639
+
640
+ Defines a symbol parameter. Must be a `Symbol`.
641
+
642
+ #### Time type
643
+
644
+ Defines a time parameter. Must be a `Time`.
645
+
646
+ #### Date type
647
+
648
+ Defines a time parameter. Must be a `Date`.
649
+
650
+ ### Non-scalar types
651
+
652
+ - [`:array`](#array-type)
653
+ - [`:hash`](#hash-type)
654
+
655
+ #### Array type
656
+
657
+ Defines an array parameter. Must be an `Array`.
658
+
659
+ Arrays are a special type. They can accept a block that defines its item types,
660
+ which may be a nested schema.
661
+
662
+ ```ruby
663
+ # array of hashes
664
+ param :boundless_array, type: :array do
665
+ item type: :hash do
666
+ # ...
667
+ end
668
+ end
669
+ # array of 1 integer and 1 string
670
+ param :bounded_array, type: :array do
671
+ item type: :integer
672
+ item type: :string
673
+ end
674
+ ```
675
+
676
+ #### Hash type
677
+
678
+ Defines a hash parameter. Must be a `Hash`.
679
+
680
+ Hashes are a special type. They can accept a block that defines a nested schema.
681
+
682
+ ```ruby
683
+ # define a nested schema
684
+ param :parent, type: :hash do
685
+ param :child, type: :hash do
686
+ # ...
687
+ end
688
+ end
689
+
690
+ # non-schema hash
691
+ param :only_scalars, type: :hash
692
+ param :non_scalars_too, type: :hash, allow_non_scalars: true
693
+ ```
694
+
695
+ ### Custom types
696
+
697
+ You may register custom types that can be utilized in your schemas.
698
+
699
+ Each type consists of, at minimum, a `match:` lambda. For more usage
700
+ examples, see [the default types](https://github.com/keygen-sh/typed_params/tree/master/lib/typed_params/types).
701
+
702
+ ```ruby
703
+ TypedParams.types.register(:metadata,
704
+ archetype: :hash,
705
+ match: -> value {
706
+ return false unless
707
+ value.is_a?(Hash)
708
+
709
+ # Metadata can have one layer of nested arrays/hashes
710
+ value.values.all? { |v|
711
+ case v
712
+ when Hash
713
+ v.values.none? { _1.is_a?(Array) || _1.is_a?(Hash) }
714
+ when Array
715
+ v.none? { _1.is_a?(Array) || _1.is_a?(Hash) }
716
+ else
717
+ true
718
+ end
719
+ }
720
+ },
721
+ )
722
+ ```
723
+
724
+ ## Is it any good?
725
+
726
+ [Yes.](https://news.ycombinator.com/item?id=3067434)
727
+
728
+ ## Contributing
729
+
730
+ If you have an idea, or have discovered a bug, please open an issue or create a pull request.
731
+
732
+ For security issues, please see [`SECURITY.md`](https://github.com/keygen-sh/typed_params/blob/master/SECURITY.md)
733
+
734
+ ## License
735
+
736
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).