219 lines
6.7 KiB
Ruby
219 lines
6.7 KiB
Ruby
module Hashie
|
|
class CoercionError < StandardError
|
|
def initialize(key, value, into, message)
|
|
super("Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{message}")
|
|
end
|
|
end
|
|
|
|
module Extensions
|
|
module Coercion
|
|
CORE_TYPES = {
|
|
Integer => :to_i,
|
|
Float => :to_f,
|
|
Complex => :to_c,
|
|
Rational => :to_r,
|
|
String => :to_s,
|
|
Symbol => :to_sym
|
|
}.freeze
|
|
|
|
ABSTRACT_CORE_TYPES =
|
|
{ Numeric => [Integer, Float, Complex, Rational] }
|
|
|
|
def self.included(base)
|
|
base.send :include, InstanceMethods
|
|
base.extend ClassMethods
|
|
unless base.method_defined?(:set_value_without_coercion)
|
|
base.send :alias_method, :set_value_without_coercion, :[]=
|
|
end
|
|
base.send :alias_method, :[]=, :set_value_with_coercion
|
|
end
|
|
|
|
module InstanceMethods
|
|
def set_value_with_coercion(key, value)
|
|
into = self.class.key_coercion(key) || self.class.value_coercion(value)
|
|
|
|
unless value.nil? || into.nil?
|
|
begin
|
|
value = self.class.fetch_coercion(into).call(value)
|
|
rescue NoMethodError, TypeError => e
|
|
raise CoercionError.new(key, value, into, e.message)
|
|
end
|
|
end
|
|
|
|
set_value_without_coercion(key, value)
|
|
end
|
|
|
|
def custom_writer(key, value, _convert = true)
|
|
self[key] = value
|
|
end
|
|
|
|
def replace(other_hash)
|
|
(keys - other_hash.keys).each { |key| delete(key) }
|
|
other_hash.each { |key, value| self[key] = value }
|
|
self
|
|
end
|
|
end
|
|
|
|
module ClassMethods
|
|
attr_writer :key_coercions
|
|
protected :key_coercions=
|
|
|
|
# Set up a coercion rule such that any time the specified
|
|
# key is set it will be coerced into the specified class.
|
|
# Coercion will occur by first attempting to call Class.coerce
|
|
# and then by calling Class.new with the value as an argument
|
|
# in either case.
|
|
#
|
|
# @param [Object] key the key or array of keys you would like to be coerced.
|
|
# @param [Class] into the class into which you want the key(s) coerced.
|
|
#
|
|
# @example Coerce a "user" subhash into a User object
|
|
# class Tweet < Hash
|
|
# include Hashie::Extensions::Coercion
|
|
# coerce_key :user, User
|
|
# end
|
|
def coerce_key(*attrs)
|
|
into = attrs.pop
|
|
attrs.each { |key| key_coercions[key] = into }
|
|
end
|
|
|
|
alias coerce_keys coerce_key
|
|
|
|
# Returns a hash of any existing key coercions.
|
|
def key_coercions
|
|
@key_coercions ||= {}
|
|
end
|
|
|
|
# Returns the specific key coercion for the specified key,
|
|
# if one exists.
|
|
def key_coercion(key)
|
|
key_coercions[key.to_sym]
|
|
end
|
|
|
|
# Set up a coercion rule such that any time a value of the
|
|
# specified type is set it will be coerced into the specified
|
|
# class.
|
|
#
|
|
# @param [Class] from the type you would like coerced.
|
|
# @param [Class] into the class into which you would like the value coerced.
|
|
# @option options [Boolean] :strict (true) whether use exact source class
|
|
# only or include ancestors
|
|
#
|
|
# @example Coerce all hashes into this special type of hash
|
|
# class SpecialHash < Hash
|
|
# include Hashie::Extensions::Coercion
|
|
# coerce_value Hash, SpecialHash
|
|
#
|
|
# def initialize(hash = {})
|
|
# super
|
|
# hash.each_pair do |k,v|
|
|
# self[k] = v
|
|
# end
|
|
# end
|
|
# end
|
|
def coerce_value(from, into, options = {})
|
|
options = { strict: true }.merge(options)
|
|
|
|
if ABSTRACT_CORE_TYPES.key? from
|
|
ABSTRACT_CORE_TYPES[from].each do |type|
|
|
coerce_value type, into, options
|
|
end
|
|
end
|
|
|
|
if options[:strict]
|
|
strict_value_coercions[from] = into
|
|
else
|
|
while from.superclass && from.superclass != Object
|
|
lenient_value_coercions[from] = into
|
|
from = from.superclass
|
|
end
|
|
end
|
|
end
|
|
|
|
# Return all value coercions that have the :strict rule as true.
|
|
def strict_value_coercions
|
|
@strict_value_coercions ||= {}
|
|
end
|
|
|
|
# Return all value coercions that have the :strict rule as false.
|
|
def lenient_value_coercions
|
|
@lenient_value_coercions ||= {}
|
|
end
|
|
|
|
# Fetch the value coercion, if any, for the specified object.
|
|
def value_coercion(value)
|
|
from = value.class
|
|
strict_value_coercions[from] || lenient_value_coercions[from]
|
|
end
|
|
|
|
def fetch_coercion(type)
|
|
return type if type.is_a? Proc
|
|
coercion_cache[type]
|
|
end
|
|
|
|
def coercion_cache
|
|
@coercion_cache ||= ::Hash.new do |hash, type|
|
|
hash[type] = build_coercion(type)
|
|
end
|
|
end
|
|
|
|
def build_coercion(type)
|
|
if type.is_a? Enumerable
|
|
if type.class == ::Hash
|
|
type, key_type, value_type = type.class, *type.first
|
|
build_hash_coercion(type, key_type, value_type)
|
|
else
|
|
value_type = type.first
|
|
type = type.class
|
|
build_container_coercion(type, value_type)
|
|
end
|
|
elsif CORE_TYPES.key? type
|
|
build_core_type_coercion(type)
|
|
elsif type.respond_to? :coerce
|
|
lambda do |value|
|
|
return value if value.is_a? type
|
|
type.coerce(value)
|
|
end
|
|
elsif type.respond_to? :new
|
|
lambda do |value|
|
|
return value if value.is_a? type
|
|
type.new(value)
|
|
end
|
|
else
|
|
raise TypeError, "#{type} is not a coercable type"
|
|
end
|
|
end
|
|
|
|
def build_hash_coercion(type, key_type, value_type)
|
|
key_coerce = fetch_coercion(key_type)
|
|
value_coerce = fetch_coercion(value_type)
|
|
lambda do |value|
|
|
type[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
|
|
end
|
|
end
|
|
|
|
def build_container_coercion(type, value_type)
|
|
value_coerce = fetch_coercion(value_type)
|
|
lambda do |value|
|
|
type.new(value.map { |v| value_coerce.call(v) })
|
|
end
|
|
end
|
|
|
|
def build_core_type_coercion(type)
|
|
name = CORE_TYPES[type]
|
|
lambda do |value|
|
|
return value if value.is_a? type
|
|
return value.send(name)
|
|
end
|
|
end
|
|
|
|
def inherited(klass)
|
|
super
|
|
|
|
klass.key_coercions = key_coercions.dup
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|