Class: LazyMapper

Inherits:
Object
  • Object
show all
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     => ->(o) { o },
  String     => ->(s) { s.to_s },
  Integer    => ->(i) { i.to_i },
  BigDecimal => ->(d) { d.to_d },
  Float      => ->(f) { f.to_f },
  Symbol     => ->(s) { s.to_sym },
  Hash       => ->(h) { h.to_h },
  Time       => Time.method(:iso8601),
  Date       => Date.method(:parse),
  URI        => URI.method(:parse)
}.freeze
DEFAULT_VALUES =

Default values for primitive 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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(values = {}) ⇒ LazyMapper

::new

Create a new instance by giving a Hash of attribues.

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


101
102
103
104
105
106
107
# File 'lib/lazy_mapper.rb', line 101

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

Instance Attribute Details

#mappersObject



70
71
72
# File 'lib/lazy_mapper.rb', line 70

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

Class Method Details

.attributesObject



136
137
138
# File 'lib/lazy_mapper.rb', line 136

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

.default_value_for(type, value) ⇒ Object



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

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

.default_valuesObject



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

def self.default_values
  @default_values ||= DEFAULT_VALUES
end

.from_json(json, mappers: {}) ⇒ Object

::from_json

Create a new instance by giving a Hash of unmapped attributes.

The keys in the Hash are assumed to be camelCased strings.

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" }
  ]


127
128
129
130
131
132
133
134
# File 'lib/lazy_mapper.rb', line 127

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



64
65
66
67
# File 'lib/lazy_mapper.rb', line 64

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

::is

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


223
224
225
# File 'lib/lazy_mapper.rb', line 223

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

::many

Wraps a collection

Arguments

name - The name of the attribue

type - The type of the elemnts in the collection. If an element is

already of that type, the mapper is bypassed for that element.

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 the elements in the wrapped

array. Must respond to +#call+. 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


259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/lazy_mapper.rb', line 259

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



56
57
58
# File 'lib/lazy_mapper.rb', line 56

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

.mappersObject



60
61
62
# File 'lib/lazy_mapper.rb', line 60

def self.mappers
  @mappers ||= DEFAULT_MAPPINGS
end

.one(name, type, from: map_name(name), allow_nil: true, **args) ⇒ Object

::one

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. Must be

a Callable. 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


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/lazy_mapper.rb', line 174

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



280
281
282
# File 'lib/lazy_mapper.rb', line 280

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

#inspectObject



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/lazy_mapper.rb', line 284

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