Class: LazyMapper
- Inherits:
-
Object
- Object
- LazyMapper
- Defined in:
- lib/lazy_mapper.rb
Overview
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
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 =
:nodoc:
lambda { |name| # :nodoc: name_as_str = name.to_s name_as_str = name_as_str[0...-1] if name_as_str[-1] == '?' ('@' + name_as_str).freeze }
- WRITER =
-> name { (name.to_s.delete('?') + '=').to_sym }
- TO_BOOL =
Converts a value to
trueorfalseaccording 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(unmapped_data, mappers: {}) ⇒ Object
Create a new instance by giving a Hash of unmapped attributes.
-
.from_json(*args, &block) ⇒ Object
:nodoc:.
-
.inherited(klass) ⇒ Object
:nodoc:.
-
.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
- #to_h ⇒ 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.
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)
]
118 119 120 121 122 123 |
# File 'lib/lazy_mapper.rb', line 118 def initialize(values = {}) @mappers = {} values.each do |name, value| send(WRITER[name], value) end end |
Instance Attribute Details
#mappers ⇒ Object
86 87 88 |
# File 'lib/lazy_mapper.rb', line 86 def mappers @mappers ||= self.class.mappers end |
Class Method Details
.attributes ⇒ Object
71 72 73 |
# File 'lib/lazy_mapper.rb', line 71 def self.attributes @attributes ||= {} end |
.default_value_for(type, value) ⇒ Object
Adds (or overrides) a default type for a given type
40 41 42 |
# File 'lib/lazy_mapper.rb', line 40 def self.default_value_for type, value default_values[type] = value end |
.default_values ⇒ Object
44 45 46 |
# File 'lib/lazy_mapper.rb', line 44 def self.default_values @default_values ||= DEFAULT_VALUES end |
.from(unmapped_data, 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
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) })
154 155 156 157 158 159 160 161 |
# File 'lib/lazy_mapper.rb', line 154 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 |
.from_json(*args, &block) ⇒ Object
:nodoc:
163 164 165 166 |
# File 'lib/lazy_mapper.rb', line 163 def self.from_json *args, &block # :nodoc: warn "#{ self }.from_json is deprecated. Use #{ self }.from instead." from *args, &block end |
.inherited(klass) ⇒ Object
:nodoc:
75 76 77 78 79 80 81 82 83 84 |
# File 'lib/lazy_mapper.rb', line 75 def self.inherited klass # :nodoc: # 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 |
.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
245 246 247 |
# File 'lib/lazy_mapper.rb', line 245 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 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
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/lazy_mapper.rb', line 276 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 |
.mapper_for(type, mapper) ⇒ Object
Adds a mapper for a give type
63 64 65 |
# File 'lib/lazy_mapper.rb', line 63 def self.mapper_for(type, mapper) mappers[type] = mapper end |
.mappers ⇒ Object
67 68 69 |
# File 'lib/lazy_mapper.rb', line 67 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
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/lazy_mapper.rb', line 197 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 |
Instance Method Details
#add_mapper_for(type, &block) ⇒ Object
Adds an instance-level type mapper
302 303 304 |
# File 'lib/lazy_mapper.rb', line 302 def add_mapper_for(type, &block) mappers[type] = block end |
#inspect ⇒ Object
312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/lazy_mapper.rb', line 312 def inspect @__under_inspection__ ||= 0 return "<#{ self.class.name } ... >" if @__under_inspection__ > 0 @__under_inspection__ += 1 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 |
#to_h ⇒ Object
306 307 308 309 310 |
# File 'lib/lazy_mapper.rb', line 306 def to_h attributes.each_with_object({}) {|(key, _value), h| h[key] = self.send key } end |