Module: Permittable::Coercion
- Defined in:
- lib/permittable.rb
Overview
Strict params-shaped coercion, shared by request-time validation and
macro-time default: checking. Every entry point returns
[:ok, cast_value] or [:error, code_string].
Constant Summary collapse
- TRUE_VALUES =
[true, "true", "1", 1].freeze
- FALSE_VALUES =
[false, "false", "0", 0].freeze
Class Method Summary collapse
-
.apply_normalize(normalizer, value) ⇒ Object
Presets only make sense on String input; a non-String value (JSON numbers, booleans) skips normalization and goes straight to the cast.
- .cast(type, value) ⇒ Object
- .cast_boolean(value) ⇒ Object
- .cast_date(value) ⇒ Object
-
.cast_datetime(value) ⇒ Object
A zoneless String parses as UTC regardless of the host timezone (deterministic); explicit offsets are honoured and normalised to UTC.
- .cast_decimal(value) ⇒ Object
- .cast_float(value) ⇒ Object
- .cast_integer(value) ⇒ Object
- .cast_string(value) ⇒ Object
-
.check_custom(validator, value) ⇒ Object
A custom validator returning a Symbol fails with that symbol as the violation code; false/nil fails as "invalid"; any other truthy value passes.
-
.check_scalar(field, value) ⇒ Object
Full pipeline for one scalar field: normalize → cast → in / format / length / validate.
- .check_scalar_rules(field, value) ⇒ Object
-
.included_in?(allowed, value) ⇒ Boolean
Range#include? walks discrete ranges; cover? is the O(1) bounds check and the right semantics for validation.
- .length_ok?(spec, length) ⇒ Boolean
-
.scalar_shaped?(value) ⇒ Boolean
Arrays, hashes, and nested ActionController::Parameters (
?age[]=1,?age[x]=1) can never satisfy a scalar type.
Class Method Details
.apply_normalize(normalizer, value) ⇒ Object
Presets only make sense on String input; a non-String value (JSON numbers, booleans) skips normalization and goes straight to the cast.
286 287 288 289 290 |
# File 'lib/permittable.rb', line 286 def apply_normalize(normalizer, value) return value unless normalizer && value.is_a?(String) normalizer.call(value) end |
.cast(type, value) ⇒ Object
200 201 202 203 204 |
# File 'lib/permittable.rb', line 200 def cast(type, value) return [:error, "invalid_type"] unless scalar_shaped?(value) public_send("cast_#{type}", value) end |
.cast_boolean(value) ⇒ Object
253 254 255 256 257 258 |
# File 'lib/permittable.rb', line 253 def cast_boolean(value) return [:ok, true] if TRUE_VALUES.include?(value) return [:ok, false] if FALSE_VALUES.include?(value) [:error, "invalid_type"] end |
.cast_date(value) ⇒ Object
260 261 262 263 264 265 266 267 268 |
# File 'lib/permittable.rb', line 260 def cast_date(value) case value when Date then [:ok, value] when String then [:ok, Date.parse(value)] else [:error, "invalid_type"] end rescue ArgumentError, RangeError [:error, "invalid_type"] end |
.cast_datetime(value) ⇒ Object
A zoneless String parses as UTC regardless of the host timezone (deterministic); explicit offsets are honoured and normalised to UTC.
272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/permittable.rb', line 272 def cast_datetime(value) case value # DateTime is listed here, ahead of Date, because it subclasses Date. when ActiveSupport::TimeWithZone, Time, DateTime then [:ok, value.to_time.utc] when Date then [:ok, Time.utc(value.year, value.month, value.day)] when String then [:ok, DateTime.parse(value).to_time.utc] else [:error, "invalid_type"] end rescue ArgumentError, RangeError [:error, "invalid_type"] end |
.cast_decimal(value) ⇒ Object
244 245 246 247 248 249 250 251 |
# File 'lib/permittable.rb', line 244 def cast_decimal(value) case value when Numeric, String then [:ok, BigDecimal(value.to_s)] else [:error, "invalid_type"] end rescue ArgumentError [:error, "invalid_type"] end |
.cast_float(value) ⇒ Object
234 235 236 237 238 239 240 241 242 |
# File 'lib/permittable.rb', line 234 def cast_float(value) case value when Numeric then [:ok, value.to_f] when String then [:ok, Float(value)] else [:error, "invalid_type"] end rescue ArgumentError [:error, "invalid_type"] end |
.cast_integer(value) ⇒ Object
223 224 225 226 227 228 229 230 231 232 |
# File 'lib/permittable.rb', line 223 def cast_integer(value) case value when Integer then [:ok, value] when Float then value == value.truncate ? [:ok, value.to_i] : [:error, "invalid_type"] when String then [:ok, Integer(value, 10)] else [:error, "invalid_type"] end rescue ArgumentError [:error, "invalid_type"] end |
.cast_string(value) ⇒ Object
215 216 217 218 219 220 221 |
# File 'lib/permittable.rb', line 215 def cast_string(value) case value when String then [:ok, value] when Numeric, true, false then [:ok, value.to_s] else [:error, "invalid_type"] end end |
.check_custom(validator, value) ⇒ Object
A custom validator returning a Symbol fails with that symbol as the violation code; false/nil fails as "invalid"; any other truthy value passes.
190 191 192 193 194 195 196 197 198 |
# File 'lib/permittable.rb', line 190 def check_custom(validator, value) return [:ok, value] unless validator verdict = validator.call(value) return [:error, verdict.to_s] if verdict.is_a?(Symbol) return [:error, "invalid"] unless verdict [:ok, value] end |
.check_scalar(field, value) ⇒ Object
Full pipeline for one scalar field: normalize → cast → in / format / length / validate.
171 172 173 174 175 176 177 |
# File 'lib/permittable.rb', line 171 def check_scalar(field, value) value = apply_normalize(field[:normalize], value) status, value = cast(field[:type], value) return [status, value] unless status == :ok check_scalar_rules(field, value) end |
.check_scalar_rules(field, value) ⇒ Object
179 180 181 182 183 184 185 |
# File 'lib/permittable.rb', line 179 def check_scalar_rules(field, value) return [:error, "inclusion"] if field[:in] && !included_in?(field[:in], value) return [:error, "format"] if field[:format] && !field[:format].match?(value) return [:error, "length"] if field[:length] && !length_ok?(field[:length], value.length) check_custom(field[:validate], value) end |
.included_in?(allowed, value) ⇒ Boolean
Range#include? walks discrete ranges; cover? is the O(1) bounds check and the right semantics for validation.
294 295 296 |
# File 'lib/permittable.rb', line 294 def included_in?(allowed, value) allowed.is_a?(Range) ? allowed.cover?(value) : allowed.include?(value) end |
.length_ok?(spec, length) ⇒ Boolean
298 299 300 |
# File 'lib/permittable.rb', line 298 def length_ok?(spec, length) spec.is_a?(Range) ? spec.cover?(length) : spec == length end |
.scalar_shaped?(value) ⇒ Boolean
Arrays, hashes, and nested ActionController::Parameters
(?age[]=1, ?age[x]=1) can never satisfy a scalar type.
208 209 210 211 212 213 |
# File 'lib/permittable.rb', line 208 def scalar_shaped?(value) return false if value.is_a?(Array) || value.is_a?(Hash) return false if defined?(ActionController::Parameters) && value.is_a?(ActionController::Parameters) true end |