module Coercive

Public: The Coercive module implements a succinct DSL for declaring callable modules that will validate and coerce input data to an expected format.

Public Instance Methods

call(input) click to toggle source

Public: Coercive the given input using the declared attribute coercions.

input - input Hash with string keys that correspond to declared attributes.

Returns a Hash with known attributes as string keys and coerced values. Raises a Coercive::Error if the given input is not a Hash, or if there are any unknown string keys in the Hash, or if the values for the known keys do not pass the inner coercions for the associated declared attributes.

# File lib/coercive.rb, line 26
def call(input)
  fail Coercive::Error.new("not_valid") unless input.is_a?(Hash)

  errors  = {}

  # Fetch attributes from the input Hash into the fetched_attrs Hash.
  #
  # Each fetch function is responsible for fetching its associated attribute
  # into the fetched_attrs Hash, or choosing not to fetch it, or choosing to
  # raise a Coercive::Error.
  #
  # These fetch functions encapsulate the respective strategies for dealing
  # with required, optional, or implicit attributes appropriately.
  fetched_attrs = {}
  attr_fetch_fns.each do |name, fetch_fn|
    begin
      fetch_fn.call(input, fetched_attrs)
    rescue Coercive::Error => e
      errors[name] = e.errors
    end
  end

  # Check for unknown names in the input (not declared, and thus not fetched).
  input.each_key do |name|
    errors[name] = "unknown" unless fetched_attrs.key?(name)
  end

  # Coercive fetched attributes into the coerced_attrs Hash.
  #
  # Each coerce function will coerce the given input value for that attribute
  # to an acceptable output value, or choose to raise a Coercive::Error.
  coerced_attrs = {}
  fetched_attrs.each do |name, value|
    coerce_fn = attr_coerce_fns.fetch(name)
    begin
      coerced_attrs[name] = coerce_fn.call(value)
    rescue Coercive::Error => e
      errors[name] = e.errors
    end
  end

  # Fail if fetching or coercion caused any errors.
  fail Coercive::Error.new(errors) unless errors.empty?

  coerced_attrs
end

Private Instance Methods

any() click to toggle source

Public DSL: Return a coerce function that doesn't change or reject anything. Used when declaring an attribute. See documentation for attr_coerce_fns.

# File lib/coercive.rb, line 133
def any
  ->(input) do
    input
  end
end
array(inner_coerce_fn) click to toggle source

Public DSL: Return a coercion function to coerce input to an Array. Used when declaring an attribute. See documentation for attr_coerce_fns.

inner_coerce_fn - the coerce function to use on each element of the Array.

# File lib/coercive.rb, line 293
def array(inner_coerce_fn)
  ->(input) do
    output = []
    errors = []
    Array(input).each do |value|
      begin
        output << inner_coerce_fn.call(value)
        errors << nil # pad the errors array with a nil element so that any
                      # errors that follow will be in the right position
      rescue Coercive::Error => e
        errors << e.errors
      end
    end

    fail Coercive::Error.new(errors) if errors.any?

    output
  end
end
attr_coerce_fns() click to toggle source

Private: Hash with String attribute names as keys and fetch function values.

Each coerce function will be called with one argument: the input to coerce.

The coerce function can use any logic to convert the given input value to an acceptable output value, or raise a Coercive::Error for failure.

In practice, it is most common to use one of the builtin generator methods (for example, string, or array(string)), or to use a module that was declared using the Coercive DSL functions, though any custom coerce function may be created and used for other behaviour, provided that it conforms to the same interface.

# File lib/coercive.rb, line 87
def attr_coerce_fns
  @attr_coerce_fns ||= {}
end
attr_fetch_fns() click to toggle source

Private: Hash with String attribute names as keys and fetch function values.

Each fetch function will be called with two arguments: 1 - input Hash of input attributes with String keys. 2 - output Hash in which the fetched attribute should be stored (if at all).

The fetch function can use any logic to determine whether the attribute is present, whether it should be stored, whether to use an implicit default value, or whether to raise a Coercive::Error to propagate failure upward.

In practice, it is most common to use one of the builtin generator methods, (required, optional, or implicit) to create the fetch function, though any custom fetch function could also be used for other behaviour.

The return value of the fetch function will be ignored.

# File lib/coercive.rb, line 106
def attr_fetch_fns
  @attr_fetch_fns ||= {}
end
attribute(name, coerce_fn, fetch_fn_generator) click to toggle source

Public DSL: Declare a named attribute with a coercion and fetcher mechanism.

name - a Symbol name for this attribute. coerce_fn - a coerce function which may be any callable object

that accepts a single argument as the input data and
returns the coerced output (or raises a Coercive::Error).
See documentation for the attr_coerce_fns method.

fetch_fn_generator - a callable generator that returns a fetch function when

given the String name of the attribute to be fetched.
See documentation for the attr_fetch_fns method.

Returns the given name.

# File lib/coercive.rb, line 122
def attribute(name, coerce_fn, fetch_fn_generator)
  str_name = name.to_s

  attr_coerce_fns[str_name] = coerce_fn
  attr_fetch_fns[str_name]  = fetch_fn_generator.call(str_name)

  name
end
boolean(true_if: member([true, "true"]), false_if: member([false, "false"])) click to toggle source

Public DSL: Return a coerce function to coerce input to true or false. Used when declaring an attribute. See documentation for attr_coerce_fns.

true_if - coerce function to override which values will coerce to true. false_if - coerce function to override which values will coerce to false.

# File lib/coercive.rb, line 194
def boolean(true_if: member([true, "true"]), false_if: member([false, "false"]))
  ->(input) do
    test_success = ->(coerce_fn) do
      begin
        coerce_fn.call(input)
      rescue Coercive::Error
        return false
      end

      true
    end

    if test_success.(true_if)
      true
    elsif test_success.(false_if)
      false
    else
      fail Coercive::Error.new("not_valid")
    end
  end
end
date(format: nil) click to toggle source

Public DSL: Return a coercion function to coerce input into a Date. Used when declaring an attribute. See documentation for attr_coerce_fns.

format - String following Ruby's `strftime` format to change the parsing behavior. When empty

it will expect the String to be ISO 8601 compatible.
# File lib/coercive.rb, line 252
def date(format: nil)
  ->(input) do
    input = begin
              if format
                Date.strptime(input, format)
              else
                Date.iso8601(input)
              end
            rescue ArgumentError
              fail Coercive::Error.new("not_valid")
            end

    input
  end
end
datetime(format: nil) click to toggle source

Public DSL: Return a coercion function to coerce input into a DateTime. Used when declaring an attribute. See documentation for attr_coerce_fns.

format - String following Ruby's `strftime` format to change the parsing behavior. When empty

it will expect the String to be ISO 8601 compatible.
# File lib/coercive.rb, line 273
def datetime(format: nil)
  ->(input) do
    input = begin
              if format
                DateTime.strptime(input, format)
              else
                DateTime.iso8601(input)
              end
            rescue ArgumentError
              fail Coercive::Error.new("not_valid")
            end

    input
  end
end
float(min: nil, max: nil) click to toggle source

Public DSL: Return a coerce function to coerce input to a Float. Used when declaring an attribute. See documentation for attr_coerce_fns.

# File lib/coercive.rb, line 174
def float(min: nil, max: nil)
  ->(input) do
    input = begin
              Float(input)
            rescue TypeError, ArgumentError
              fail Coercive::Error.new("not_numeric")
            end

    fail Coercive::Error.new("too_low")  if min && input < min
    fail Coercive::Error.new("too_high") if max && input > max

    input
  end
end
hash(key_coerce_fn, val_coerce_fn) click to toggle source

Public DSL: Return a coercion function to coerce input to a Hash. Used when declaring an attribute. See documentation for attr_coerce_fns.

key_coerce_fn - the coerce function to use on each key of the Hash. val_coerce_fn - the coerce function to use on each value of the Hash.

# File lib/coercive.rb, line 318
def hash(key_coerce_fn, val_coerce_fn)
  ->(input) do
    fail Coercive::Error.new("not_valid") unless input.is_a?(Hash)

    output = {}
    errors = {}
    input.each do |key, value|
      begin
        key         = key_coerce_fn.call(key)
        output[key] = val_coerce_fn.call(value)
      rescue Coercive::Error => e
        errors[key] = e.errors
      end
    end

    fail Coercive::Error.new(errors) if errors.any?

    output
  end
end
implicit(default) click to toggle source

Public DSL: Return a generator function for an “implicit” fetch function. Used when declaring an attribute. See documentation for attr_fetch_fns.

The fetcher will store either the present attribute or the given default.

default - the implicit value to use if the attribute is not present.

# File lib/coercive.rb, line 376
def implicit(default)
  ->(name) do
    ->(attrs, fetched) do
      fetched[name] = attrs.key?(name) ? attrs[name] : default
    end
  end
end
integer(min: nil, max: nil) click to toggle source

Public DSL: Return a coerce function to coerce input to an Integer. Used when declaring an attribute. See documentation for attr_coerce_fns.

# File lib/coercive.rb, line 155
def integer(min: nil, max: nil)
  ->(input) do
    fail Coercive::Error.new("float_not_permitted") if input.is_a?(Float)

    input = begin
              Integer(input)
            rescue TypeError, ArgumentError
              fail Coercive::Error.new("not_numeric")
            end

    fail Coercive::Error.new("too_low")  if min && input < min
    fail Coercive::Error.new("too_high") if max && input > max

    input
  end
end
member(set) click to toggle source

Public DSL: Return a coerce function to validate that the input is a member of the given set. That is, the input must be equal to at least one member of the given set, or a Coercive::Error will be raised. Used when declaring an attribute. See documentation for attr_coerce_fns.

set - the Array of objects to use as the set for checking membership.

# File lib/coercive.rb, line 145
def member(set)
  ->(input) do
    fail Coercive::Error.new("not_valid") unless set.include?(input)

    input
  end
end
optional() click to toggle source

Public DSL: Return a generator function for a “optional” fetch function. Used when declaring an attribute. See documentation for attr_fetch_fns.

The fetcher will store the attribute if it is present.

# File lib/coercive.rb, line 362
def optional
  ->(name) do
    ->(input, fetched) do
      fetched[name] = input[name] if input.key?(name)
    end
  end
end
required() click to toggle source

Public DSL: Return a generator function for a “required” fetch function. Used when declaring an attribute. See documentation for attr_fetch_fns.

The fetcher will store the present attribute or raise a Coercive::Error.

# File lib/coercive.rb, line 348
def required
  ->(name) do
    ->(input, fetched) do
      fail Coercive::Error.new("not_present") unless input.key?(name)

      fetched[name] = input[name]
    end
  end
end
string(min: nil, max: nil, pattern: nil) click to toggle source

Public DSL: Return a coerce function to coerce input to a String. Used when declaring an attribute. See documentation for attr_coerce_fns.

min - if given, restrict the minimum size of the input String. max - if given, restrict the maximum size of the input String. pattern - if given, enforce that the input String matches the pattern.

# File lib/coercive.rb, line 222
def string(min: nil, max: nil, pattern: nil)
  ->(input) do
    input = begin
              String(input)
            rescue TypeError
              fail Coercive::Error.new("not_valid")
            end

    if min && min > 0
      fail Coercive::Error.new("is_empty")  if input.empty?
      fail Coercive::Error.new("too_short") if input.bytesize < min
    end

    if max && input.bytesize > max
      fail Coercive::Error.new("too_long")
    end

    if pattern && !pattern.match(input)
      fail Coercive::Error.new("not_valid")
    end

    input
  end
end
uri(*args) click to toggle source

Public DSL: See Coercive::URI.coerce_fn

# File lib/coercive.rb, line 340
def uri(*args)
  Coercive::URI.coerce_fn(*args)
end