active_typed_params 0.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +534 -0
- data/lib/active_typed_params/builder.rb +357 -0
- data/lib/active_typed_params/compiler.rb +360 -0
- data/lib/active_typed_params/controller.rb +29 -0
- data/lib/active_typed_params/declaration.rb +90 -0
- data/lib/active_typed_params/enum_adapter.rb +28 -0
- data/lib/active_typed_params/errors.rb +78 -0
- data/lib/active_typed_params/inference.rb +264 -0
- data/lib/active_typed_params/inline.rb +94 -0
- data/lib/active_typed_params/node.rb +70 -0
- data/lib/active_typed_params/railtie.rb +19 -0
- data/lib/active_typed_params/runtime.rb +182 -0
- data/lib/active_typed_params/schema.rb +176 -0
- data/lib/active_typed_params/version.rb +5 -0
- data/lib/active_typed_params.rb +148 -0
- metadata +92 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveTypedParams
|
|
4
|
+
class Schema
|
|
5
|
+
OPTIONS = %i[unknown required memoize root source].freeze
|
|
6
|
+
UNKNOWN_MODES = %i[ignore report reject].freeze
|
|
7
|
+
MUTEX = Mutex.new
|
|
8
|
+
|
|
9
|
+
attr_reader :owner, :name, :options, :compiled_module
|
|
10
|
+
|
|
11
|
+
def initialize(name:, owner: nil, options: {}, block: nil, nodes: nil, module_name: nil)
|
|
12
|
+
raise SchemaError, "typed_params #{name.inspect} needs a block" if block.nil? && nodes.nil?
|
|
13
|
+
|
|
14
|
+
unknown = options.keys - OPTIONS
|
|
15
|
+
raise SchemaError, "typed_params #{name.inspect} does not accept #{unknown.inspect}" if unknown.any?
|
|
16
|
+
unless UNKNOWN_MODES.include?(options.fetch(:unknown, :ignore))
|
|
17
|
+
raise SchemaError, "typed_params #{name.inspect} unknown: must be one of #{UNKNOWN_MODES.inspect}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
raise SchemaError, "typed_params #{name.inspect} source: must be a proc" if options.key?(:source) && !options[:source].is_a?(Proc)
|
|
21
|
+
|
|
22
|
+
@owner = owner
|
|
23
|
+
@name = name.to_sym
|
|
24
|
+
@options = options.freeze
|
|
25
|
+
@block = block
|
|
26
|
+
@nodes = nodes&.freeze
|
|
27
|
+
@module_name = module_name
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# With `root:`, the block declares the keys of that required hash, as
|
|
31
|
+
# `params.require(:root).permit(...)` does, and the result is the inner hash.
|
|
32
|
+
def nodes
|
|
33
|
+
@nodes ||= begin
|
|
34
|
+
nodes = Builder.run(@block, required: options.fetch(:required, false), owner:)
|
|
35
|
+
root ? [Node.build(key: root, type: :hash, required: true, children: nodes)] : nodes
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def root
|
|
40
|
+
options[:root]&.to_sym
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A proc run on the controller for the params to validate, such as
|
|
44
|
+
# `-> { params.fetch(:contact, {}) }`, instead of all of `params`.
|
|
45
|
+
def params_source
|
|
46
|
+
options[:source]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unknown_mode
|
|
50
|
+
options.fetch(:unknown, :ignore)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# One line per key with its type, nested keys indented and `?` marking an
|
|
54
|
+
# optional key, so a snapshot shows what a request can set.
|
|
55
|
+
def describe
|
|
56
|
+
Schema.describe_nodes(nodes, 0).join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.describe_nodes(nodes, depth)
|
|
60
|
+
nodes.flat_map { |node| ["#{" " * depth}#{node.key}: #{describe_type(node)}#{"?" unless node.required}", *describe_children(node, depth + 1)] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.describe_type(node)
|
|
64
|
+
case node.type
|
|
65
|
+
when :array then "array(#{describe_type(node.item)}#{describe_cap(node.max_length)})"
|
|
66
|
+
when :map then "map(#{node.map_key.type} => #{describe_type(node.item)}#{describe_cap(node.max_length)})"
|
|
67
|
+
when :hash then node.open ? "hash(open)" : "hash"
|
|
68
|
+
when :one_of then "one_of(#{node.children.map { |alternative| describe_type(alternative) }.join(", ")})"
|
|
69
|
+
when :string then describe_string(node.max_length)
|
|
70
|
+
else node.type.to_s
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.describe_cap(max_length)
|
|
75
|
+
case max_length
|
|
76
|
+
when Builder::DEFAULT_COLLECTION_MAX_LENGTH then ""
|
|
77
|
+
when nil then ", uncapped"
|
|
78
|
+
else ", #{max_length}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.describe_string(max_length)
|
|
83
|
+
case max_length
|
|
84
|
+
when Builder::DEFAULT_STRING_MAX_LENGTH then "string"
|
|
85
|
+
when ActiveTypedParams.config.text_max_length then "text"
|
|
86
|
+
when nil then "string(uncapped)"
|
|
87
|
+
else "string(#{max_length})"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.describe_children(node, depth)
|
|
92
|
+
holder = (node.array? || node.map?) ? node.item : node
|
|
93
|
+
holder.hash? ? describe_nodes(holder.children, depth) : []
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def memoize?
|
|
97
|
+
options.fetch(:memoize, false)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def memo_ivar
|
|
101
|
+
"@__typed_params_#{name}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Validates params outside a controller, as a definition owned by a plain
|
|
105
|
+
# object does. Procs and conditions run on the context, which also names
|
|
106
|
+
# the report.
|
|
107
|
+
def validate(params, context: nil, request: nil)
|
|
108
|
+
call(context || Object.new, params, request)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def source
|
|
112
|
+
(@compiler || Compiler.new(self)).source
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def installed?
|
|
116
|
+
@installed
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Defines the real method on a module included in the owner after the stub
|
|
120
|
+
# module, so it takes precedence over the stub while an override on the
|
|
121
|
+
# owner or a subclass still reaches it with `super`.
|
|
122
|
+
def install!
|
|
123
|
+
return if @installed
|
|
124
|
+
|
|
125
|
+
MUTEX.synchronize do
|
|
126
|
+
return if @installed
|
|
127
|
+
|
|
128
|
+
@compiled_module = compile
|
|
129
|
+
@method = @compiled_module.instance_method(name)
|
|
130
|
+
owner&.include(@compiled_module)
|
|
131
|
+
@compiler = nil
|
|
132
|
+
@installed = true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Runs the installed method on any object, bypassing an override on a
|
|
137
|
+
# controller; a definition validates whatever params it is given.
|
|
138
|
+
def call(context, *arguments)
|
|
139
|
+
install!
|
|
140
|
+
@method.bind_call(context, *arguments)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def request_available?
|
|
144
|
+
return @request_available if defined?(@request_available)
|
|
145
|
+
|
|
146
|
+
@request_available = case owner
|
|
147
|
+
when nil then false
|
|
148
|
+
when Class then owner.method_defined?(:request) || owner.private_method_defined?(:request)
|
|
149
|
+
else true
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def module_name
|
|
154
|
+
@module_name ||= begin
|
|
155
|
+
base = owner.nil? ? "Definition" : (owner.name || "Anonymous_#{owner.object_id}")
|
|
156
|
+
"#{base.gsub("::", "_")}__#{name}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def compiler
|
|
163
|
+
@compiler ||= Compiler.new(self)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def compile
|
|
167
|
+
generated = compiler.source
|
|
168
|
+
Module.new.tap do |mod|
|
|
169
|
+
mod.set_temporary_name("active_typed_params/#{module_name}")
|
|
170
|
+
compiler.constants.each { |constant, value| mod.const_set(constant, value) }
|
|
171
|
+
mod.module_eval(generated, "active_typed_params/#{module_name}", 1)
|
|
172
|
+
mod.send(:private, name)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "bigdecimal"
|
|
6
|
+
require "date"
|
|
7
|
+
require "time"
|
|
8
|
+
require "active_support"
|
|
9
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
10
|
+
require "active_support/notifications"
|
|
11
|
+
require "action_controller"
|
|
12
|
+
|
|
13
|
+
require "active_typed_params/version"
|
|
14
|
+
require "active_typed_params/errors"
|
|
15
|
+
require "active_typed_params/node"
|
|
16
|
+
require "active_typed_params/enum_adapter"
|
|
17
|
+
require "active_typed_params/runtime"
|
|
18
|
+
require "active_typed_params/declaration"
|
|
19
|
+
require "active_typed_params/builder"
|
|
20
|
+
require "active_typed_params/compiler"
|
|
21
|
+
require "active_typed_params/schema"
|
|
22
|
+
require "active_typed_params/inline"
|
|
23
|
+
require "active_typed_params/controller"
|
|
24
|
+
require "active_typed_params/inference"
|
|
25
|
+
require "active_typed_params/railtie" if defined?(Rails::Railtie)
|
|
26
|
+
|
|
27
|
+
# Typed, compiled request parameters: see the `typed_params` controller DSL.
|
|
28
|
+
module ActiveTypedParams
|
|
29
|
+
class Configuration
|
|
30
|
+
# Each adapter receives the source given to `enum` and returns a Hash from
|
|
31
|
+
# raw request value to returned value, or nil when it does not handle it.
|
|
32
|
+
attr_accessor :enum_adapters
|
|
33
|
+
|
|
34
|
+
attr_accessor :file_classes
|
|
35
|
+
|
|
36
|
+
# The cap for `text` and for strings inference leaves to the model or the
|
|
37
|
+
# interaction: long, but bounded.
|
|
38
|
+
attr_accessor :text_max_length
|
|
39
|
+
|
|
40
|
+
# Each inferrer receives an ActiveModel attribute type and returns a
|
|
41
|
+
# Declaration, or nil when it does not handle it.
|
|
42
|
+
attr_accessor :type_inferrers
|
|
43
|
+
|
|
44
|
+
# Filter names `inputs_from` never declares, for inputs every interaction shares.
|
|
45
|
+
attr_accessor :ignored_filters
|
|
46
|
+
|
|
47
|
+
# Each inferrer receives an ActiveInteraction filter and returns a
|
|
48
|
+
# Declaration, or nil when it does not handle it.
|
|
49
|
+
attr_accessor :filter_inferrers
|
|
50
|
+
|
|
51
|
+
def initialize
|
|
52
|
+
@enum_adapters = []
|
|
53
|
+
@type_inferrers = []
|
|
54
|
+
@ignored_filters = []
|
|
55
|
+
@filter_inferrers = []
|
|
56
|
+
@text_max_length = 65_535
|
|
57
|
+
@file_classes = []
|
|
58
|
+
@file_classes << ActionDispatch::Http::UploadedFile if defined?(ActionDispatch::Http::UploadedFile)
|
|
59
|
+
@file_classes << Rack::Test::UploadedFile if defined?(Rack::Test::UploadedFile)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
def config
|
|
65
|
+
@config ||= Configuration.new
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def configure
|
|
69
|
+
yield config
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A definition survives code reloading, so one declared in an initializer
|
|
73
|
+
# must not reference reloadable classes, such as models or enums. Declare
|
|
74
|
+
# it beside the model instead, where reloading declares it again.
|
|
75
|
+
def define(name, **options, &block)
|
|
76
|
+
definitions[name.to_sym] = Schema.new(name:, options:, block:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Keys to merge with `use`, kept beside the model they describe, with no
|
|
80
|
+
# method and no global name: `IDEA_PARAMS = ActiveTypedParams.fragment { … }`.
|
|
81
|
+
def fragment(&block)
|
|
82
|
+
Schema.new(name: :fragment, block:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def definition(name)
|
|
86
|
+
definitions.fetch(name.to_sym) { raise SchemaError, "no schema defined as #{name.inspect}" }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def register(schema)
|
|
90
|
+
(registry[schema.owner] ||= {})[schema.name] = schema
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def schema(owner, name)
|
|
94
|
+
registry.dig(owner, name.to_sym) || raise(SchemaError, "#{owner} declares no typed_params #{name.inspect}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A schema declared on the owner or one of its ancestors, or else a global definition.
|
|
98
|
+
def find_schema(owner, name)
|
|
99
|
+
owner&.ancestors&.each do |ancestor|
|
|
100
|
+
schema = registry.dig(ancestor, name.to_sym)
|
|
101
|
+
return schema if schema
|
|
102
|
+
end
|
|
103
|
+
definition(name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def source(owner, name)
|
|
107
|
+
schema(owner, name).source
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def schemas
|
|
111
|
+
registry.values.flat_map(&:values)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Every schema a controller or a global definition declares, as a snapshot spec lists them.
|
|
115
|
+
def all_schemas
|
|
116
|
+
schemas + definitions.values
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def compile_all!
|
|
120
|
+
all_schemas.each(&:install!)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def inline_schema(key)
|
|
124
|
+
inline_schemas[key] ||= yield
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Code reloading replaces controller classes, which register their schemas
|
|
128
|
+
# again on load; schemas registered with `define` live in initializers and stay.
|
|
129
|
+
def unload!
|
|
130
|
+
registry.clear
|
|
131
|
+
inline_schemas.clear
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def registry
|
|
137
|
+
@registry ||= {}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def definitions
|
|
141
|
+
@definitions ||= {}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def inline_schemas
|
|
145
|
+
@inline_schemas ||= {}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active_typed_params
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Máximo Mussini
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionpack
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
description: Declare the keys and types a controller action accepts. Bad requests
|
|
41
|
+
are rejected with a 400 that names the parameter, and each declaration is compiled
|
|
42
|
+
into a single method, several times faster than params.permit.
|
|
43
|
+
email:
|
|
44
|
+
- maximomussini@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files:
|
|
48
|
+
- README.md
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- lib/active_typed_params.rb
|
|
54
|
+
- lib/active_typed_params/builder.rb
|
|
55
|
+
- lib/active_typed_params/compiler.rb
|
|
56
|
+
- lib/active_typed_params/controller.rb
|
|
57
|
+
- lib/active_typed_params/declaration.rb
|
|
58
|
+
- lib/active_typed_params/enum_adapter.rb
|
|
59
|
+
- lib/active_typed_params/errors.rb
|
|
60
|
+
- lib/active_typed_params/inference.rb
|
|
61
|
+
- lib/active_typed_params/inline.rb
|
|
62
|
+
- lib/active_typed_params/node.rb
|
|
63
|
+
- lib/active_typed_params/railtie.rb
|
|
64
|
+
- lib/active_typed_params/runtime.rb
|
|
65
|
+
- lib/active_typed_params/schema.rb
|
|
66
|
+
- lib/active_typed_params/version.rb
|
|
67
|
+
homepage: https://github.com/ElMassimo/active_typed_params
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
source_code_uri: https://github.com/ElMassimo/active_typed_params
|
|
72
|
+
changelog_uri: https://github.com/ElMassimo/active_typed_params/blob/main/CHANGELOG.md
|
|
73
|
+
allowed_push_host: https://rubygems.org
|
|
74
|
+
rubygems_mfa_required: 'true'
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.3.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.20
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Typed strong parameters for Rails controllers, compiled into plain Ruby methods.
|
|
92
|
+
test_files: []
|