sinatra-my-params 0.0.11 → 0.0.12
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 +4 -4
- data/Rakefile +2 -10
- data/lib/permit_params.rb +134 -13
- data/spec/permit_params_advanced_spec.rb +232 -0
- data/spec/permit_params_shape_spec.rb +0 -1
- data/spec/permit_params_spec.rb +0 -1
- metadata +48 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3559e8affd6284a7ae9c07239ce1e4709fdab501899e7fc272efdad7fd25a0f8
|
|
4
|
+
data.tar.gz: 5187c37f8fd9d090d5fccd1c9b954b44bdc284f63dae452b8a9c2810f57fdb9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7621f5ef6dfccb1ac643132708cc8705e4a78e7c52a1ea33efd33932c911d78a7dcb8d01f35426a722f956fe3f0b7e524be12745de7385f039431b4f2762835d
|
|
7
|
+
data.tar.gz: a5463391804fa066d6cde70582698de0e144aaa4cf39bc4ffa925bc7e3a11cc9a58381de74d87131afb3effaf0db60c489a91227703b389939b8542c16e64ab9
|
data/Rakefile
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
require "rake/testtask"
|
|
2
1
|
require "rspec/core/rake_task"
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
t.libs << "test"
|
|
6
|
-
end
|
|
3
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
10
|
-
|
|
11
|
-
task :default => :spec
|
|
12
|
-
rescue LoadError
|
|
13
|
-
end
|
|
5
|
+
task :default => :spec
|
data/lib/permit_params.rb
CHANGED
|
@@ -8,22 +8,46 @@ module PermitParams
|
|
|
8
8
|
attr_accessor :param, :options
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Raised when a field marked required: true has no value and no
|
|
12
|
+
# default:. Always a subclass of InvalidParameterError, so existing
|
|
13
|
+
# `rescue PermitParams::InvalidParameterError` code keeps working
|
|
14
|
+
# unchanged if you don't care about the distinction.
|
|
15
|
+
class MissingParameterError < InvalidParameterError; end
|
|
16
|
+
|
|
11
17
|
def permitted_params(params, permitted = {}, strong_validation = false, options = {})
|
|
12
18
|
return params if permitted.empty?
|
|
13
19
|
|
|
14
20
|
coerced_params = {}
|
|
21
|
+
seen = {}
|
|
15
22
|
|
|
16
23
|
params.each do |key, value|
|
|
17
24
|
next unless permitted?(permitted: permitted, key: key, value: value)
|
|
18
25
|
|
|
26
|
+
spec = normalize_spec(permitted[key.to_sym])
|
|
27
|
+
seen[key.to_sym] = true
|
|
28
|
+
|
|
19
29
|
coerced = coerce(
|
|
20
30
|
param: value,
|
|
21
|
-
type:
|
|
31
|
+
type: spec[:type],
|
|
22
32
|
strong_validation: strong_validation,
|
|
23
|
-
options: options
|
|
33
|
+
options: merge_field_options(options, spec)
|
|
24
34
|
)
|
|
25
|
-
|
|
35
|
+
coerced = apply_constraints(coerced, spec, raw_value: value, strong_validation: strong_validation)
|
|
36
|
+
|
|
37
|
+
if coerced.nil?
|
|
38
|
+
fill_missing!(coerced_params, key, spec, present: true, raw_value: value)
|
|
39
|
+
else
|
|
40
|
+
coerced_params[key] = coerced
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
permitted.each_key do |perm_key|
|
|
45
|
+
next if seen[perm_key]
|
|
46
|
+
|
|
47
|
+
spec = normalize_spec(permitted[perm_key])
|
|
48
|
+
fill_missing!(coerced_params, perm_key, spec, present: false, raw_value: nil)
|
|
26
49
|
end
|
|
50
|
+
|
|
27
51
|
coerced_params
|
|
28
52
|
end
|
|
29
53
|
|
|
@@ -41,16 +65,95 @@ module PermitParams
|
|
|
41
65
|
DATE_TIME_TYPES = [Date, Time, DateTime].freeze
|
|
42
66
|
NUMERIC_PARSE_TYPES = [Integer, Float].freeze
|
|
43
67
|
|
|
68
|
+
# Per-field overrides that can live inside a Hash spec (e.g.
|
|
69
|
+
# `{ type: Array, of: Integer, delimiter: ';' }`) instead of only in
|
|
70
|
+
# the global 4th-argument `options` hash. Field-level values win.
|
|
71
|
+
FIELD_OPTION_KEYS = %i[delimiter separator integer_precision shape of].freeze
|
|
72
|
+
|
|
44
73
|
def permitted?(permitted:, key:, value:)
|
|
45
74
|
permitted.keys.map(&:to_s).include?(key.to_s) && !value.nil?
|
|
46
75
|
end
|
|
47
76
|
|
|
77
|
+
# A permitted value can still just be a bare type (`Integer`, `String`,
|
|
78
|
+
# `Boolean`, ...) exactly like before - that keeps every pre-0.0.12
|
|
79
|
+
# usage working unchanged. It can now ALSO be a Hash describing a
|
|
80
|
+
# richer contract: { type:, required:, default:, in:, match:, min:,
|
|
81
|
+
# max:, of:, shape:, delimiter:, separator:, integer_precision: }.
|
|
82
|
+
def normalize_spec(raw_spec)
|
|
83
|
+
if raw_spec.is_a?(::Hash)
|
|
84
|
+
spec = raw_spec.dup
|
|
85
|
+
spec[:type] = Any unless spec.key?(:type)
|
|
86
|
+
spec
|
|
87
|
+
else
|
|
88
|
+
{ type: raw_spec }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def merge_field_options(global_options, spec)
|
|
93
|
+
overrides = spec.select { |k, _| FIELD_OPTION_KEYS.include?(k) }
|
|
94
|
+
global_options.merge(overrides)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Called once per permitted field whenever it ends up without a
|
|
98
|
+
# coerced value - either because it failed validation (present: true)
|
|
99
|
+
# or because it was never supplied at all (present: false).
|
|
100
|
+
def fill_missing!(coerced_params, key, spec, present:, raw_value:)
|
|
101
|
+
if spec.key?(:default)
|
|
102
|
+
coerced_params[key] = spec[:default]
|
|
103
|
+
elsif spec[:required]
|
|
104
|
+
if present
|
|
105
|
+
raise InvalidParameterError, "'#{raw_value}' is not a valid #{spec[:type]}"
|
|
106
|
+
else
|
|
107
|
+
raise MissingParameterError, "'#{key}' is required"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
# else: optional and absent/invalid - silently omitted, same as pre-0.0.12
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apply_constraints(value, spec, raw_value:, strong_validation:)
|
|
114
|
+
return value if value.nil?
|
|
115
|
+
|
|
116
|
+
violation = constraint_violation(value, spec)
|
|
117
|
+
return value unless violation
|
|
118
|
+
|
|
119
|
+
raise InvalidParameterError, "'#{raw_value}' is not a valid #{spec[:type]} (#{violation})" if strong_validation
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def constraint_violation(value, spec)
|
|
125
|
+
return "must be one of #{spec[:in].inspect}" if spec[:in] && !spec[:in].include?(value)
|
|
126
|
+
return 'does not match required format' if spec[:match] && !(value.is_a?(String) && spec[:match].match?(value))
|
|
127
|
+
|
|
128
|
+
comparable = comparable_value(value)
|
|
129
|
+
return "must be >= #{spec[:min]}" if spec[:min] && comparable && comparable < spec[:min]
|
|
130
|
+
return "must be <= #{spec[:max]}" if spec[:max] && comparable && comparable > spec[:max]
|
|
131
|
+
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# What min:/max: compares against: the value itself for numbers, its
|
|
136
|
+
# length for anything else that has one (String, Array, Hash).
|
|
137
|
+
def comparable_value(value)
|
|
138
|
+
return value if value.is_a?(Numeric)
|
|
139
|
+
return value.length if value.respond_to?(:length)
|
|
140
|
+
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
48
144
|
def coerce(param:, type:, strong_validation: false, options: {})
|
|
49
145
|
return param if type == Any
|
|
50
146
|
|
|
51
147
|
begin
|
|
52
148
|
return nil if param.nil?
|
|
53
|
-
|
|
149
|
+
|
|
150
|
+
# `of:` on an Array field means every element needs its own
|
|
151
|
+
# coercion, so an already-Array param (e.g. from a JSON body)
|
|
152
|
+
# can't take the "already the right class" shortcut below.
|
|
153
|
+
of = options[:of]
|
|
154
|
+
array_with_of = type == Array && of
|
|
155
|
+
|
|
156
|
+
return param if !array_with_of && begin
|
|
54
157
|
param.is_a?(type)
|
|
55
158
|
rescue StandardError
|
|
56
159
|
false
|
|
@@ -66,17 +169,25 @@ module PermitParams
|
|
|
66
169
|
return Date.parse(param) if type == Date
|
|
67
170
|
return Time.parse(param) if type == Time
|
|
68
171
|
return DateTime.parse(param) if type == DateTime
|
|
69
|
-
return coerce_array(param, options) if type == Array
|
|
172
|
+
return coerce_array(param, options, strong_validation: strong_validation) if type == Array
|
|
70
173
|
return coerce_shape(param, options) if type == Shape
|
|
71
174
|
return coerce_hash(param, options) if type == Hash
|
|
72
175
|
return coerce_boolean(param) if [TrueClass, FalseClass, Boolean].include? type
|
|
73
176
|
|
|
177
|
+
nil
|
|
178
|
+
rescue InvalidParameterError
|
|
179
|
+
# Raised by a nested coerce() call (e.g. one bad element inside an
|
|
180
|
+
# `of:` array). Re-raise as-is so the precise inner message survives
|
|
181
|
+
# instead of being replaced by a generic outer one.
|
|
182
|
+
raise if strong_validation
|
|
183
|
+
|
|
74
184
|
nil
|
|
75
185
|
rescue StandardError
|
|
76
|
-
# Any failure while coercing untrusted input (malformed
|
|
77
|
-
# unexpected nested shape, missing stdlib constant, etc.)
|
|
78
|
-
# crash the caller's request handler - it's either
|
|
79
|
-
# (strong_validation) or dropped silently, same
|
|
186
|
+
# Any other failure while coercing untrusted input (malformed
|
|
187
|
+
# value, unexpected nested shape, missing stdlib constant, etc.)
|
|
188
|
+
# must never crash the caller's request handler - it's either
|
|
189
|
+
# rejected loudly (strong_validation) or dropped silently, same
|
|
190
|
+
# as an invalid value.
|
|
80
191
|
raise InvalidParameterError, "'#{param}' is not a valid #{type}" if strong_validation
|
|
81
192
|
|
|
82
193
|
nil
|
|
@@ -87,11 +198,21 @@ module PermitParams
|
|
|
87
198
|
Integer(param, options[:integer_precision] || 10)
|
|
88
199
|
end
|
|
89
200
|
|
|
90
|
-
def coerce_array(param, options = {})
|
|
91
|
-
|
|
92
|
-
|
|
201
|
+
def coerce_array(param, options = {}, strong_validation: false)
|
|
202
|
+
elements =
|
|
203
|
+
if param.is_a?(Array)
|
|
204
|
+
param
|
|
205
|
+
else
|
|
206
|
+
delimiter = valid_delimiter?(param, options[:delimiter])
|
|
207
|
+
return unless delimiter
|
|
208
|
+
|
|
209
|
+
param.split(delimiter).map(&:strip)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
of = options[:of]
|
|
213
|
+
return Array(elements) unless of
|
|
93
214
|
|
|
94
|
-
|
|
215
|
+
elements.map { |el| coerce(param: el, type: of, strong_validation: strong_validation, options: options) }.compact
|
|
95
216
|
end
|
|
96
217
|
|
|
97
218
|
def coerce_hash(param, options = {})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'permit_params'
|
|
4
|
+
require 'rspec'
|
|
5
|
+
|
|
6
|
+
describe PermitParams do
|
|
7
|
+
include PermitParams
|
|
8
|
+
|
|
9
|
+
# -- required: ------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
describe 'required:' do
|
|
12
|
+
it 'raises MissingParameterError when a required param is absent' do
|
|
13
|
+
expect do
|
|
14
|
+
permitted_params({}, { email: { type: String, required: true } })
|
|
15
|
+
end.to raise_error(PermitParams::MissingParameterError, "'email' is required")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'raises MissingParameterError when a required param is explicitly nil' do
|
|
19
|
+
expect do
|
|
20
|
+
permitted_params({ email: nil }, { email: { type: String, required: true } })
|
|
21
|
+
end.to raise_error(PermitParams::MissingParameterError, "'email' is required")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'raises InvalidParameterError (not MissingParameterError) when present but invalid, even with strong_validation false' do
|
|
25
|
+
expect do
|
|
26
|
+
permitted_params({ age: 'not a number' }, { age: { type: Integer, required: true } })
|
|
27
|
+
end.to raise_error(PermitParams::InvalidParameterError, "'not a number' is not a valid Integer")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'does not raise when a required param is present and valid' do
|
|
31
|
+
output = permitted_params({ email: 'a@b.com' }, { email: { type: String, required: true } })
|
|
32
|
+
expect(output).to eq(email: 'a@b.com')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'is a no-op for optional params left unspecified (backward compatible default)' do
|
|
36
|
+
output = permitted_params({}, { nickname: String })
|
|
37
|
+
expect(output).to eq({})
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'MissingParameterError is also an InvalidParameterError, so old rescue clauses still catch it' do
|
|
41
|
+
expect(PermitParams::MissingParameterError.ancestors).to include(PermitParams::InvalidParameterError)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# -- default: ---------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
describe 'default:' do
|
|
48
|
+
it 'fills in the default when the param is absent' do
|
|
49
|
+
output = permitted_params({}, { role: { type: String, default: 'member' } })
|
|
50
|
+
expect(output).to eq(role: 'member')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'fills in the default when the param fails coercion' do
|
|
54
|
+
output = permitted_params({ limit: 'lots' }, { limit: { type: Integer, default: 10 } })
|
|
55
|
+
expect(output).to eq(limit: 10)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'uses the given value instead of the default when valid' do
|
|
59
|
+
output = permitted_params({ limit: '25' }, { limit: { type: Integer, default: 10 } })
|
|
60
|
+
expect(output).to eq(limit: 25)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'default takes priority over required (no error raised)' do
|
|
64
|
+
output = permitted_params({}, { role: { type: String, required: true, default: 'member' } })
|
|
65
|
+
expect(output).to eq(role: 'member')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# -- in: (enum) ---------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe 'in:' do
|
|
72
|
+
let(:permitted) { { status: { type: String, in: %w[draft published] } } }
|
|
73
|
+
|
|
74
|
+
it 'keeps a value that is in the allowed list' do
|
|
75
|
+
output = permitted_params({ status: 'draft' }, permitted)
|
|
76
|
+
expect(output).to eq(status: 'draft')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'drops a value that is not in the allowed list (lenient)' do
|
|
80
|
+
output = permitted_params({ status: 'deleted' }, permitted)
|
|
81
|
+
expect(output).to eq({})
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'raises with strong_validation when value is not in the allowed list' do
|
|
85
|
+
expect do
|
|
86
|
+
permitted_params({ status: 'deleted' }, permitted, true)
|
|
87
|
+
end.to raise_error(PermitParams::InvalidParameterError, "'deleted' is not a valid String (must be one of [\"draft\", \"published\"])")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# -- match: (regex) ------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
describe 'match:' do
|
|
94
|
+
let(:permitted) { { code: { type: String, match: /\A[A-Z]{3}\d{3}\z/ } } }
|
|
95
|
+
|
|
96
|
+
it 'keeps a value that matches the format' do
|
|
97
|
+
output = permitted_params({ code: 'ABC123' }, permitted)
|
|
98
|
+
expect(output).to eq(code: 'ABC123')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'drops a value that does not match the format (lenient)' do
|
|
102
|
+
output = permitted_params({ code: 'nope' }, permitted)
|
|
103
|
+
expect(output).to eq({})
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'raises with strong_validation when value does not match the format' do
|
|
107
|
+
expect do
|
|
108
|
+
permitted_params({ code: 'nope' }, permitted, true)
|
|
109
|
+
end.to raise_error(PermitParams::InvalidParameterError)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# -- min: / max: ----------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
describe 'min: / max:' do
|
|
116
|
+
it 'keeps a numeric value within bounds' do
|
|
117
|
+
output = permitted_params({ age: '25' }, { age: { type: Integer, min: 18, max: 65 } })
|
|
118
|
+
expect(output).to eq(age: 25)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'drops a numeric value below the minimum' do
|
|
122
|
+
output = permitted_params({ age: '10' }, { age: { type: Integer, min: 18 } })
|
|
123
|
+
expect(output).to eq({})
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'drops a numeric value above the maximum' do
|
|
127
|
+
output = permitted_params({ age: '99' }, { age: { type: Integer, max: 65 } })
|
|
128
|
+
expect(output).to eq({})
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'applies min:/max: to string length' do
|
|
132
|
+
output = permitted_params({ name: 'a' }, { name: { type: String, min: 2 } })
|
|
133
|
+
expect(output).to eq({})
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'applies min:/max: to array length' do
|
|
137
|
+
output = permitted_params({ tags: %w[a b] }, { tags: { type: Array, max: 1 } })
|
|
138
|
+
expect(output).to eq({})
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# -- of: (typed array elements) --------------------------------------------
|
|
143
|
+
|
|
144
|
+
describe 'of:' do
|
|
145
|
+
it 'coerces each delimited element to the given type' do
|
|
146
|
+
output = permitted_params({ ids: '1,2,3' }, { ids: { type: Array, of: Integer } })
|
|
147
|
+
expect(output).to eq(ids: [1, 2, 3])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'coerces each element of an already-Array param (e.g. JSON body)' do
|
|
151
|
+
output = permitted_params({ ids: %w[1 2 3] }, { ids: { type: Array, of: Integer } })
|
|
152
|
+
expect(output).to eq(ids: [1, 2, 3])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'drops individual invalid elements (lenient)' do
|
|
156
|
+
output = permitted_params({ ids: '1,x,3' }, { ids: { type: Array, of: Integer } })
|
|
157
|
+
expect(output).to eq(ids: [1, 3])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'raises with strong_validation, preserving the precise inner element message' do
|
|
161
|
+
expect do
|
|
162
|
+
permitted_params({ ids: '1,x,3' }, { ids: { type: Array, of: Integer } }, true)
|
|
163
|
+
end.to raise_error(PermitParams::InvalidParameterError, "'x' is not a valid Integer")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'supports of: Shape for an array of nested objects' do
|
|
167
|
+
permitted = { items: { type: Array, of: PermitParams::Shape, shape: { id: Integer, name: String } } }
|
|
168
|
+
input = { items: [{ id: 1, name: 'a' }, { id: 2, name: 'b' }] }
|
|
169
|
+
output = permitted_params(input, permitted)
|
|
170
|
+
expect(output).to eq(items: [{ id: 1, name: 'a' }, { id: 2, name: 'b' }])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'drops array elements whose shape does not match' do
|
|
174
|
+
permitted = { items: { type: Array, of: PermitParams::Shape, shape: { id: Integer } } }
|
|
175
|
+
input = { items: [{ id: 1 }, { id: 'not an integer' }] }
|
|
176
|
+
output = permitted_params(input, permitted)
|
|
177
|
+
expect(output).to eq(items: [{ id: 1 }])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# -- per-field shape:/delimiter:/separator:/integer_precision: -------------
|
|
182
|
+
|
|
183
|
+
describe 'per-field options override the global options hash' do
|
|
184
|
+
it 'lets two different Shape params use two different shapes at once' do
|
|
185
|
+
permitted = {
|
|
186
|
+
author: { type: PermitParams::Shape, shape: { name: String } },
|
|
187
|
+
book: { type: PermitParams::Shape, shape: { title: String, year: Integer } }
|
|
188
|
+
}
|
|
189
|
+
input = { author: { name: 'Ada' }, book: { title: 'Notes', year: 1843 } }
|
|
190
|
+
|
|
191
|
+
output = permitted_params(input, permitted)
|
|
192
|
+
expect(output).to eq(author: { name: 'Ada' }, book: { title: 'Notes', year: 1843 })
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'falls back to the global options hash shape: when a field does not override it' do
|
|
196
|
+
permitted = { legacy: PermitParams::Shape }
|
|
197
|
+
output = permitted_params({ legacy: { a: 1 } }, permitted, false, shape: { a: Integer })
|
|
198
|
+
expect(output).to eq(legacy: { a: 1 })
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'lets a field override the global delimiter' do
|
|
202
|
+
permitted = { tags: { type: Array, delimiter: ';' } }
|
|
203
|
+
output = permitted_params({ tags: '1; 2' }, permitted, false, delimiter: ',')
|
|
204
|
+
expect(output).to eq(tags: %w[1 2])
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# -- combinations -----------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
describe 'combined real-world example' do
|
|
211
|
+
it 'validates a small "create user" style payload in one call' do
|
|
212
|
+
permitted = {
|
|
213
|
+
email: { type: String, required: true, match: /\A[^@\s]+@[^@\s]+\z/ },
|
|
214
|
+
role: { type: String, in: %w[member admin], default: 'member' },
|
|
215
|
+
age: { type: Integer, min: 13 },
|
|
216
|
+
tags: { type: Array, of: String, max: 5 }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
output = permitted_params(
|
|
220
|
+
{ email: 'a@b.com', age: '30', tags: 'ruby,sinatra' },
|
|
221
|
+
permitted
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
expect(output).to eq(
|
|
225
|
+
email: 'a@b.com',
|
|
226
|
+
role: 'member',
|
|
227
|
+
age: 30,
|
|
228
|
+
tags: %w[ruby sinatra]
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
data/spec/permit_params_spec.rb
CHANGED
metadata
CHANGED
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sinatra-my-params
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marco Aviles
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rack-test
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.1'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.12'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.12'
|
|
13
55
|
description: A simple params sanitizer (originally created for sinatra)
|
|
14
56
|
email: gdmarav374@gmail.com
|
|
15
57
|
executables: []
|
|
@@ -19,6 +61,7 @@ files:
|
|
|
19
61
|
- Rakefile
|
|
20
62
|
- bin/permit_params
|
|
21
63
|
- lib/permit_params.rb
|
|
64
|
+
- spec/permit_params_advanced_spec.rb
|
|
22
65
|
- spec/permit_params_shape_spec.rb
|
|
23
66
|
- spec/permit_params_spec.rb
|
|
24
67
|
homepage: https://github.com/mhero/sinatra-my-params
|
|
@@ -32,7 +75,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
32
75
|
requirements:
|
|
33
76
|
- - ">="
|
|
34
77
|
- !ruby/object:Gem::Version
|
|
35
|
-
version: '
|
|
78
|
+
version: '2.6'
|
|
36
79
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
37
80
|
requirements:
|
|
38
81
|
- - ">="
|
|
@@ -46,3 +89,4 @@ summary: permit_params!
|
|
|
46
89
|
test_files:
|
|
47
90
|
- spec/permit_params_spec.rb
|
|
48
91
|
- spec/permit_params_shape_spec.rb
|
|
92
|
+
- spec/permit_params_advanced_spec.rb
|