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.



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
  when ActiveSupport::TimeWithZone, Time then [:ok, value.to_time.utc]
  when 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.

Returns:

  • (Boolean)


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

Returns:

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

Returns:

  • (Boolean)


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