lib/lazy_mapper.rb in lazy_mapper-0.2.0 vs lib/lazy_mapper.rb in lazy_mapper-0.2.1

- old
+ new

@@ -1,10 +1,10 @@ require 'bigdecimal' require 'bigdecimal/util' require 'time' -## +# # 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. @@ -18,43 +18,52 @@ # end # class LazyMapper + # # Default mappings for built-in types + # DEFAULT_MAPPINGS = { - 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 }, + 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 + # + # Adds (or overrides) a default type for a given type + # def self.default_value_for type, value default_values[type] = value end def self.default_values @default_values ||= DEFAULT_VALUES end - - # Default values for primitive types + # + # Default values for built-in value types + # DEFAULT_VALUES = { String => '', Integer => 0, Numeric => 0, Float => 0.0, BigDecimal => BigDecimal.new('0'), Array => [] }.freeze + # + # Adds a mapper for a give type + # def self.mapper_for(type, mapper) mappers[type] = mapper end def self.mappers @@ -80,14 +89,16 @@ ('@' + name_as_str).freeze } WRITER = -> name { (name.to_s.gsub('?', '') + '=').to_sym } - # = ::new # - # Create a new instance by giving a Hash of attribues. + # 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"), @@ -104,27 +115,38 @@ values.each do |name, value| send(WRITER[name], value) end end - # = ::from_json # # 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, + # 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" } - # ] + # { "id" => 91, "name" => "Kristoffer" } ]}, + # mappers: { + # :amount => -> x { Money.new(x) }, + # User => User.method(:new) }) # 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 @@ -135,33 +157,28 @@ def self.attributes @attributes ||= {} end - # = ::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 + # +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 + # +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+. + # +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. + # +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. + # +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 # @@ -190,30 +207,29 @@ } attributes[name] = type end - ## + # # Converts a value to true or false according to its truthyness + # TO_BOOL = -> b { !!b } - # = ::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+. + # 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. + # +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 + # +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? } @@ -224,32 +240,28 @@ one name, [TrueClass, FalseClass], from: from, allow_nil: false, map: map, default: default end singleton_class.send(:alias_method, :has, :is) - - # = ::many # - # Wraps a collection + # Defines a collection attribute # # == Arguments # - # +name+ - The name of the attribue + # +name+ - The name of the attribute # - # +type+ - The type of the elemnts in the collection. If an element is - # already of that type, the mapper is bypassed for that element. + # +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+. + # 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. + # +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. + # in the wrapped JSON object. # # == Example # # class Bar < LazyMapper # many :underlings, Person, from: "serfs", map: ->(p) { Person.new(p) } @@ -275,10 +287,13 @@ end } } end + # + # Adds an instance-level type mapper + # def add_mapper_for(type, &block) mappers[type] = block end def inspect @@ -303,11 +318,11 @@ def json @json ||= {} end - ## + # # Defines how to map an attribute name # to the corresponding name in the unmapped # JSON object. # # Defaults to CAMELIZE @@ -358,8 +373,8 @@ def memoize name, ivar = IVAR[name] send WRITER[name], yield unless instance_variable_defined?(ivar) instance_variable_get(ivar) end - SNAKE_CASE_PATTERN = /(_[a-z])/ + SNAKE_CASE_PATTERN = /(_[a-z])/ # :nodoc: CAMELIZE = -> name { name.to_s.gsub(SNAKE_CASE_PATTERN) { |x| x[1].upcase }.gsub('?', '') } end