Class: LazyMapper
- Inherits:
-
Object
- Object
- LazyMapper
- Defined in:
- lib/lazy_mapper.rb
Overview
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.
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
Constant Summary collapse
- DEFAULT_MAPPINGS =
Default mappings for built-in types
{ Object => :itself.to_proc, String => :to_s.to_proc, Integer => :to_i.to_proc, BigDecimal => :to_d.to_proc, Float => :to_f.to_proc, Symbol => :to_sym.to_proc, Hash => :to_h.to_proc, Time => Time.method(:iso8601), Date => Date.method(:parse), URI => URI.method(:parse) }.freeze
- DEFAULT_VALUES =
Default values for built-in value types
{ String => '', Integer => 0, Numeric => 0, Float => 0.0, BigDecimal => BigDecimal.new('0'), Array => [] }.freeze
- IVAR =
-> name { name_as_str = name.to_s if name_as_str[-1] == '?' name_as_str = name_as_str[0...-1] end ('@' + name_as_str).freeze }
- WRITER =
-> name { (name.to_s.gsub('?', '') + '=').to_sym }
- TO_BOOL =
Converts a value to true or false according to its truthyness
-> b { !!b }
Instance Attribute Summary collapse
- #mappers ⇒ Object readonly
Class Method Summary collapse
- .attributes ⇒ Object
-
.default_value_for(type, value) ⇒ Object
Adds (or overrides) a default type for a given type.
- .default_values ⇒ Object
-
.from_json(json, mappers: {}) ⇒ Object
Create a new instance by giving a Hash of unmapped attributes.
- .inherited(klass) ⇒ Object
-
.is(name, from: map_name(name), map: TO_BOOL, default: false) ⇒ Object
Defines an boolean attribute.
-
.many(name, type, from: map_name(name), **args) ⇒ Object
Defines a collection attribute.
-
.mapper_for(type, mapper) ⇒ Object
Adds a mapper for a give type.
- .mappers ⇒ Object
-
.one(name, type, from: map_name(name), allow_nil: true, **args) ⇒ Object
Defines an attribute and creates a reader and a writer for it.
Instance Method Summary collapse
-
#add_mapper_for(type, &block) ⇒ Object
Adds an instance-level type mapper.
-
#initialize(values = {}) ⇒ LazyMapper
constructor
Creates a new instance by giving a Hash of attribues.
- #inspect ⇒ Object
Constructor Details
#initialize(values = {}) ⇒ LazyMapper
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.
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)
]
112 113 114 115 116 117 118 |
# File 'lib/lazy_mapper.rb', line 112 def initialize(values = {}) @json = {} @mappers = {} values.each do |name, value| send(WRITER[name], value) end end |
Instance Attribute Details
#mappers ⇒ Object
79 80 81 |
# File 'lib/lazy_mapper.rb', line 79 def mappers @mappers ||= self.class.mappers end |
Class Method Details
.attributes ⇒ Object
158 159 160 |
# File 'lib/lazy_mapper.rb', line 158 def self.attributes @attributes ||= {} end |
.default_value_for(type, value) ⇒ Object
Adds (or overrides) a default type for a given type
42 43 44 |
# File 'lib/lazy_mapper.rb', line 42 def self.default_value_for type, value default_values[type] = value end |
.default_values ⇒ Object
46 47 48 |
# File 'lib/lazy_mapper.rb', line 46 def self.default_values @default_values ||= DEFAULT_VALUES end |
.from_json(json, mappers: {}) ⇒ Object
Create a new instance by giving a Hash of unmapped attributes.
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. 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({
"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) })
149 150 151 152 153 154 155 156 |
# File 'lib/lazy_mapper.rb', line 149 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 instance = new instance.send :json=, json.to_h instance.send :mappers=, mappers instance end |
.inherited(klass) ⇒ Object
73 74 75 76 |
# File 'lib/lazy_mapper.rb', line 73 def self.inherited(klass) klass.instance_variable_set IVAR[:mappers], self.mappers.dup klass.instance_variable_set IVAR[:default_values], self.default_values.dup end |
.is(name, from: map_name(name), map: TO_BOOL, default: false) ⇒ Object
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
239 240 241 |
# File 'lib/lazy_mapper.rb', line 239 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 |
.many(name, type, from: map_name(name), **args) ⇒ Object
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 JSON object. 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.
Example
class Bar < LazyMapper
many :underlings, Person, from: "serfs", map: ->(p) { Person.new(p) }
# ...
end
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/lazy_mapper.rb', line 271 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 = json[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 } } end |
.mapper_for(type, mapper) ⇒ Object
Adds a mapper for a give type
65 66 67 |
# File 'lib/lazy_mapper.rb', line 65 def self.mapper_for(type, mapper) mappers[type] = mapper end |
.mappers ⇒ Object
69 70 71 |
# File 'lib/lazy_mapper.rb', line 69 def self.mappers @mappers ||= DEFAULT_MAPPINGS end |
.one(name, type, from: map_name(name), allow_nil: true, **args) ⇒ Object
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
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/lazy_mapper.rb', line 191 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 = json[from] mapped_value(name, unmapped_value, type, **args) } } attributes[name] = type end |
Instance Method Details
#add_mapper_for(type, &block) ⇒ Object
Adds an instance-level type mapper
295 296 297 |
# File 'lib/lazy_mapper.rb', line 295 def add_mapper_for(type, &block) mappers[type] = block end |
#inspect ⇒ Object
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/lazy_mapper.rb', line 299 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| 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(', ') } >" @__under_inspection__ -= 1 res end |