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.
- checksums.yaml +15 -0
- data/.gitignore +31 -0
- data/.metrics +17 -0
- data/.rspec +4 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +20 -0
- data/LICENSE +20 -0
- data/README.markdown +211 -0
- data/Rakefile +15 -0
- data/flat_map.gemspec +30 -0
- data/lib/flat_map.rb +9 -0
- data/lib/flat_map/base_mapper.rb +95 -0
- data/lib/flat_map/base_mapper/attribute_methods.rb +54 -0
- data/lib/flat_map/base_mapper/factory.rb +238 -0
- data/lib/flat_map/base_mapper/mapping.rb +123 -0
- data/lib/flat_map/base_mapper/mounting.rb +168 -0
- data/lib/flat_map/base_mapper/persistence.rb +145 -0
- data/lib/flat_map/base_mapper/skipping.rb +62 -0
- data/lib/flat_map/base_mapper/traits.rb +94 -0
- data/lib/flat_map/empty_mapper.rb +29 -0
- data/lib/flat_map/errors.rb +57 -0
- data/lib/flat_map/mapper.rb +213 -0
- data/lib/flat_map/mapper/skipping.rb +45 -0
- data/lib/flat_map/mapper/targeting.rb +130 -0
- data/lib/flat_map/mapping.rb +124 -0
- data/lib/flat_map/mapping/factory.rb +21 -0
- data/lib/flat_map/mapping/reader.rb +12 -0
- data/lib/flat_map/mapping/reader/basic.rb +28 -0
- data/lib/flat_map/mapping/reader/formatted.rb +45 -0
- data/lib/flat_map/mapping/reader/formatted/formats.rb +28 -0
- data/lib/flat_map/mapping/reader/method.rb +25 -0
- data/lib/flat_map/mapping/reader/proc.rb +15 -0
- data/lib/flat_map/mapping/writer.rb +11 -0
- data/lib/flat_map/mapping/writer/basic.rb +25 -0
- data/lib/flat_map/mapping/writer/method.rb +28 -0
- data/lib/flat_map/mapping/writer/proc.rb +18 -0
- data/lib/flat_map/version.rb +3 -0
- data/spec/flat_map/empty_mapper_spec.rb +36 -0
- data/spec/flat_map/errors_spec.rb +23 -0
- data/spec/flat_map/mapper/attribute_methods_spec.rb +36 -0
- data/spec/flat_map/mapper/callbacks_spec.rb +76 -0
- data/spec/flat_map/mapper/factory_spec.rb +258 -0
- data/spec/flat_map/mapper/mapping_spec.rb +98 -0
- data/spec/flat_map/mapper/mounting_spec.rb +142 -0
- data/spec/flat_map/mapper/skipping_spec.rb +91 -0
- data/spec/flat_map/mapper/targeting_spec.rb +156 -0
- data/spec/flat_map/mapper/traits_spec.rb +172 -0
- data/spec/flat_map/mapper/validations_spec.rb +72 -0
- data/spec/flat_map/mapper_spec.rb +9 -0
- data/spec/flat_map/mapping/factory_spec.rb +12 -0
- data/spec/flat_map/mapping/reader/basic_spec.rb +15 -0
- data/spec/flat_map/mapping/reader/formatted_spec.rb +62 -0
- data/spec/flat_map/mapping/reader/method_spec.rb +13 -0
- data/spec/flat_map/mapping/reader/proc_spec.rb +13 -0
- data/spec/flat_map/mapping/writer/basic_spec.rb +15 -0
- data/spec/flat_map/mapping/writer/method_spec.rb +13 -0
- data/spec/flat_map/mapping/writer/proc_spec.rb +13 -0
- data/spec/flat_map/mapping_spec.rb +123 -0
- data/spec/spec_helper.rb +7 -0
- data/tmp/metric_fu/_data/20131218.yml +6902 -0
- data/tmp/metric_fu/_data/20131219.yml +6726 -0
- 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
|