lib/lazy_mapper.rb in lazy_mapper-0.2.1 vs lib/lazy_mapper.rb in lazy_mapper-0.3.0
- old
+ new
@@ -1,25 +1,23 @@
require 'bigdecimal'
require 'bigdecimal/util'
require 'time'
#
-# Wraps a JSON object and lazily maps its attributes to domain objects
-# using either a set of default mappers (for Ruby's built-in types), or
-# custom mappers specified by the client.
+# 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.
#
-# The mapped values are memoized.
-#
# 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
#
-
class LazyMapper
#
# Default mappings for built-in types
#
@@ -68,37 +66,45 @@
def self.mappers
@mappers ||= DEFAULT_MAPPINGS
end
+ def self.attributes
+ @attributes ||= {}
+ end
+
def self.inherited(klass)
- klass.instance_variable_set IVAR[:mappers], self.mappers.dup
- klass.instance_variable_set IVAR[:default_values], self.default_values.dup
+ # Make the subclass "inherit" the values of these class instance variables
+ %i[
+ mappers
+ default_values
+ attributes
+ ].each do |s|
+ klass.instance_variable_set IVAR[s], self.send(s).dup
+ end
end
-
def mappers
@mappers ||= self.class.mappers
end
- IVAR = -> name {
+ IVAR = lambda { |name| # :nodoc:
name_as_str = name.to_s
- if name_as_str[-1] == '?'
- name_as_str = name_as_str[0...-1]
- end
+ name_as_str = name_as_str[0...-1] if name_as_str[-1] == '?'
('@' + name_as_str).freeze
}
- WRITER = -> name { (name.to_s.gsub('?', '') + '=').to_sym }
+ WRITER = -> name { (name.to_s.delete('?') + '=').to_sym }
#
# Creates a new instance by giving a Hash of attribues.
#
# Attribute values are type checked according to how they were defined.
- # If a value has the wrong type, a `TypeError` is raised.
#
+ # 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"),
@@ -108,11 +114,10 @@
# User.new("id" => 66, "name" => "Anders"),
# User.new("id" => 91, "name" => "Kristoffer)
# ]
def initialize(values = {})
- @json = {}
@mappers = {}
values.each do |name, value|
send(WRITER[name], value)
end
end
@@ -122,20 +127,20 @@
#
# The keys in the Hash are assumed to be camelCased strings.
#
# == Arguments
#
- # +json+ - The unmapped data as a Hash(-like object). Must respond to #to_h.
+ # +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_json({
+ # Foo.from({
# "xmlId" => 42,
# "createdAt" => "2015-07-29 14:07:35 +0200",
# "amount" => "$2.00",
# "users" => [
# { "id" => 23, "name" => "Adam" },
@@ -144,21 +149,22 @@
# { "id" => 91, "name" => "Kristoffer" } ]},
# mappers: {
# :amount => -> x { Money.new(x) },
# User => User.method(:new) })
#
- def self.from_json json, mappers: {}
- return nil if json.nil?
- fail TypeError, "#{ json.inspect } is not a Hash" unless json.respond_to? :to_h
+ 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 :json=, json.to_h
+ instance.send :unmapped_data=, unmapped_data.to_h
instance.send :mappers=, mappers
instance
end
- def self.attributes
- @attributes ||= {}
+ def self.from_json *args, &block
+ warn "#{ self }.from_json is deprecated. Use #{ self }.from instead."
+ from *args, &block
end
#
# Defines an attribute and creates a reader and a writer for it.
# The writer verifies the type of it's supplied value.
@@ -199,20 +205,20 @@
}
# Define reader
define_method(name) {
memoize(name, ivar) {
- unmapped_value = json[from]
+ unmapped_value = unmapped_data[from]
mapped_value(name, unmapped_value, type, **args)
}
}
attributes[name] = type
end
#
- # Converts a value to true or false according to its truthyness
+ # Converts a value to +true+ or +false+ according to its truthyness
#
TO_BOOL = -> b { !!b }
#
# Defines an boolean attribute
@@ -249,19 +255,18 @@
#
# +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 JSON object.
+ # +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 wrapped value is not present
- # in the wrapped JSON object.
+ # +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) }
@@ -277,11 +282,11 @@
}
# Define getter
define_method(name) {
memoize(name) {
- unmapped_value = json[from]
+ 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
@@ -294,34 +299,32 @@
#
def add_mapper_for(type, &block)
mappers[type] = block
end
+ def to_h
+ attributes.each_with_object({}) {|(key, _value), h|
+ h[key] = self.send key
+ }
+ end
+
def inspect
@__under_inspection__ ||= 0
return "<#{ self.class.name } ... >" if @__under_inspection__ > 0
@__under_inspection__ += 1
- attributes = self.class.attributes
- if self.class.superclass.respond_to? :attributes
- attributes = self.class.superclass.attributes.merge attributes
- end
- present_attributes = attributes.keys.each_with_object({}) {|name, memo|
+ present_attributes = attributes.keys.each_with_object({}) { |name, memo|
value = self.send name
memo[name] = value unless value.nil?
}
- "<#{ self.class.name } #{ present_attributes.map {|k,v| k.to_s + ': ' + v.inspect }.join(', ') } >"
- res = "<#{ self.class.name } #{ present_attributes.map {|k,v| k.to_s + ': ' + v.inspect }.join(', ') } >"
+ "<#{ self.class.name } #{ present_attributes.map { |k, v| k.to_s + ': ' + v.inspect }.join(', ') } >"
+ res = "<#{ self.class.name } #{ present_attributes.map { |k, v| k.to_s + ': ' + v.inspect }.join(', ') } >"
@__under_inspection__ -= 1
res
end
protected
- def json
- @json ||= {}
- end
-
#
# Defines how to map an attribute name
# to the corresponding name in the unmapped
# JSON object.
#
@@ -331,21 +334,29 @@
CAMELIZE[name]
end
private
- attr_writer :json
+ attr_writer :unmapped_data
attr_writer :mappers
+ def unmapped_data
+ @unmapped_data ||= {}
+ end
+
def mapping_for(name, type)
mappers[name] || mappers[type] || self.class.mappers[type]
end
def default_value(type)
self.class.default_values[type]
end
+ def attributes
+ self.class.attributes
+ end
+
def mapped_value(name, unmapped_value, type, map: mapping_for(name, type), default: default_value(type))
if unmapped_value.nil?
# Duplicate to prevent accidental sharing between instances
default.dup
else
@@ -367,14 +378,13 @@
*all_but_last, last = list
return last if all_but_last.empty?
[ all_but_last.join(separator), last ].join conjunction
end
-
def memoize name, ivar = IVAR[name]
send WRITER[name], yield unless instance_variable_defined?(ivar)
instance_variable_get(ivar)
end
SNAKE_CASE_PATTERN = /(_[a-z])/ # :nodoc:
- CAMELIZE = -> name { name.to_s.gsub(SNAKE_CASE_PATTERN) { |x| x[1].upcase }.gsub('?', '') }
+ CAMELIZE = -> name { name.to_s.gsub(SNAKE_CASE_PATTERN) { |x| x[1].upcase }.delete('?') }
end