typed_params 0.2.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.
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).