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.
295 296 297 298 299 |
# File 'lib/permittable.rb', line 295 def apply_normalize(normalizer, value) return value unless normalizer && value.is_a?(String) normalizer.call(value) end |
.cast(type, value) ⇒ Object
209 210 211 212 213 |
# File 'lib/permittable.rb', line 209 def cast(type, value) return [:error, "invalid_type"] unless scalar_shaped?(value) public_send("cast_#{type}", value) end |
.cast_boolean(value) ⇒ Object
262 263 264 265 266 267 |
# File 'lib/permittable.rb', line 262 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
269 270 271 272 273 274 275 276 277 |
# File 'lib/permittable.rb', line 269 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.
281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/permittable.rb', line 281 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
253 254 255 256 257 258 259 260 |
# File 'lib/permittable.rb', line 253 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
243 244 245 246 247 248 249 250 251 |
# File 'lib/permittable.rb', line 243 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
232 233 234 235 236 237 238 239 240 241 |
# File 'lib/permittable.rb', line 232 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
224 225 226 227 228 229 230 |
# File 'lib/permittable.rb', line 224 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.
199 200 201 202 203 204 205 206 207 |
# File 'lib/permittable.rb', line 199 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.
180 181 182 183 184 185 186 |
# File 'lib/permittable.rb', line 180 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
188 189 190 191 192 193 194 |
# File 'lib/permittable.rb', line 188 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.
303 304 305 |
# File 'lib/permittable.rb', line 303 def included_in?(allowed, value) allowed.is_a?(Range) ? allowed.cover?(value) : allowed.include?(value) end |
.length_ok?(spec, length) ⇒ Boolean
307 308 309 |
# File 'lib/permittable.rb', line 307 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.
217 218 219 220 221 222 |
# File 'lib/permittable.rb', line 217 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 |