flat_map 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +31 -0
  3. data/.metrics +17 -0
  4. data/.rspec +4 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +9 -0
  8. data/Gemfile +20 -0
  9. data/LICENSE +20 -0
  10. data/README.markdown +211 -0
  11. data/Rakefile +15 -0
  12. data/flat_map.gemspec +30 -0
  13. data/lib/flat_map.rb +9 -0
  14. data/lib/flat_map/base_mapper.rb +95 -0
  15. data/lib/flat_map/base_mapper/attribute_methods.rb +54 -0
  16. data/lib/flat_map/base_mapper/factory.rb +238 -0
  17. data/lib/flat_map/base_mapper/mapping.rb +123 -0
  18. data/lib/flat_map/base_mapper/mounting.rb +168 -0
  19. data/lib/flat_map/base_mapper/persistence.rb +145 -0
  20. data/lib/flat_map/base_mapper/skipping.rb +62 -0
  21. data/lib/flat_map/base_mapper/traits.rb +94 -0
  22. data/lib/flat_map/empty_mapper.rb +29 -0
  23. data/lib/flat_map/errors.rb +57 -0
  24. data/lib/flat_map/mapper.rb +213 -0
  25. data/lib/flat_map/mapper/skipping.rb +45 -0
  26. data/lib/flat_map/mapper/targeting.rb +130 -0
  27. data/lib/flat_map/mapping.rb +124 -0
  28. data/lib/flat_map/mapping/factory.rb +21 -0
  29. data/lib/flat_map/mapping/reader.rb +12 -0
  30. data/lib/flat_map/mapping/reader/basic.rb +28 -0
  31. data/lib/flat_map/mapping/reader/formatted.rb +45 -0
  32. data/lib/flat_map/mapping/reader/formatted/formats.rb +28 -0
  33. data/lib/flat_map/mapping/reader/method.rb +25 -0
  34. data/lib/flat_map/mapping/reader/proc.rb +15 -0
  35. data/lib/flat_map/mapping/writer.rb +11 -0
  36. data/lib/flat_map/mapping/writer/basic.rb +25 -0
  37. data/lib/flat_map/mapping/writer/method.rb +28 -0
  38. data/lib/flat_map/mapping/writer/proc.rb +18 -0
  39. data/lib/flat_map/version.rb +3 -0
  40. data/spec/flat_map/empty_mapper_spec.rb +36 -0
  41. data/spec/flat_map/errors_spec.rb +23 -0
  42. data/spec/flat_map/mapper/attribute_methods_spec.rb +36 -0
  43. data/spec/flat_map/mapper/callbacks_spec.rb +76 -0
  44. data/spec/flat_map/mapper/factory_spec.rb +258 -0
  45. data/spec/flat_map/mapper/mapping_spec.rb +98 -0
  46. data/spec/flat_map/mapper/mounting_spec.rb +142 -0
  47. data/spec/flat_map/mapper/skipping_spec.rb +91 -0
  48. data/spec/flat_map/mapper/targeting_spec.rb +156 -0
  49. data/spec/flat_map/mapper/traits_spec.rb +172 -0
  50. data/spec/flat_map/mapper/validations_spec.rb +72 -0
  51. data/spec/flat_map/mapper_spec.rb +9 -0
  52. data/spec/flat_map/mapping/factory_spec.rb +12 -0
  53. data/spec/flat_map/mapping/reader/basic_spec.rb +15 -0
  54. data/spec/flat_map/mapping/reader/formatted_spec.rb +62 -0
  55. data/spec/flat_map/mapping/reader/method_spec.rb +13 -0
  56. data/spec/flat_map/mapping/reader/proc_spec.rb +13 -0
  57. data/spec/flat_map/mapping/writer/basic_spec.rb +15 -0
  58. data/spec/flat_map/mapping/writer/method_spec.rb +13 -0
  59. data/spec/flat_map/mapping/writer/proc_spec.rb +13 -0
  60. data/spec/flat_map/mapping_spec.rb +123 -0
  61. data/spec/spec_helper.rb +7 -0
  62. data/tmp/metric_fu/_data/20131218.yml +6902 -0
  63. data/tmp/metric_fu/_data/20131219.yml +6726 -0
  64. metadata +184 -0
@@ -0,0 +1,145 @@
1
+ module FlatMap
2
+ # This module provides persistence functionality for mappers. Note
3
+ # that term of persistence here does not imply storing information
4
+ # in database or other place. This module provides methods for
5
+ # saving operation as a work flow of applying parameters to mapper
6
+ # and all of its mounted mappers in a right way, running callbacks,
7
+ # etc.
8
+ #
9
+ # See {Mapper::Targeting} for a place where mapper targets are
10
+ # actually get persisted / updated.
11
+ #
12
+ # In particular, validation and save methods are defined here. And
13
+ # the <tt>save</tt> method itself is defined as a callback. Also, Rails
14
+ # multiparam attributes extraction is defined within this module.
15
+ module BaseMapper::Persistence
16
+ # Write a passed set of +params+. Then try to save the model if +self+
17
+ # passes validation. Saving is performed in a transaction.
18
+ #
19
+ # @param [Hash] params
20
+ # @return [Boolean]
21
+ def apply(params)
22
+ write(params)
23
+ valid? && save
24
+ end
25
+
26
+ # Extract the multiparam values from the passed +params+. Then use the
27
+ # resulting hash to assign values to the target. Assignment is performed
28
+ # by sending writer methods to +self+ that correspond to keys in the
29
+ # resulting +params+ hash.
30
+ #
31
+ # @param [Hash] params
32
+ # @return [Hash] params
33
+ def write(params)
34
+ extract_multiparams!(params)
35
+
36
+ params.each do |name, value|
37
+ self.send("#{name}=", value)
38
+ end
39
+ end
40
+
41
+ # Try to save the target and send a +save+ method to all mounted mappers.
42
+ #
43
+ # The order in which mappings are saved is important, since we save
44
+ # records with :validate => false option. Since Rails will perform
45
+ # auto-saving on associations (and it in its turn will try to save associated
46
+ # record with :validate => true option. To be more precise, with
47
+ # :validate => !autosave option, where autosave corresponds to that option
48
+ # of reflection, which is usually not specified, i.e. nil), we might come to
49
+ # a situation of saving a record with nil foreign key for belongs_to association,
50
+ # which will raise exception. Thus, we want to explicitly save records in
51
+ # order which will allow them to be saved.
52
+ # Return +false+ if that chain of +save+ calls returns +true+ on any of
53
+ # its elements. Return +true+ otherwise.
54
+ #
55
+ # Saving is performed as a callback.
56
+ #
57
+ # @return [Boolean]
58
+ def save
59
+ before_res = save_mountings(before_save_mountings)
60
+ target_res = self_mountings.map{ |mount| mount.shallow_save }.all?
61
+ after_res = save_mountings(after_save_mountings)
62
+
63
+ before_res && target_res && after_res
64
+ end
65
+
66
+ # Perform target save with callbacks call
67
+ #
68
+ # @return [Boolean]
69
+ def shallow_save
70
+ run_callbacks(:save){ save_target }
71
+ end
72
+
73
+ # Send <tt>:save</tt> method to all mountings in list. Will return +true+
74
+ # only if all savings are positive.
75
+ #
76
+ # @param [Array<FlatMap::BaseMapper>] mountings
77
+ # @return [Boolean]
78
+ def save_mountings(mountings)
79
+ mountings.map{ |mount| mount.save }.all?
80
+ end
81
+ private :save_mountings
82
+
83
+ # Return +true+ if the mapper is valid, i.e. if it is valid itself, and if
84
+ # all mounted mappers (traits and other mappers) are also valid.
85
+ #
86
+ # Accepts any parameters, but doesn't use them to be compatible with
87
+ # ActiveRecord calls.
88
+ #
89
+ # @return [Boolean]
90
+ def valid?(*)
91
+ res = trait_mountings.map(&:valid?).all?
92
+ res = super && res # we do want to call super
93
+ mounting_res = mapper_mountings.map(&:valid?).all?
94
+ consolidate_errors!
95
+ res && mounting_res
96
+ end
97
+
98
+ # Consolidate the errors of all mounted mappers to a set of errors of +self+.
99
+ #
100
+ # @return [Array<ActiveModel::Errors>]
101
+ def consolidate_errors!
102
+ mountings.map(&:errors).each do |errs|
103
+ errors.messages.merge!(errs.to_hash){ |key, old, new| old.concat(new) }
104
+ end
105
+ end
106
+ private :consolidate_errors!
107
+
108
+ # Overridden to use {FlatMap::Errors} instead of ActiveModel ones.
109
+ #
110
+ # @return [FlatMap::Errors]
111
+ def errors
112
+ @errors ||= FlatMap::Errors.new(self)
113
+ end
114
+
115
+ # Extract Rails multiparam parameters from the +params+, modifying
116
+ # original hash. Behaves somewhat like
117
+ # {ActiveRecord::AttributeAssignment#extract_callstack_for_multiparameter_attributes}
118
+ # See this method for more details.
119
+ #
120
+ # @param [Hash] params
121
+ # @return [Array<FlatMap::Mapping>] return value is not used, original
122
+ # +params+ hash is modified instead and used later on.
123
+ def extract_multiparams!(params)
124
+ all_mappings.select(&:multiparam?).each do |mapping|
125
+ param_keys = params.keys.
126
+ select { |key| key.to_s =~ /#{mapping.full_name}\(\d+[isf]\)/ }.
127
+ sort_by{ |key| key.to_s[/\((\d+)\w*\)/, 1].to_i }
128
+
129
+ next if param_keys.empty?
130
+
131
+ args = param_keys.inject([]) do |values, _key|
132
+ value = params.delete _key
133
+ type = _key[/\(\d+(\w*)\)/, 1]
134
+ value = value.send("to_#{type}") unless type.blank?
135
+
136
+ values.push value
137
+ values
138
+ end
139
+
140
+ params[mapping.name] = mapping.multiparam.new(*args) rescue nil
141
+ end
142
+ end
143
+ private :extract_multiparams!
144
+ end
145
+ end
@@ -0,0 +1,62 @@
1
+ module FlatMap
2
+ # This helper module provides helper functionality that allow to
3
+ # exclude specific mapper from a processing chain.
4
+ module BaseMapper::Skipping
5
+ # Mark self as skipped, i.e. it will not be subject of
6
+ # validation and saving chain.
7
+ #
8
+ # @return [Object]
9
+ def skip!
10
+ @_skip_processing = true
11
+ end
12
+
13
+ # Remove "skip" mark from +self+ and "destroyed" flag from
14
+ # the target.
15
+ #
16
+ # @return [Object]
17
+ def use!
18
+ @_skip_processing = nil
19
+ end
20
+
21
+ # Return +true+ if +self+ was marked for skipping.
22
+ #
23
+ # @return [Boolean]
24
+ def skipped?
25
+ !!@_skip_processing
26
+ end
27
+
28
+ # Override {FlatMap::BaseMapper::Persistence#valid?} to
29
+ # force it to return +true+ if +self+ is marked for skipping.
30
+ #
31
+ # @param [Symbol] context useless context parameter to make it compatible with
32
+ # ActiveRecord models.
33
+ #
34
+ # @return [Boolean]
35
+ def valid?(context = nil)
36
+ skipped? || super
37
+ end
38
+
39
+ # Override {FlatMap::BaseMapper::Persistence#save} method to
40
+ # force it to return +true+ if +self+ is marked for skipping.
41
+ #
42
+ # @return [Boolean]
43
+ def save
44
+ skipped? || super
45
+ end
46
+
47
+ # Override {FlatMap::BaseMapper::Persistence#shallow_save} method
48
+ # to make it possible to skip traits.
49
+ #
50
+ # @return [Boolean]
51
+ def shallow_save
52
+ skipped? || super
53
+ end
54
+
55
+ # Mark self as used and then delegated to original
56
+ # {FlatMap::BaseMapper::Persistence#write}.
57
+ def write(*)
58
+ use!
59
+ super
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,94 @@
1
+ module FlatMap
2
+ # This small module allows mappers to define traits, which technically
3
+ # means mounting anonymous mappers, attached to host one.
4
+ module BaseMapper::Traits
5
+ extend ActiveSupport::Concern
6
+
7
+ # Traits class macros
8
+ module ClassMethods
9
+ # Define a trait for a mapper class. In implementation terms, a trait
10
+ # is nothing more than a mounted mapper, owned by a host mapper.
11
+ # It shares all mappings with it.
12
+ # The block is passed as a body of the anonymous mapper class.
13
+ #
14
+ # @param [Symbol] name
15
+ def trait(name, &block)
16
+ base_class = self < FlatMap::Mapper ? FlatMap::Mapper : FlatMap::EmptyMapper
17
+ mapper_class = Class.new(base_class, &block)
18
+ mapper_class_name = "#{ancestors.first.name}#{name.to_s.camelize}Trait"
19
+ mapper_class.singleton_class.send(:define_method, :name){ mapper_class_name }
20
+ mount mapper_class, :trait_name => name
21
+ end
22
+ end
23
+
24
+ # Override the original {FlatMap::BaseMapper::Mounting#mountings}
25
+ # method to filter out those traited mappers that are not required for
26
+ # trait setup of +self+. Also, handle any inline extension that may be
27
+ # defined on the mounting mapper, which is attached as a singleton trait.
28
+ #
29
+ # @return [Array<FlatMap::BaseMapper>]
30
+ def mountings
31
+ @mountings ||= begin
32
+ mountings = self.class.mountings.
33
+ reject{ |factory|
34
+ factory.traited? &&
35
+ !factory.required_for_any_trait?(traits)
36
+ }
37
+ mountings.concat(singleton_class.mountings)
38
+ mountings.map{ |factory| factory.create(self, *traits) }
39
+ end
40
+ end
41
+
42
+ # Return a list of all mountings that represent full picture of +self+, i.e.
43
+ # +self+ and all traits, including deeply nested, that are mounted on self
44
+ #
45
+ # @return [Array<FlatMap::BaseMapper>]
46
+ def self_mountings
47
+ mountings.select(&:owned?).map{ |mount| mount.self_mountings }.flatten.concat [self]
48
+ end
49
+
50
+ # Try to find trait mapper with name that corresponds to +trait_name+
51
+ # Used internally to manipulate such mappers (for example, skip some traits)
52
+ # in some scenarios.
53
+ #
54
+ # @param [Symbol] trait_name
55
+ # @return [FlatMap::BaseMapper, nil]
56
+ def trait(trait_name)
57
+ self_mountings.find{ |mount| mount.class.name.underscore =~ /#{trait_name}_trait$/ }
58
+ end
59
+
60
+ # Return :extension trait, if present
61
+ #
62
+ # @return [FlatMap::BaseMapper]
63
+ def extension
64
+ trait(:extension)
65
+ end
66
+
67
+ # Return only mountings that are actually traits for host mapper.
68
+ #
69
+ # @return [Array<FlatMap::BaseMapper>]
70
+ def trait_mountings
71
+ result = mountings.select{ |mount| mount.owned? }
72
+ # mapper extension has more priority then traits, and
73
+ # has to be processed first.
74
+ result.unshift(result.pop) if result.length > 1 && result[-1].extension?
75
+ result
76
+ end
77
+ protected :trait_mountings
78
+
79
+ # Return only mountings that correspond to external mappers.
80
+ #
81
+ # @return [Array<FlatMap::BaseMapper>]
82
+ def mapper_mountings
83
+ mountings.select{ |mount| !mount.owned? }
84
+ end
85
+ protected :mapper_mountings
86
+
87
+ # Return +true+ if +self+ is extension of host mapper.
88
+ #
89
+ # @return [Boolean]
90
+ def extension?
91
+ owned? && self.class.name =~ /ExtensionTrait$/
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,29 @@
1
+ module FlatMap
2
+ # +EmptyMapper+ behaves in a very same way as targeted {Mapper Mappers}
3
+ # with only distinction of absence of required target. That makes them
4
+ # a platform for mounting other mappers and placing control structures
5
+ # of business logic.
6
+ #
7
+ # Form more detailed information on what mappers are and their API
8
+ # refer to {Mapper}.
9
+ class EmptyMapper < BaseMapper
10
+ # Initializes +mapper+ with +traits+, which are
11
+ # used to fetch proper list of mounted mappers.
12
+ #
13
+ # @param [*Symbol] traits List of traits
14
+ def initialize(*traits)
15
+ @traits = traits
16
+
17
+ if block_given?
18
+ singleton_class.trait :extension, &Proc.new
19
+ end
20
+ end
21
+
22
+ # Return +true+ since there's no target.
23
+ #
24
+ # @return [true]
25
+ def save_target
26
+ true
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ module FlatMap
2
+ # Inherited from ActiveModel::Errors to slightly ease work when writing
3
+ # attributes in a way that can possibly result in an exception. If we'd
4
+ # want to add errors on that point and see them in the resulting object,
5
+ # we have to preserve them before owner's <tt>run_validations!</tt> method
6
+ # call, since it will clear all the errors.
7
+ #
8
+ # After validation complete, preserved errors are added to the list of
9
+ # the original ones.
10
+ #
11
+ # Usecase scenario:
12
+ #
13
+ # class MyMapper < FlatMap::Mapper
14
+ # def custom_attr=(value)
15
+ # raise MyException, 'cannot be foo' if value == 'foo'
16
+ # rescue MyException => e
17
+ # errors.preserve :custom_attr, e.message
18
+ # end
19
+ # end
20
+ #
21
+ # mapper = MyMapper.new(MyObject.new)
22
+ # mapper.apply(:custom_attr => 'foo') # => false
23
+ # mapper.errors[:custom_attr] # => ['cannot be foo']
24
+ class Errors < ActiveModel::Errors
25
+ # Add <tt>@preserved_errors</tt> to object.
26
+ def initialize(*)
27
+ @preserved_errors = {}
28
+ super
29
+ end
30
+
31
+ # Postpone error. It will be added to <tt>@messages</tt> later,
32
+ # on <tt>empty?</tt> method call.
33
+ #
34
+ # @param [String, Symbol] key
35
+ # @param [String] message
36
+ def preserve(key, message)
37
+ @preserved_errors[key] = message
38
+ end
39
+
40
+ # Overloaded to add <tt>@preserved_errors</tt> to the list of
41
+ # original <tt>@messages</tt>. <tt>@preserved_errors</tt> are
42
+ # cleared after this method call.
43
+ def empty?
44
+ unless @preserved_errors.empty?
45
+ @preserved_errors.each{ |key, value| add(key, value) }
46
+ @preserved_errors.clear
47
+ end
48
+ super
49
+ end
50
+
51
+ # Overridden to add suffixing support for mappings of mappers with name suffix
52
+ def add(attr, *args)
53
+ attr = :"#{attr}_#{@base.suffix}" if attr != :base && @base.suffixed?
54
+ super
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,213 @@
1
+ module FlatMap
2
+ # == Mapper
3
+ #
4
+ # FlatMap mappers are designed to provide complex set of data, distributed over
5
+ # associated AR models, in the simple form of a plain hash. They accept a plain
6
+ # hash of the same format and distribute its values over deeply nested AR models.
7
+ #
8
+ # To achieve this goal, Mapper uses three major concepts: Mappings, Mountings and
9
+ # Traits.
10
+ #
11
+ # === Mappings
12
+ #
13
+ # Mappings are defined view Mapper.map method. They represent a simple one-to-one
14
+ # relation between target attribute and a mapper, extended by additional features
15
+ # for convenience. The best way to show how they work is by example:
16
+ #
17
+ # class CustomerMapper < FlatMap::Mapper
18
+ # # When there is no need to rename attributes, they can be passed as array:
19
+ # map :first_name, :last_name
20
+ #
21
+ # # When hash is used, it will map field name to attribute name:
22
+ # map :dob => :date_of_birth
23
+ #
24
+ # # Also, additional options can be used:
25
+ # map :name_suffix, :format => :enum
26
+ # map :password, :reader => false, :writer => :assign_password
27
+ #
28
+ # # Or you can combine all definitions together if they all are common:
29
+ # map :first_name, :last_name,
30
+ # :dob => :date_of_birth,
31
+ # :suffix => :name_suffix,
32
+ # :reader => :my_custom_reader
33
+ # end
34
+ #
35
+ # When mappings are defined, one can read and write values using them:
36
+ #
37
+ # mapper = CustomerMapper.find(1)
38
+ # mapper.read # => {:first_name => 'John', :last_name => 'Smith', :dob => '02/01/1970'}
39
+ # mapper.write(params) # will assign same-looking hash of arguments
40
+ #
41
+ # Following options may be used when defining mappings:
42
+ # [<tt>:format</tt>] Allows to additionally process output value on reading it. All formats are
43
+ # defined within FlatMap::Mapping::Reader::Formatted::Formats and
44
+ # specify the actual output of the mapping
45
+ # [<tt>:reader</tt>] Allows you to manually control reader value of a mapping, or a group of
46
+ # mappings listed on definition. When String or Symbol is used, will call
47
+ # a method, defined by mapper class, and pass mapping object to it. When
48
+ # lambda is used, mapper's target (the model) will be passed to it.
49
+ # [<tt>:writer</tt>] Just like with the :reader option, allows to control how value is assigned
50
+ # (written). Works the same way as :reader does, but additionally value is
51
+ # sent to both mapper method and lambda.
52
+ # [<tt>:multiparam</tt>] If used, multiparam attributes will be extracted from params, when
53
+ # those are passed for writing. Class should be passed as a value for
54
+ # this option. Object of this class will be initialized with the arguments
55
+ # extracted from params hash.
56
+ #
57
+ # === Mountings
58
+ #
59
+ # Mappers may be mounted on top of each other. This ability allows host mapper to gain all the
60
+ # mappings of the mounted mapper, thus providing more information for external usage (both reading
61
+ # and writing). Usually, target for mounted mapper may be obtained from association of target of
62
+ # the host mapper itself, but may be defined manually.
63
+ #
64
+ # class CustomerMapper < FlatMap::Mapper
65
+ # map :first_name, :last_name
66
+ # end
67
+ #
68
+ # class CustomerAccountMapper < FlatMap::Mapper
69
+ # map :source, :brand, :format => :enum
70
+ #
71
+ # mount :customer
72
+ # end
73
+ #
74
+ # mapper = CustomerAccountMapper.find(1)
75
+ # mapper.read # => {:first_name => 'John', :last_name => 'Smith', :source => nil, :brand => 'FTW'}
76
+ # mapper.write(params) # Will assign params for both CustomerAccount and Customer records
77
+ #
78
+ # The following options may be used when mounting a mapper:
79
+ # [<tt>:mapper_class</tt>] Specifies mapper class if it cannot be determined from mounting itself
80
+ # [<tt>:target</tt>] Allows to manually specify target for the new mapper. May be oject or lambda
81
+ # with arity of one that accepts host mapper target as argument. Comes in handy
82
+ # when target cannot be obviously detected or requires additional setup:
83
+ # <tt>mount :title, :target => lambda{ |customer| customer.title_customers.build.build_title }</tt>
84
+ # [<tt>:traits</tt>] Specifies list of traits to be used by mounted mapper
85
+ # [<tt>:suffix</tt>] Specifies the suffix that will be appended to all mappings and mountings of mapper,
86
+ # as well as mapper name itself.
87
+ #
88
+ # === Traits
89
+ #
90
+ # Traits allow mappers to encapsulate named sets of additional definitions, and use them optionally
91
+ # on mapper initialization. Everything that can be defined within the mapper may be defined within
92
+ # the trait. In fact, from the implementation perspective traits are mappers themselves that are
93
+ # mounted on the host mapper.
94
+ #
95
+ # class CustomerAccountMapper < FlatMap::Mapper
96
+ # map :brand, :format => :enum
97
+ #
98
+ # trait :with_email do
99
+ # map :source, :format => :enum
100
+ #
101
+ # mount :email_address
102
+ #
103
+ # trait :with_email_phones_residence do
104
+ # mount :customer, :traits => [:with_phone_numbers, :with_residence]
105
+ # end
106
+ # end
107
+ # end
108
+ #
109
+ # CustomerAccountMapper.find(1).read # => {:brand => 'TLP'}
110
+ # CustomerAccountMapper.find(1, :with_email).read # => {:brand => 'TLP', :source => nil, :email_address => 'j.smith@gmail.com'}
111
+ # CustomerAccountMapper.find(1, :with_email_phone_residence).read # => :brand, :source, :email_address, phone numbers,
112
+ # #:residence attributes - all will be available for reading and writing in plain hash
113
+ #
114
+ # === Extensions
115
+ #
116
+ # When mounting a mapper, one can pass an optional block. This block is used as an extension for a mounted
117
+ # mapper and acts as an anonymous trait. For example:
118
+ #
119
+ # class CustomerAccountMapper < FlatMap::Mapper
120
+ # mount :customer do
121
+ # map :dob => :date_of_birth, :format => :i18n_l
122
+ # validates_presence_of :dob
123
+ #
124
+ # mount :unique_identifier
125
+ #
126
+ # validates_acceptance_of :mandatory_agreement, :message => "You must check this box to continue"
127
+ # end
128
+ # end
129
+ #
130
+ # === Validation
131
+ #
132
+ # <tt>FlatMap::Mapper</tt> includes <tt>ActiveModel::Validations</tt> module, allowing each model to
133
+ # perform its own validation routines before trying to save its target (which is usually AR model). Mapper
134
+ # validation is very handy when mappers are used with Rails forms, since there no need to lookup for a
135
+ # deeply nested errors hash of the AR models to extract error messages. Mapper validations will attach
136
+ # messages to mapping names.
137
+ #
138
+ # Mapper validations become even more useful when used within traits, providing way of very flexible validation sets.
139
+ #
140
+ # === Callbacks
141
+ #
142
+ # Since mappers include <tt>ActiveModel::Validation</tt>, they already support ActiveSupport's callbacks.
143
+ # Additionally, <tt>:save</tt> callbacks have been defined (i.e. there have been define_callbacks <tt>:save</tt>
144
+ # call for <tt>FlatMap::Mapper</tt>). This allows you to control flow of mapper saving:
145
+ #
146
+ # set_callback :save, :before, :set_model_validation
147
+ #
148
+ # def set_model_validation
149
+ # target.use_validation :some_themis_validation
150
+ # end
151
+ #
152
+ # === Skipping
153
+ #
154
+ # In some cases, it is required to omit mapper processing after it has been created within mounting chain. If
155
+ # <tt>skip!</tt> method is called on mapper, it will return <tt>true</tt> for <tt>valid?</tt> and <tt>save</tt>
156
+ # method calls without performing any other operations. For example:
157
+ #
158
+ # class CustomerMapper < FlatMap::Mapper
159
+ # # some definitions
160
+ #
161
+ # trait :product_selection do
162
+ # attr_reader :selected_product_id
163
+ #
164
+ # mount :product
165
+ #
166
+ # set_callback :validate, :before, :ignore_new_product
167
+ #
168
+ # def ignore_new_bank_account
169
+ # mounting(:product).skip! if product_selected?
170
+ # end
171
+ #
172
+ # # some more definitions
173
+ # end
174
+ # end
175
+ #
176
+ # === Attribute Methods
177
+ #
178
+ # All mappers have the ability to read and write values via method calls:
179
+ #
180
+ # mapper.read[:first_name] # => John
181
+ # mapper.first_name # => 'John'
182
+ # mapper.last_name = 'Smith'
183
+ class Mapper < BaseMapper
184
+ extend ActiveSupport::Autoload
185
+
186
+ autoload :Targeting
187
+ autoload :Skipping
188
+
189
+ include Targeting
190
+ include Skipping
191
+
192
+ attr_reader :target
193
+
194
+ delegate :logger, :to => :target
195
+
196
+ # Initializes +mapper+ with +target+ and +traits+, which are
197
+ # used to fetch proper list of mounted mappers. Raises error
198
+ # if target is not specified.
199
+ #
200
+ # @param [Object] target Target of mapping
201
+ # @param [*Symbol] traits List of traits
202
+ # @raise [FlatMap::Mapper::NoTargetError]
203
+ def initialize(target, *traits)
204
+ raise Targeting::NoTargetError.new(self.class) unless target.present?
205
+
206
+ @target, @traits = target, traits
207
+
208
+ if block_given?
209
+ singleton_class.trait :extension, &Proc.new
210
+ end
211
+ end
212
+ end
213
+ end