typed_params 1.0.3 → 1.1.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: 440661d066c2dffa2f84361a952e7158ca7d56d16037ca2c937acb30f2f1cad6
4
- data.tar.gz: ad9f0aad3b817b5fa5f99d6d9eaa9e8f73d556c037cd03207bdc10b51fb26e81
3
+ metadata.gz: 532f36036d10c99438c7764f96edee2a81609957e91549be1d39c09eeac3b986
4
+ data.tar.gz: 2780ca74ef0c99371dc87486134e37b25c8de93a4814f297d7d1955fab18ba32
5
5
  SHA512:
6
- metadata.gz: 5919d79cc5c6f4b8bc7c8b1f65520b04e7fb11632f3b20f50f1c99f9346602f2f471058d293da5e14709c6b5f95ae2f1b1ddf04fa3c235b1314ef701333b568d
7
- data.tar.gz: dea4d641834d9ae1184cdcc5653f1d04bdca18650d787d3557991ab03219ebc2234b36e9edaa9bd7733466852538491e3ad42e93029d9a748c7cea5a4b735105
6
+ metadata.gz: 52d65870463fa00390b94bb2b469f3c12dda164518d4a32ed3151c5f09303ceb546ab4fcc76388eb0c42902a19a64ba249eb44119492ab771175c876821e0d7e
7
+ data.tar.gz: cd70318ed475aa00a8cbdafc89656957a93abe17c1108b0d39bca744102a1e91546d483168b8e5a9e8ac1ae9b58bb17870594d8611b8a05d46d46862f744d7ae
data/README.md CHANGED
@@ -14,7 +14,7 @@ to serve millions of API requests per day.
14
14
  class UsersController < ApplicationController
15
15
  include TypedParams::Controller
16
16
 
17
- rescue_from TypedParams::InvalidParameterError, -> err {
17
+ rescue_from TypedParams::InvalidParameterError, with: -> err {
18
18
  render_bad_request err.message, source: err.path.to_s
19
19
  }
20
20
 
@@ -38,10 +38,16 @@ end
38
38
 
39
39
  Sponsored by:
40
40
 
41
- [![Keygen logo](https://github.com/keygen-sh/typed_params/assets/6979737/f2947915-2956-4415-a9c0-5411c388ea96)](https://keygen.sh)
41
+ <a href="https://keygen.sh?ref=typed_params">
42
+ <div>
43
+ <img src="https://keygen.sh/images/logo-pill.png" width="200" alt="Keygen">
44
+ </div>
45
+ </a>
42
46
 
43
47
  _An open, source-available software licensing and distribution API._
44
48
 
49
+ __
50
+
45
51
  Links:
46
52
 
47
53
  - [Installing `typed_params`](#installation)
@@ -52,6 +58,7 @@ Links:
52
58
  - [Query schemas](#query-schemas)
53
59
  - [Defining schemas](#defining-schemas)
54
60
  - [Shared schemas](#shared-schemas)
61
+ - [Namespaced schemas](#namespaced-schemas)
55
62
  - [Configuration](#configuration)
56
63
  - [Invalid parameters](#invalid-parameters)
57
64
  - [Unpermitted parameters](#unpermitted-parameters)
@@ -115,7 +122,7 @@ To start, include the controller module:
115
122
  class ApplicationController < ActionController::API
116
123
  include TypedParams::Controller
117
124
 
118
- rescue_from TypedParams::InvalidParameterError, -> err {
125
+ rescue_from TypedParams::InvalidParameterError, with: -> err {
119
126
  render_bad_request err.message, source: err.path.to_s
120
127
  }
121
128
  end
@@ -266,13 +273,28 @@ class PostsController < ApplicationController
266
273
  end
267
274
  ```
268
275
 
269
- Named schemas can have an optional `:namespace` as well.
276
+ ### Namespaced schemas
277
+
278
+ Schemas can have an optional `:namespace`. This can be especially useful when
279
+ defining and sharing schemas across multiple versions of an API.
270
280
 
271
281
  ```ruby
272
- typed_schema :post, namespace: :v1 do
273
- param :title, type: :string, length: { within: 10..80 }
274
- param :content, type: :string, length: { minimum: 100 }
275
- param :author_id, type: :integer
282
+ class PostsController < ApplicationController
283
+ typed_schema :post, namespace: :v1 do
284
+ param :title, type: :string, length: { within: 10..80 }
285
+ param :content, type: :string, length: { minimum: 100 }
286
+ param :author_id, type: :integer
287
+ end
288
+
289
+ typed_params schema: %i[v1 post]
290
+ def create
291
+ # ...
292
+ end
293
+
294
+ typed_params schema: %i[v1 post]
295
+ def update
296
+ # ...
297
+ end
276
298
  end
277
299
  ```
278
300
 
@@ -345,7 +367,7 @@ TypedParams.configure do |config|
345
367
  #
346
368
  # With an invalid `child_key`, the path would be:
347
369
  #
348
- # rescue_from TypedParams::UnpermittedParameterError, -> err {
370
+ # rescue_from TypedParams::UnpermittedParameterError, with: -> err {
349
371
  # puts err.path.to_s # => parentKey.childKey
350
372
  # }
351
373
  #
@@ -362,7 +384,7 @@ You can rescue this error at the controller-level like so:
362
384
 
363
385
  ```ruby
364
386
  class ApplicationController < ActionController::API
365
- rescue_from TypedParams::InvalidParameterError, -> err {
387
+ rescue_from TypedParams::InvalidParameterError, with: -> err {
366
388
  render_bad_request "invalid parameter: #{err.message}", parameter: err.path.to_dot_notation
367
389
  }
368
390
  end
@@ -388,7 +410,7 @@ You can rescue this error at the controller-level like so:
388
410
  ```ruby
389
411
  class ApplicationController < ActionController::API
390
412
  # NOTE: Should be rescued before TypedParams::InvalidParameterError
391
- rescue_from TypedParams::UnpermittedParameterError, -> err {
413
+ rescue_from TypedParams::UnpermittedParameterError, with: -> err {
392
414
  render_bad_request "unpermitted parameter: #{err.path.to_jsonapi_pointer}"
393
415
  }
394
416
  end
@@ -621,9 +643,18 @@ The lambda must return a tuple with the new key and value.
621
643
  #### Validate parameter
622
644
 
623
645
  Define a custom validation for the parameter, outside of the default
624
- validations.
646
+ validations. The can be useful for defining mutually exclusive params,
647
+ or even validating that an ID exists before proceeding.
625
648
 
626
649
  ```ruby
650
+ # Mutually exclusive params (i.e. either-or, not both)
651
+ param :login, type: :hash, validate: -> v { v.key?(:username) ^ v.key?(:email) } do
652
+ param :username, type: :string, optional: true
653
+ param :email, type: :string, optional: true
654
+ param :password, type: :string
655
+ end
656
+
657
+ # Assert user exists
627
658
  param :user, type: :integer, validate: -> id {
628
659
  User.exists?(id)
629
660
  }
@@ -633,6 +664,15 @@ The lambda should accept a value and return a boolean. When the boolean
633
664
  evaluates to `false`, a `TypedParams::InvalidParameterError` will
634
665
  be raised.
635
666
 
667
+ To customize the error message, the lambda can raise a `TypedParams::ValidationError`
668
+ error:
669
+
670
+ ```ruby
671
+ param :invalid, type: :string, validate: -> v {
672
+ raise TypedParams::ValidationError, 'is always invalid'
673
+ }
674
+ ```
675
+
636
676
  ### Shared options
637
677
 
638
678
  You can define a set of options that will be applied to immediate
@@ -708,7 +748,7 @@ which may be a nested schema.
708
748
  ```ruby
709
749
  # array of hashes
710
750
  param :boundless_array, type: :array do
711
- item type: :hash do
751
+ items type: :hash do
712
752
  # ...
713
753
  end
714
754
  end
@@ -3,15 +3,19 @@
3
3
  require 'typed_params/handler'
4
4
  require 'typed_params/handler_set'
5
5
  require 'typed_params/schema_set'
6
+ require 'typed_params/memoize'
6
7
 
7
8
  module TypedParams
8
9
  module Controller
9
10
  extend ActiveSupport::Concern
10
11
 
11
12
  included do
13
+ include Memoize
14
+
12
15
  cattr_accessor :typed_handlers, default: HandlerSet.new
13
16
  cattr_accessor :typed_schemas, default: SchemaSet.new
14
17
 
18
+ memoize
15
19
  def typed_params(format: AUTO)
16
20
  handler = typed_handlers.params[self.class, action_name.to_sym]
17
21
 
@@ -42,6 +46,7 @@ module TypedParams
42
46
  )
43
47
  end
44
48
 
49
+ memoize
45
50
  def typed_query(format: AUTO)
46
51
  handler = typed_handlers.query[self.class, action_name.to_sym]
47
52
 
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedParams
4
+ module Memoize
5
+ def self.included(klass) = klass.extend ClassMethods
6
+
7
+ module ClassMethods
8
+ cattr_accessor :memoize_queue, default: []
9
+
10
+ def memoize
11
+ if memoize_queue.include?(self)
12
+ memoize_queue.clear
13
+
14
+ raise RuntimeError, 'memoize cannot be called more than once in succession'
15
+ end
16
+
17
+ memoize_queue << self
18
+ end
19
+
20
+ private
21
+
22
+ def singleton_method_added(method_name)
23
+ while klass = memoize_queue.shift
24
+ raise RuntimeError, "memoize cannot be instrumented more than once for class method #{method_name}" if
25
+ klass.respond_to?(:"unmemoized_#{method_name}")
26
+
27
+ method_visibility = case
28
+ when klass.singleton_class.private_method_defined?(method_name)
29
+ :private
30
+ when klass.singleton_class.protected_method_defined?(method_name)
31
+ :protected
32
+ when klass.singleton_class.public_method_defined?(method_name)
33
+ :public
34
+ end
35
+
36
+ klass.singleton_class.send(:alias_method, :"unmemoized_#{method_name}", method_name)
37
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
38
+ def self.#{method_name}(*args, **kwargs, &block)
39
+ key = args.hash ^ kwargs.hash ^ block.hash
40
+
41
+ return @_memoized_#{method_name}_values[key] if
42
+ defined?(@_memoized_#{method_name}_values) && @_memoized_#{method_name}_values.key?(key)
43
+
44
+ value = unmemoized_#{method_name}(*args, **kwargs, &block)
45
+ memo = @_memoized_#{method_name}_values ||= {}
46
+ memo[key] = value
47
+
48
+ value
49
+ end
50
+ RUBY
51
+
52
+ klass.singleton_class.send(method_visibility, method_name)
53
+ end
54
+
55
+ super
56
+ ensure
57
+ memoize_queue.clear
58
+ end
59
+
60
+ def method_added(method_name)
61
+ while klass = memoize_queue.shift
62
+ raise RuntimeError, "memoize cannot be instrumented more than once for instance method #{method_name}" if
63
+ klass.method_defined?(:"unmemoized_#{method_name}")
64
+
65
+ method_visibility = case
66
+ when klass.private_method_defined?(method_name)
67
+ :private
68
+ when klass.protected_method_defined?(method_name)
69
+ :protected
70
+ when klass.public_method_defined?(method_name)
71
+ :public
72
+ end
73
+
74
+ klass.alias_method :"unmemoized_#{method_name}", method_name
75
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
76
+ def #{method_name}(*args, **kwargs, &block)
77
+ key = args.hash ^ kwargs.hash ^ block.hash
78
+
79
+ return @_memoized_#{method_name}_values[key] if
80
+ defined?(@_memoized_#{method_name}_values) && @_memoized_#{method_name}_values.key?(key)
81
+
82
+ value = unmemoized_#{method_name}(*args, **kwargs, &block)
83
+ memo = @_memoized_#{method_name}_values ||= {}
84
+ memo[key] = value
85
+
86
+ value
87
+ end
88
+ RUBY
89
+
90
+ klass.send(method_visibility, method_name)
91
+ end
92
+
93
+ super
94
+ ensure
95
+ memoize_queue.clear
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypedParams
4
- VERSION = '1.0.3'
4
+ VERSION = '1.1.0'
5
5
  end
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.0.3
4
+ version: 1.1.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: 2023-06-08 00:00:00.000000000 Z
11
+ date: 2023-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -63,6 +63,7 @@ files:
63
63
  - lib/typed_params/handler.rb
64
64
  - lib/typed_params/handler_set.rb
65
65
  - lib/typed_params/mapper.rb
66
+ - lib/typed_params/memoize.rb
66
67
  - lib/typed_params/namespaced_set.rb
67
68
  - lib/typed_params/parameter.rb
68
69
  - lib/typed_params/parameterizer.rb