Class: LazyMapper

Inherits:
Object
  • Object
show all
Defined in:
lib/lazy_mapper/lazy_mapper.rb,
lib/lazy_mapper.rb,
lib/lazy_mapper/version.rb,
lib/lazy_mapper/defaults.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

VERSION =
'0.4.1'
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(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 true or false according to its truthyness

-> b { !!b }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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)
  ]


90
91
92
93
94
95
# File 'lib/lazy_mapper/lazy_mapper.rb', line 90

def initialize(values = {})
  @mappers = {}
  values.each do |name, value|
    send(WRITER[name], value)
  end
end

Instance Attribute Details

#mappersObject



58
59
60
# File 'lib/lazy_mapper/lazy_mapper.rb', line 58

def mappers
  @mappers ||= self.class.mappers
end

Class Method Details

.attributesObject



41
42
43
# File 'lib/lazy_mapper/lazy_mapper.rb', line 41

def self.attributes
  @attributes ||= {}
end

.default_value_for(type, value) ⇒ Object

Adds (or overrides) a default type for a given type



22
23
24
# File 'lib/lazy_mapper/lazy_mapper.rb', line 22

def self.default_value_for type, value
  default_values[type] = value
end

.default_valuesObject



26
27
28
# File 'lib/lazy_mapper/lazy_mapper.rb', line 26

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) })


126
127
128
129
130
131
132
133
134
# 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

.inherited(klass) ⇒ Object

:nodoc:



45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/lazy_mapper/lazy_mapper.rb', line 45

def self.inherited klass # :nodoc:
  # Make the subclass "inherit" the values of these class instance variables
  %i[
    default_values
    attributes
  ].each do |s|
    klass.instance_variable_set IVAR[s], self.send(s).dup
  end

  # If a mapper is does not exist in the derived class, look it up in this class
  klass.instance_variable_set('@mappers', Hash.new { |_mappers, type| mappers[type] })
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


213
214
215
# 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

.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


244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# 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

.map_name(name) ⇒ Object

Defines how to map an attribute name to the corresponding name in the unmapped JSON object.

Defaults to CAMELIZE



310
311
312
# File 'lib/lazy_mapper/lazy_mapper.rb', line 310

def self.map_name(name)
  CAMELIZE[name]
end

.mapper_for(type, mapper) ⇒ Object

Adds a mapper for a give type



33
34
35
# File 'lib/lazy_mapper/lazy_mapper.rb', line 33

def self.mapper_for(type, mapper)
  mappers[type] = mapper
end

.mappersObject



37
38
39
# File 'lib/lazy_mapper/lazy_mapper.rb', line 37

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


165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# 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

Instance Method Details

#add_mapper_for(type, &block) ⇒ Object

Adds an instance-level type mapper



270
271
272
# File 'lib/lazy_mapper/lazy_mapper.rb', line 270

def add_mapper_for(type, &block)
  mappers[type] = block
end

#inspectObject



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# 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

#to_hObject

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



280
281
282
283
284
# 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