class LazyMapper
Wraps a Hash or Hash-like data structure of primitive values and lazily maps its attributes to semantically rich domain objects using either a set of default mappers (for Ruby's built-in value types), or custom mappers which can be added either at the class level or at the instance level.
Example:
class Foo < LazyMapper one :id, Integer, from: 'xmlId' one :created_at, Time one :amount, Money, map: Money.method(:parse) many :users, User, map: ->(u) { User.new(u) } end
Constants
- CAMELIZE
- DEFAULT_MAPPINGS
Default mappings for built-in types
- DEFAULT_VALUES
Default values for built-in value types
- TO_BOOL
Converts a value to
true
orfalse
according to its truthyness- VERSION
- WRITER
Attributes
Public Class Methods
# File lib/lazy_mapper/lazy_mapper.rb, line 41 def self.attributes @attributes ||= {} end
Adds (or overrides) a default type for a given type
# File lib/lazy_mapper/lazy_mapper.rb, line 22 def self.default_value_for type, value default_values[type] = value end
# File lib/lazy_mapper/lazy_mapper.rb, line 26 def self.default_values @default_values ||= DEFAULT_VALUES end
Create a new instance by giving a Hash of unmapped attributes.
The keys in the Hash are assumed to be camelCased strings.
Arguments¶ ↑
unmapped_data
- The unmapped data as a Hash(-like object). Must respond to to_h
. Keys are assumed to be camelCased string
mappers:
- Optional instance-level mappers. Keys can either be classes or symbols corresponding to named attributes.
Example¶ ↑
Foo.from({ "xmlId" => 42, "createdAt" => "2015-07-29 14:07:35 +0200", "amount" => "$2.00", "users" => [ { "id" => 23, "name" => "Adam" }, { "id" => 45, "name" => "Ole" }, { "id" => 66, "name" => "Anders" }, { "id" => 91, "name" => "Kristoffer" } ]}, mappers: { :amount => -> x { Money.new(x) }, User => User.method(:new) })
# File lib/lazy_mapper/lazy_mapper.rb, line 126 def self.from unmapped_data, mappers: {} return nil if unmapped_data.nil? fail TypeError, "#{ unmapped_data.inspect } is not a Hash" unless unmapped_data.respond_to? :to_h instance = new instance.send :unmapped_data=, unmapped_data.to_h instance.send :mappers=, mappers instance end
Defines an boolean attribute
Arguments¶ ↑
name
- The name of the attribue
from:
- Specifies the name of the wrapped value in the JSON object. Defaults to camelCased version of name
.
map:
- Specifies a custom mapper to apply to the wrapped value. Must be a Callable. Defaults to TO_BOOL
if unspecified.
default:
The default value to use if the value is missing. False, if unspecified
Example¶ ↑
class Foo < LazyMapper is :green?, from: "isGreen", map: ->(x) { !x.zero? } # ... end
# File lib/lazy_mapper/lazy_mapper.rb, line 213 def self.is name, from: map_name(name), map: TO_BOOL, default: false one name, [TrueClass, FalseClass], from: from, allow_nil: false, map: map, default: default end
Defines a collection attribute
Arguments¶ ↑
name
- The name of the attribute
type
- The type of the elements in the collection.
from:
- Specifies the name of the wrapped array in the unmapped data. Defaults to camelCased version of name
.
map:
- Specifies a custom mapper to apply to each elements in the wrapped collection. If unspecified, it defaults to the default mapper for the specified type
or simply the identity mapper if no default mapper exists.
default:
- The default value to use, if the unmapped value is missing.
Example¶ ↑
class Bar < LazyMapper many :underlings, Person, from: "serfs", map: ->(p) { Person.new(p) } # ... end
# File lib/lazy_mapper/lazy_mapper.rb, line 244 def self.many(name, type, from: map_name(name), **args) # Define setter define_method(WRITER[name]) { |val| check_type! val, Enumerable, allow_nil: false instance_variable_set(IVAR[name], val) } # Define getter define_method(name) { memoize(name) { unmapped_value = unmapped_data[from] if unmapped_value.is_a? Array unmapped_value.map { |v| mapped_value(name, v, type, **args) } else mapped_value name, unmapped_value, Array, **args end } } attributes[name] = Array end
Defines how to map an attribute name to the corresponding name in the unmapped JSON object.
Defaults to CAMELIZE
# File lib/lazy_mapper/lazy_mapper.rb, line 310 def self.map_name(name) CAMELIZE[name] end
Adds a mapper for a give type
# File lib/lazy_mapper/lazy_mapper.rb, line 33 def self.mapper_for(type, mapper) mappers[type] = mapper end
# File lib/lazy_mapper/lazy_mapper.rb, line 37 def self.mappers @mappers ||= DEFAULT_MAPPINGS end
Creates a new instance by giving a Hash of attribues.
Attribute values are type checked according to how they were defined.
Fails with TypeError
, if a value doesn't have the expected type.
Example¶ ↑
Foo.new :id => 42, :created_at => Time.parse("2015-07-29 14:07:35 +0200"), :amount => Money.parse("$2.00"), :users => [ User.new("id" => 23, "name" => "Adam"), User.new("id" => 45, "name" => "Ole"), User.new("id" => 66, "name" => "Anders"), User.new("id" => 91, "name" => "Kristoffer) ]
# File lib/lazy_mapper/lazy_mapper.rb, line 90 def initialize(values = {}) @mappers = {} values.each do |name, value| send(WRITER[name], value) end end
Defines an attribute and creates a reader and a writer for it. The writer verifies the type of it's supplied value.
Arguments¶ ↑
name
- The name of the attribue
type
- The type of the attribute. If the wrapped value is already of that type, the mapper is bypassed. If the type is allowed be one of several, use an Array to to specify which ones
from:
- Specifies the name of the wrapped value in the JSON object. Defaults to camelCased version of name
.
map:
- Specifies a custom mapper to apply to the wrapped value. If unspecified, it defaults to the default mapper for the specified type
or simply the identity mapper if no default mapper exists.
default:
- The default value to use, if the wrapped value is not present in the wrapped JSON object.
allow_nil:
- If true, allows the mapped value to be nil. Defaults to true.
Example¶ ↑
class Foo < LazyMapper one :boss, Person, from: "supervisor", map: ->(p) { Person.new(p) } one :weapon, [BladedWeapon, Firearm], default: Sixshooter.new # ... end
# File lib/lazy_mapper/lazy_mapper.rb, line 165 def self.one(name, type, from: map_name(name), allow_nil: true, **args) ivar = IVAR[name] # Define writer define_method(WRITER[name]) { |val| check_type! val, type, allow_nil: allow_nil instance_variable_set(ivar, val) } # Define reader define_method(name) { memoize(name, ivar) { unmapped_value = unmapped_data[from] mapped_value(name, unmapped_value, type, **args) } } attributes[name] = type end
Public Instance Methods
Adds an instance-level type mapper
# File lib/lazy_mapper/lazy_mapper.rb, line 270 def add_mapper_for(type, &block) mappers[type] = block end
# File lib/lazy_mapper/lazy_mapper.rb, line 286 def inspect @__under_inspection__ ||= 0 return "<#{ self.class.name } ... >" if @__under_inspection__.positive? @__under_inspection__ += 1 present_attributes = attributes.keys.each_with_object({}) { |name, memo| ivar = IVAR[name] next unless self.instance_variable_defined? ivar memo[name] = self.instance_variable_get ivar } res = "<#{ self.class.name } #{ present_attributes.map { |k, v| k.to_s + ': ' + v.inspect }.join(', ') } >" @__under_inspection__ -= 1 res end
# File lib/lazy_mapper/lazy_mapper.rb, line 58 def mappers @mappers ||= self.class.mappers end
Returns a Hash
with keys corresponding to attribute names, and values corresponding to mapped attribute values.
Note: This will eagerly map all attributes that haven't yet been mapped
# File lib/lazy_mapper/lazy_mapper.rb, line 280 def to_h attributes.each_with_object({}) { |(key, _value), h| h[key] = self.send key } end
Private Instance Methods
# File lib/lazy_mapper/lazy_mapper.rb, line 331 def attributes self.class.attributes end
# File lib/lazy_mapper/lazy_mapper.rb, line 348 def check_type! value, type, allow_nil: permitted_types = allow_nil ? Array(type) + [ NilClass ] : Array(type) return if permitted_types.any? value.method(:is_a?) fail TypeError.new "#{ self.class.name }: "\ "#{ value.inspect } is a #{ value.class } "\ "but was supposed to be a #{ humanize_list permitted_types, conjunction: ' or ' }" end
# File lib/lazy_mapper/lazy_mapper.rb, line 327 def default_value(type) self.class.default_values[type] end
- 1,2,3
-
-> “1, 2 and 3”
- 1, 2
-
-> “1 and 2”
- 1
-
-> “1”
# File lib/lazy_mapper/lazy_mapper.rb, line 360 def humanize_list terms, separator: ', ', conjunction: ' and ' *all_but_last, last = terms return last if all_but_last.empty? [ all_but_last.join(separator), last ].join conjunction end
# File lib/lazy_mapper/lazy_mapper.rb, line 335 def mapped_value(name, unmapped_value, type, map: mapping_for(name, type), default: default_value(type)) return default.dup if unmapped_value.nil? # Duplicate to prevent accidental sharing between instances if map.nil? fail ArgumentError, "missing mapper for #{ name } (#{ type }). "\ "Unmapped value: #{ unmapped_value.inspect }" end return map.call(unmapped_value, self) if map.arity > 1 map.call(unmapped_value) end
# File lib/lazy_mapper/lazy_mapper.rb, line 323 def mapping_for(name, type) mappers[name] || mappers[type] || self.class.mappers[type] end
# File lib/lazy_mapper/lazy_mapper.rb, line 367 def memoize name, ivar = IVAR[name] send WRITER[name], yield unless instance_variable_defined?(ivar) instance_variable_get(ivar) end
# File lib/lazy_mapper/lazy_mapper.rb, line 319 def unmapped_data @unmapped_data ||= {} end