typed_params 1.2.7 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6f336626eb409c9c4fcb45b2a3cb56c01bf5a08c8f82eafb57d21d3721e7ad1
4
- data.tar.gz: 54e0b4b15c6b99a3d8abca95f7e12892245c7c334551aba158b0797588f77904
3
+ metadata.gz: aedcf15896c7a9b3975e1d8fcd5ec05752120f813ff92707fa9c7684fdb77f64
4
+ data.tar.gz: 1593452e1eef49a1ec497f9118ff3f08f3902ed117d9785f4c0f54b3bcf417b8
5
5
  SHA512:
6
- metadata.gz: 57378c5f785cf126d5284c42334663b8a89e322097168d2776d63188481b227d1491168574c557b1e58def51e561dbc181f767073f74b3a510817921f1cca4fc
7
- data.tar.gz: 66e21d13ecf91c3a4b15f09ee7d054ad88bcc0d62fd51218fc460463ac32d9878d701bdd0cd08c511ba919cb6b63d743a8d9a444906ca9961a7eb537b4db83cc
6
+ metadata.gz: fad33f1bac6f33d40487e88fab1bba065223e9986c3cf96fc1dc2654d87b2cbc2b59d82af073ca833a71e5d953eab6793f43f2b8b4bc698e21d21e50c544b96b
7
+ data.tar.gz: 2b0b082f223b1a7b6fd31d1f2a8b4f86364a117cf4efb1893d31a3c395a276ceaf0797481b9958679780c3b1814d62c842b3e696ff605a9b4bd89d4e52ed2649
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0
4
+
5
+ - Add `depth:` validator for asserting maximum depth of `hash` and `array` params.
6
+
7
+ ## 1.3.0
8
+
9
+ - Add `any` type to skip type validation for a given param.
10
+
3
11
  ## 1.2.7
4
12
 
5
13
  - Fix issue where the JSONAPI formatter did not ignore `meta` when `data` is omitted.
data/README.md CHANGED
@@ -68,6 +68,7 @@ Links:
68
68
  - [Non-scalar types](#non-scalar-types)
69
69
  - [Custom types](#custom-types)
70
70
  - [Formats](#formats)
71
+ - [Custom formats](#custom-formats)
71
72
  - [Contributing](#contributing)
72
73
  - [License](#license)
73
74
 
@@ -447,6 +448,7 @@ Parameters can have validations, transforms, and more.
447
448
  - [`:exclusion`](#exclusion-validation)
448
449
  - [`:format`](#format-validation)
449
450
  - [`:length`](#length-validation)
451
+ - [`:depth`](#depth-validation)
450
452
  - [`:transform`](#transform-parameter)
451
453
  - [`:validate`](#validate-parameter)
452
454
  - [`:polymorphic`](#polymorphic-parameter)
@@ -579,15 +581,21 @@ error.
579
581
 
580
582
  #### Allow non-scalars
581
583
 
582
- Only applicable to the `:hash` type and its subtypes. Allow non-scalar values in
583
- a `:hash` parameter. Scalar types can be found under [Types](#scalar-types).
584
+ Only applicable to the `:hash` and `:array` types, and their subtypes. Allow
585
+ non-scalar values in a `:hash` or `:array` parameter.
584
586
 
585
587
  ```ruby
588
+ # Allow unlimited nesting
586
589
  param :metadata, type: :hash, allow_non_scalars: true
590
+
591
+ # Disallow non-scalars
592
+ param :data, type: :hash, allow_non_scalars: false
587
593
  ```
588
594
 
589
595
  By default, non-scalar parameters are rejected with a `TypedParams::InvalidParameterError`
590
- error.
596
+ error. Use [`:depth`](#depth-validation) to control maximum depth.
597
+
598
+ Scalar types can be found under [Types](#scalar-types).
591
599
 
592
600
  #### Nilify blanks
593
601
 
@@ -638,6 +646,20 @@ param :odd, type: :string, length: { in: [2, 4, 6, 8] }
638
646
  param :ten, type: :string, length: { is: 10 }
639
647
  ```
640
648
 
649
+ #### Depth validation
650
+
651
+ The parameter must be a certain depth.
652
+
653
+ ```ruby
654
+ # Allow nested objects up to 2 levels deep
655
+ param :config, type: :hash, depth: { maximum: 2 }
656
+
657
+ # Allow nested arrays up to 3 levels deep
658
+ param :data, type: :array, depth: { maximum: 3 }
659
+ ```
660
+
661
+ Only applicable for `:hash` and `:array` types. Automatically sets `allow_non_scalars: true`.
662
+
641
663
  #### Transform parameter
642
664
 
643
665
  Transform the parameter using a lambda. This is commonly used to transform a
@@ -780,6 +802,7 @@ Type `:date`. Defines a date parameter. Must be a `Date`.
780
802
 
781
803
  - [`:array`](#array-type)
782
804
  - [`:hash`](#hash-type)
805
+ - [`:any`](#any-type)
783
806
 
784
807
  #### Array type
785
808
 
@@ -819,6 +842,15 @@ end
819
842
  # non-schema hash
820
843
  param :only_scalars, type: :hash
821
844
  param :non_scalars_too, type: :hash, allow_non_scalars: true
845
+ param :limited_nesting, type: :hash, depth: { maximum: 2 }
846
+ ```
847
+
848
+ #### Any type
849
+
850
+ Type `:any`. Defines a parameter that is not type checked.
851
+
852
+ ```ruby
853
+ param :anything_goes, type: :any
822
854
  ```
823
855
 
824
856
  ### Custom types
@@ -944,6 +976,45 @@ class UsersController < ApplicationController
944
976
  end
945
977
  ```
946
978
 
979
+ ### Custom formats
980
+
981
+ You may register custom formatters that can be utilized in your schemas.
982
+
983
+ Each formatter consists of, at minimum, a `transform:` lambda, accepting a
984
+ params hash as well as optional `controller:` and `schema:` keywords, and
985
+ returning the formatted params.
986
+
987
+ For more usage examples, see [the default formatters](https://github.com/keygen-sh/typed_params/tree/master/lib/typed_params/formatters).
988
+
989
+ ```rb
990
+ TypedParams::Formatters.register(:strong_params,
991
+ transform: -> (params, controller:) {
992
+ wrapper = controller.controller_name.singularize.to_sym
993
+ unwrapped = params[wrapper]
994
+
995
+ ActionController::Parameters.new(unwrapped).permit!
996
+ },
997
+ )
998
+ ```
999
+
1000
+ ```rb
1001
+ typed_params {
1002
+ format :strong_params
1003
+
1004
+ param :user, type: :hash do
1005
+ param :email, type: :string, format: { with: /@/ }
1006
+ param :password, type: :string
1007
+ end
1008
+ }
1009
+ def create
1010
+ puts user_params
1011
+ # => #<ActionController::Parameters
1012
+ # {"email"=>"json@smith.example","password"=>"7c84241a1102"}
1013
+ # permitted: true
1014
+ # >
1015
+ end
1016
+ ```
1017
+
947
1018
  ## Is it any good?
948
1019
 
949
1020
  [Yes.](https://news.ycombinator.com/item?id=3067434)
@@ -12,21 +12,29 @@ module TypedParams
12
12
  def to_json_pointer = '/' + keys.map { transform_key(_1) }.join('/')
13
13
  def to_dot_notation = keys.map { transform_key(_1) }.join('.')
14
14
 
15
+ def +(other)
16
+ raise ArgumentError, 'must be a Path object or nil' unless other in Path | nil
17
+
18
+ return self if
19
+ other.nil?
20
+
21
+ Path.new(*keys, *other.keys, casing:)
22
+ end
23
+
15
24
  def to_s
16
25
  keys.map { transform_key(_1) }.reduce(+'') do |s, key|
17
- next s << key if s.blank?
18
-
19
26
  case key
20
27
  when Integer
21
28
  s << "[#{key}]"
22
29
  else
23
- s << ".#{key}"
30
+ s << '.' unless s.blank?
31
+ s << key.to_s
24
32
  end
25
33
  end
26
34
  end
27
35
 
28
36
  def inspect
29
- "#<#{self.class.name}: #{to_s.inspect}>"
37
+ "#<#{self.class.name}: #{to_s.inspect} keys=#{keys.inspect} casing=#{casing.inspect}>"
30
38
  end
31
39
 
32
40
  private
@@ -34,6 +34,7 @@ module TypedParams
34
34
  exclusion: nil,
35
35
  format: nil,
36
36
  length: nil,
37
+ depth: nil,
37
38
  transform: nil,
38
39
  validate: nil,
39
40
  if: nil,
@@ -76,6 +77,9 @@ module TypedParams
76
77
  length.key?(:is)
77
78
  )
78
79
 
80
+ raise ArgumentError, 'depth must be a hash with :maximum key' unless
81
+ depth.nil? || depth.is_a?(Hash) && depth.key?(:maximum)
82
+
79
83
  @controller = controller
80
84
  @source = source
81
85
  @type = Types[type]
@@ -88,14 +92,15 @@ module TypedParams
88
92
  @coerce = coerce && @type.coercable?
89
93
  @polymorphic = polymorphic
90
94
  @allow_blank = key == ROOT || allow_blank
95
+ @allow_non_scalars = allow_non_scalars || depth.present?
91
96
  @allow_nil = allow_nil
92
- @allow_non_scalars = allow_non_scalars
93
97
  @nilify_blanks = nilify_blanks
94
98
  @noop = noop
95
99
  @inclusion = inclusion
96
100
  @exclusion = exclusion
97
101
  @format = format
98
102
  @length = length
103
+ @depth = depth
99
104
  @casing = casing
100
105
  @transform = transform
101
106
  @children = nil
@@ -119,6 +124,9 @@ module TypedParams
119
124
  @validations << Validations::Length.new(length) if
120
125
  length.present?
121
126
 
127
+ @validations << Validations::Depth.new(depth) if
128
+ depth.present?
129
+
122
130
  @validations << Validations::Validation.wrap(validate) if
123
131
  validate.present?
124
132
 
@@ -261,6 +269,7 @@ module TypedParams
261
269
  end
262
270
  end
263
271
 
272
+
264
273
  def root? = key == ROOT
265
274
  def child? = !root?
266
275
  def children? = !children.blank?
@@ -281,7 +290,8 @@ module TypedParams
281
290
  def unless? = !@unless.nil?
282
291
  def array? = Types.array?(type)
283
292
  def hash? = Types.hash?(type)
284
- def scalar? = Types.scalar?(type)
293
+ def scalar? = type.scalar?
294
+ def any? = type.any?
285
295
  def formatter? = !!@formatter
286
296
 
287
297
  def inspect
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedParams
4
+ module Types
5
+ register(:any,
6
+ abstract: true,
7
+ match: -> _ {},
8
+ )
9
+ end
10
+ end
@@ -41,6 +41,7 @@ module TypedParams
41
41
  def accepts_block? = !!@accepts_block
42
42
  def coercable? = !!@coerce
43
43
  def scalar? = !!@scalar
44
+ def any? = @type == :any
44
45
  def abstract? = !!@abstract
45
46
  def subtype? = !!@archetype
46
47
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typed_params/validations/validation'
4
+ require 'typed_params/path'
5
+
6
+ module TypedParams
7
+ module Validations
8
+ class Depth < Validation
9
+ def call(value)
10
+ case options
11
+ in maximum: Integer => maximum_depth
12
+ return if
13
+ maximum_depth.nil? || maximum_depth == Float::INFINITY
14
+
15
+ validate_depth!(value, maximum_depth:)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def validate_depth!(current_value, maximum_depth:, current_depth: 0, current_path: [])
22
+ case current_value
23
+ in Hash => hash
24
+ hash.each do |key, value|
25
+ path = current_path + [key]
26
+ next if
27
+ Types.scalar?(value)
28
+
29
+ raise ValidationError.new("maximum depth of #{maximum_depth} exceeded", path: Path.new(*path)) if
30
+ current_depth >= maximum_depth
31
+
32
+ validate_depth!(value, maximum_depth:, current_depth: current_depth + 1, current_path: path)
33
+ end
34
+ in Array => arr
35
+ arr.each_with_index do |value, index|
36
+ path = current_path + [index]
37
+ next if
38
+ Types.scalar?(value)
39
+
40
+ raise ValidationError.new("maximum depth of #{maximum_depth} exceeded", path: Path.new(*path)) if
41
+ current_depth >= maximum_depth
42
+
43
+ validate_depth!(value, maximum_depth:, current_depth: current_depth + 1, current_path: path)
44
+ end
45
+ else
46
+ # TODO(ezekg) add support for custom types e.g. enumerables?
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -28,7 +28,7 @@ module TypedParams
28
28
 
29
29
  # Assert type
30
30
  raise InvalidParameterError.new("type mismatch (received #{type.humanize} expected #{schema.type.humanize})", path: param.path, source: schema.source) unless
31
- type == schema.type || type.subtype? && type.archetype == schema.type
31
+ schema.any? || type == schema.type || type.subtype? && type.archetype == schema.type
32
32
 
33
33
  # Assertions for params without children
34
34
  if schema.children.nil?
@@ -67,7 +67,7 @@ module TypedParams
67
67
  schema.validations.each do |validation|
68
68
  validation.call(param.value)
69
69
  rescue ValidationError => e
70
- raise InvalidParameterError.new(e.message, path: param.path, source: schema.source)
70
+ raise InvalidParameterError.new(e.message, path: param.path + e.path, source: schema.source)
71
71
  end
72
72
  end
73
73
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypedParams
4
- VERSION = '1.2.7'
4
+ VERSION = '1.4.0'
5
5
  end
data/lib/typed_params.rb CHANGED
@@ -26,6 +26,7 @@ require 'typed_params/transforms/noop'
26
26
  require 'typed_params/transforms/transform'
27
27
  require 'typed_params/transformer'
28
28
  require 'typed_params/types'
29
+ require 'typed_params/types/any'
29
30
  require 'typed_params/types/array'
30
31
  require 'typed_params/types/boolean'
31
32
  require 'typed_params/types/date'
@@ -39,6 +40,7 @@ require 'typed_params/types/string'
39
40
  require 'typed_params/types/symbol'
40
41
  require 'typed_params/types/time'
41
42
  require 'typed_params/types/type'
43
+ require 'typed_params/validations/depth'
42
44
  require 'typed_params/validations/exclusion'
43
45
  require 'typed_params/validations/format'
44
46
  require 'typed_params/validations/inclusion'
@@ -58,9 +60,22 @@ module TypedParams
58
60
 
59
61
  class UndefinedActionError < StandardError; end
60
62
  class InvalidMethodError < StandardError; end
61
- class ValidationError < StandardError; end
62
63
  class CoercionError < StandardError; end
63
64
 
65
+ class ValidationError < StandardError
66
+ attr_reader :path
67
+
68
+ def initialize(message, path: nil)
69
+ @path = path
70
+
71
+ super(message)
72
+ end
73
+
74
+ def inspect
75
+ "#<#{self.class.name} message=#{message.inspect} path=#{path.inspect}>"
76
+ end
77
+ end
78
+
64
79
  class InvalidParameterError < StandardError
65
80
  attr_reader :source,
66
81
  :path
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_params
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.7
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zeke Gabrielse
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-28 00:00:00.000000000 Z
11
+ date: 2025-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -79,6 +79,7 @@ files:
79
79
  - lib/typed_params/transforms/noop.rb
80
80
  - lib/typed_params/transforms/transform.rb
81
81
  - lib/typed_params/types.rb
82
+ - lib/typed_params/types/any.rb
82
83
  - lib/typed_params/types/array.rb
83
84
  - lib/typed_params/types/boolean.rb
84
85
  - lib/typed_params/types/date.rb
@@ -92,6 +93,7 @@ files:
92
93
  - lib/typed_params/types/symbol.rb
93
94
  - lib/typed_params/types/time.rb
94
95
  - lib/typed_params/types/type.rb
96
+ - lib/typed_params/validations/depth.rb
95
97
  - lib/typed_params/validations/exclusion.rb
96
98
  - lib/typed_params/validations/format.rb
97
99
  - lib/typed_params/validations/inclusion.rb