flat_map 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|