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     => :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

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

#mappersObject



79
80
81
# File 'lib/lazy_mapper.rb', line 79

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

Class Method Details

.attributesObject



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_valuesObject



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

.mappersObject



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

#inspectObject



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