FlatMap is a flexible tool for mapping a complex, deeply nested object graph into a mapper object with all mapped attributes accessible in a plain way.
gem "HornsAndHooves-flat_map", require: "flat_map"
FlatMap mappers are designed to provide complex set of data, distributed over associated AR models, in the simple form of a plain hash. They accept a plain hash of the same format and distribute its values over deeply nested AR models. To achieve this goal, Mapper uses three major concepts: Mappings, Mountings and Traits.
Mappings are defined view Mapper.map
method. They represent a simple one-to-one
relation between target attribute and a mapper, extended by additional features
for convenience. The best way to show how they work is by example:
class CustomerMapper < FlatMap::Mapper
# When there is no need to rename attributes, they can be passed as array:
map :first_name, :last_name
# When hash is used, it will map field name to attribute name:
map :dob => :date_of_birth
# Also, additional options can be used:
map :name_suffix, :format => :enum
map :password, :reader => false, :writer => :assign_password
# Or you can combine all definitions together if they all are common:
map :first_name, :last_name,
:dob => :date_of_birth,
:suffix => :name_suffix,
:reader => :my_custom_reader
end
When mappings are defined, one can read and write values using them:
mapper = CustomerMapper.find(1)
mapper.read # => {:first_name => 'John', :last_name => 'Smith', :dob => '02/01/1970'}
mapper.write(params) # will assign same-looking hash of arguments
Following options may be used when defining mappings:
:format
Allows to additionally process output value on reading it. All formats are defined withinFlatMap::Mapping::Reader::Formatted::Formats
and specify the actual output of the mapping:reader
Allows you to manually control reader value of a mapping, or a group of mappings listed on definition. When String or Symbol is used, will call a method, defined by mapper class, and pass mapping object to it. When lambda is used, mapper's target (the model) will be passed to it.:writer
Just like with the :reader option, allows to control how value is assigned (written). Works the same way as :reader does, but additionally value is sent to both mapper method and lambda.:multiparam
If used, multiparam attributes will be extracted from params, when those are passed for writing. Class should be passed as a value for this option. Object of this class will be initialized with the arguments extracted from params hash.
Mappers may be mounted on top of each other. This ability allows host mapper to gain all the mappings of the mounted mapper, thus providing more information for external usage (both reading and writing). Usually, target for mounted mapper may be obtained from association of target of the host mapper itself, but may be defined manually.
class CustomerMapper < FlatMap::Mapper
map :first_name, :last_name
end
class CustomerAccountMapper < FlatMap::Mapper
map :source, :brand, :format => :enum
mount :customer
end
mapper = CustomerAccountMapper.find(1)
mapper.read # => {:first_name => 'John', :last_name => 'Smith', :source => nil, :brand => 'FTW'}
mapper.write(params) # Will assign params for both CustomerAccount and Customer records
The following options may be used when mounting a mapper:
:mapper_class
Specifies mapper class if it cannot be determined from mounting itself.:mapper_class_name
Alternate string form of class name instead of mapper_class.:target
Allows to manually specify target for the new mapper. May be oject or lambda with arity of one that accepts host mapper target as argument. Comes in handy when target cannot be obviously detected or requires additional setup:mount :title, :target => lambda{ |customer| customer.title_customers.build.build_title }
:traits
Specifies list of traits to be used by mounted mapper:suffix
Specifies the suffix that will be appended to all mappings and mountings of mapper, as well as mapper name itself.
Traits allow mappers to encapsulate named sets of additional definitions, and use them optionally on mapper initialization. Everything that can be defined within the mapper may be defined within the trait. In fact, from the implementation perspective traits are mappers themselves that are mounted on the host mapper.
class CustomerAccountMapper < FlatMap::Mapper
map :brand, :format => :enum
trait :with_email do
map :source, :format => :enum
mount :email_address
trait :with_email_phones_residence do
mount :customer, :traits => [:with_phone_numbers, :with_residence]
end
end
end
CustomerAccountMapper.find(1).read # => {:brand => 'TLP'}
CustomerAccountMapper.find(1, :with_email).read # => {:brand => 'TLP', :source => nil, :email_address => '[email protected]'}
CustomerAccountMapper.find(1, :with_email_phone_residence).read # => :brand, :source, :email_address, phone numbers,
#:residence attributes - all will be available for reading and writing in plain hash
When mounting a mapper, one can pass an optional block. This block is used as an extension for a mounted mapper and acts as an anonymous trait. For example:
class CustomerAccountMapper < FlatMap::Mapper
mount :customer do
map :dob => :date_of_birth, :format => :i18n_l
validates_presence_of :dob
mount :unique_identifier
validates_acceptance_of :mandatory_agreement, :message => "You must check this box to continue"
end
end
FlatMap::Mapper
includes ActiveModel::Validations
module, allowing each model to
perform its own validation routines before trying to save its target (which is usually AR model). Mapper
validation is very handy when mappers are used with Rails forms, since there no need to lookup for a
deeply nested errors hash of the AR models to extract error messages. Mapper validations will attach
messages to mapping names.
Mapper validations become even more useful when used within traits, providing way of very flexible validation sets.
Since mappers include ActiveModel::Validation
, they already support ActiveSupport's callbacks.
Additionally, :save
callbacks have been defined (i.e. there have been define_callbacks :save
call for FlatMap::Mapper
). This allows you to control flow of mapper saving:
set_callback :save, :before, :set_model_validation
def set_model_validation
target.use_validation :some_themis_validation
end
In some cases, it is required to omit mapper processing after it has been created within mounting chain. If
skip!
method is called on mapper, it will return true
for valid?
and save
method calls without performing any other operations. For example:
class CustomerMapper < FlatMap::Mapper
# some definitions
trait :product_selection do
attr_reader :selected_product_id
mount :product
set_callback :validate, :before, :ignore_new_product
def ignore_new_product
mounting(:product).skip! if product_selected?
end
# some more definitions
end
end
All mappers have the ability to read and write values via method calls:
mapper.read[:first_name] # => John
mapper.first_name # => 'John'
mapper.last_name = 'Smith'
rake spec
Copyright (c) 2014 HornsAndHooves.
Copyright (c) 2013 TMX Credit.