Class: LazyMapper

Inherits:
Object
  • Object
show all
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 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)
  ]


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

#mappersObject



86
87
88
# File 'lib/lazy_mapper.rb', line 86

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

Class Method Details

.attributesObject



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_valuesObject



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

.mappersObject



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

#inspectObject



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_hObject



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