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

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.

Returns:

  • (Boolean)


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

Returns:

  • (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.

Returns:

  • (Boolean)


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