typed_params 1.0.3 → 1.1.0

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
  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