Flatter::Extensions
A set of extensions to be used with Flatter gem.
Installation
Add this line to your application's Gemfile:
gem 'flatter-extensions'
And then execute:
$ bundle
Or install it yourself as:
$ gem install flatter-extensions
Usage
All extensions can be included at a runtime using Flatter.use
method. Usually,
this is done in your app initializer with a Flatter.configure
, like so:
Flatter.configure do |f|
f.use :order
f.use :skipping
f.use :active_record
end
Bellow is a list of available extensions with description.
Multiparam
Flatter.use :multiparam
Allows you to define multiparam mappings by using :multiparam
option to mapping.
Works pretty much like Rails
multiparam attribute assignment:
class PersonMapper < Flatter::Mapper
map :first_name, :last_name
map dob: :date_of_birth, multiparam: Date
end
# ...
mapper = PersonMapper.new(person)
mapper.write(first_name: 'John', 'dob(1i)' => '2015', 'dob(2i)' => '01', 'dob(3i)' => '15')
person.date_of_birth # => Thu, 15 Jan 2015
Skipping
Flatter.use :skipping
Allows to skip mappers (mountings) from the processing chain by calling skip!
method on a particular mapper. This is usually used in before callbacks to
avoid processing specific mappers if they fail to match some processing condition.
For example:
class Person < ActiveRecord::Base
has_many :phones
end
class PhoneMapper < Flatter::Mapper
map phone_number: :number
validates_presence_of :phone_number
end
class PersonMapper < Flatter::Mapper
mount :phone, foreign_key: :person_id
set_callback :validate, :before, :skip_empty_phone
def skip_empty_phone
# avoids validation and creation of new phone number
# if provided `phone_number` field was blank.
mounting(:phone).skip! if phone_number.blank?
end
end
Starting from version 0.2.1
, there are :skip_if
and :reject_if
options
for mounted mappers. First one is used for skipping mounting if passed condition
is evaluated to non-falsy value. Second one is used to reject specific sets of
params before they are passed for collection processing.
class EmployeeMapper < Flatter::Mapper
map :first_name, :last_name
mount :department, skip_if: -> { department_name.blank? }
mount :projects, reject_if: ->(params){ params[:project_name].blank? }
end
Order
Flatter.use :order
Allows you to manually control processing order of mappers and their mountings.
Provides :index
option for mountings, which can be either a Number, which means
order for both validation and saving routines, or a hash like {validate: -1, save: 2}
.
By default all mappers have index of 0
and processed from top to bottom.
This extension will be very handy when using with :active_record
extension, since
all targets (records) are saved without callbacks and validation, which means
you won't have such things as associations autosave. That means that to properly
save records with foreign key dependencies, you have to do it in proper order.
For example, in following scenario we use PersonMapper
to manage people. If
additionally email
was supplied, User
record will be created, and Person
record will be associated with it. This means that we need to skip User before
validation if there was no email provided, but save it before person mounter.
class Person < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :person
end
class UserMapper < Flatter::Mapper
map :email
validates_presence_of :email
end
class PersonMapper < Flatter::Mapper
trait :management do
mount :user, index: {save: -1}, mounter_foreign_key: :user_id
set_callback :validate, :before, :skip_user
def skip_user
mounting(:user).skip! if email.blank?
end
end
end
ActiveRecord
Flatter.use :active_record
Probably, the most important extension, the reason why Flatter (former FlatMap) was initially built. This extension allows you to build mappers that will handle complexity of ActiveRecord associations in a graph of related records to provide a single mapper object with plain hash of attributes that can be used to render a form, used as a form object itself to distribute form params among records, or used in your API, encapsulating processing logic with reusable traits.
When using :active_record
extension, you should keep in mind following things:
Mounted target from association
If mapper's target is an ActiveRecord::Base
object, target for mounted mappers
will be tried to be derived from relevant association. For example:
class Person < ActiveRecord::Base
belongs_to :user
has_one :location
has_many :notes
has_many :phones
end
class PersonMapper < Flatter::Mapper
mount :user
mount :location
mount :note
mount :phones
end
Here we have:
:user
is a:belongs_to
association. Target for mountedUserMapper
will be by default fetched asperson.user || person.build_user
.:location
is a:has_one
association. Just like:user
, target for mountedLocationMapper
will be fetched asperson.location || person.build_location
.:notes
is a:has_many
association, and we map singular note. In this case target forPhoneMapper
is fetched asperson.notes.build
. Thus, you may want toskip!
this mounting before save or validation to prevent creating freshly-built record, if it was not populated with any values.:phones
is a:has_many
association, mounted as a collection mountings:phones
. In this case whole association would handled by mapper as a collection (this feature is available starting from0.2.0
version offlatter
). Thus, reading instance ofPersonMapper
will give you array ofphones
, each of which will have it's ownkey
value dependent on it's definition. See flatter Collections for more details on how mapped collections are handled.
Mounting collection associations and working with them as with collections is not supported for now.
Keep in mind that you can always pass :target
option to control targets of
mounted mappers.
Saving is performed without validation and callbacks by default
On save your models will not be validated (and their validation callbacks will
not be called), and their :save
callbacks will not be executed. However,
starting from version 0.2.1
, you can use Flatter::Mapper
class-level methods
enable_callback
(and it's alias enable_callbacks
) to enable specific
callbacks, such as enable_callbacks :create, :update
.
For example, if you have multi-step form and want to put all your validations in model, there will be dozens of boolean checks to use specific validations only on specific steps. With mappers, you can define necessary sets of validations within traits and keep your models clean.
Processing order and foreign keys
Since there are no callbacks, there will be no association autosaving, which means
that your models will be saved exactly once exactly when each mapper starts it's
saving routines. That also means that you should manually handle foreign keys
assigning when creating new records. This can be done via before- or after-save
callbacks, but extension provides a handy mounting options to do it for you:
foreign_key
and mounter_foreign_key
. First option should be set when mounted
mapper depends on current one. And the second one - when current mapper depends on
mounted one. In that case :index
option should be used to force this mounting
to be processed first:
class PersonMapper < Flatter::Mapper
mount :user, mounter_foreign_key: :user_id, index: -1
mount :phone, foreign_key: :person_id
end
Transactions
With :active_record
extension you should mainly use apply(params)
method for
updating or creating your models via mappers. It wraps whole saving process (writing
values, validation and saving) in a transaction. The reason for this is that your
mappings may have custom db-mutating writers, and if saving fails, such mutations
should be rolled back. However, save
method is also wrapped in transaction and
will return false
if any mapper in processing chain will fail to save it's
target. This might happen due to DB constraints, for example.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run
rake spec
to run the tests. You can also run bin/console
for an interactive
prompt that will allow you to experiment.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko/flatter-extensions.
License
The gem is available as open source under the terms of the MIT License.