HornsAndHooves-flat_map 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -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/HornsAndHooves-flat_map.gemspec +29 -0
  10. data/LICENSE +21 -0
  11. data/README.markdown +214 -0
  12. data/Rakefile +15 -0
  13. data/lib/flat_map.rb +14 -0
  14. data/lib/flat_map/errors.rb +57 -0
  15. data/lib/flat_map/mapping.rb +124 -0
  16. data/lib/flat_map/mapping/factory.rb +21 -0
  17. data/lib/flat_map/mapping/reader.rb +12 -0
  18. data/lib/flat_map/mapping/reader/basic.rb +28 -0
  19. data/lib/flat_map/mapping/reader/formatted.rb +45 -0
  20. data/lib/flat_map/mapping/reader/formatted/formats.rb +28 -0
  21. data/lib/flat_map/mapping/reader/method.rb +25 -0
  22. data/lib/flat_map/mapping/reader/proc.rb +15 -0
  23. data/lib/flat_map/mapping/writer.rb +11 -0
  24. data/lib/flat_map/mapping/writer/basic.rb +25 -0
  25. data/lib/flat_map/mapping/writer/method.rb +28 -0
  26. data/lib/flat_map/mapping/writer/proc.rb +18 -0
  27. data/lib/flat_map/model_mapper.rb +195 -0
  28. data/lib/flat_map/model_mapper/persistence.rb +108 -0
  29. data/lib/flat_map/model_mapper/skipping.rb +45 -0
  30. data/lib/flat_map/open_mapper.rb +113 -0
  31. data/lib/flat_map/open_mapper/attribute_methods.rb +55 -0
  32. data/lib/flat_map/open_mapper/factory.rb +244 -0
  33. data/lib/flat_map/open_mapper/mapping.rb +123 -0
  34. data/lib/flat_map/open_mapper/mounting.rb +168 -0
  35. data/lib/flat_map/open_mapper/persistence.rb +178 -0
  36. data/lib/flat_map/open_mapper/skipping.rb +66 -0
  37. data/lib/flat_map/open_mapper/traits.rb +95 -0
  38. data/lib/flat_map/version.rb +3 -0
  39. data/spec/flat_map/errors_spec.rb +23 -0
  40. data/spec/flat_map/mapper/attribute_methods_spec.rb +36 -0
  41. data/spec/flat_map/mapper/callbacks_spec.rb +76 -0
  42. data/spec/flat_map/mapper/factory_spec.rb +285 -0
  43. data/spec/flat_map/mapper/mapping_spec.rb +98 -0
  44. data/spec/flat_map/mapper/mounting_spec.rb +142 -0
  45. data/spec/flat_map/mapper/persistence_spec.rb +152 -0
  46. data/spec/flat_map/mapper/skipping_spec.rb +91 -0
  47. data/spec/flat_map/mapper/targeting_spec.rb +156 -0
  48. data/spec/flat_map/mapper/traits_spec.rb +172 -0
  49. data/spec/flat_map/mapper/validations_spec.rb +72 -0
  50. data/spec/flat_map/mapper_spec.rb +9 -0
  51. data/spec/flat_map/mapping/factory_spec.rb +12 -0
  52. data/spec/flat_map/mapping/reader/basic_spec.rb +15 -0
  53. data/spec/flat_map/mapping/reader/formatted_spec.rb +62 -0
  54. data/spec/flat_map/mapping/reader/method_spec.rb +13 -0
  55. data/spec/flat_map/mapping/reader/proc_spec.rb +13 -0
  56. data/spec/flat_map/mapping/writer/basic_spec.rb +15 -0
  57. data/spec/flat_map/mapping/writer/method_spec.rb +13 -0
  58. data/spec/flat_map/mapping/writer/proc_spec.rb +13 -0
  59. data/spec/flat_map/mapping_spec.rb +123 -0
  60. data/spec/flat_map/open_mapper_spec.rb +19 -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 +220 -0
@@ -0,0 +1,108 @@
1
+ module FlatMap
2
+ # This module enhances and modifies original FlatMap::OpenMapper::Persistence
3
+ # functionality for ActiveRecord models as targets.
4
+ module ModelMapper::Persistence
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Writer of the target class name. Allows manual control over target
9
+ # class of the mapper, for example:
10
+ #
11
+ # class CustomerMapper
12
+ # self.target_class_name = 'Customer::Active'
13
+ # end
14
+ class_attribute :target_class_name
15
+ end
16
+
17
+ # ModelMethods class macros
18
+ module ClassMethods
19
+ # Find a record of the +target_class+ by +id+ and use it as a
20
+ # target for a new mapper, with a list of passed +traits+ applied
21
+ # to it.
22
+ #
23
+ # @param [#to_i] id of the record
24
+ # @param [*Symbol] traits
25
+ # @return [FlatMap::Mapper] mapper
26
+ def find(id, *traits, &block)
27
+ new(target_class.find(id), *traits, &block)
28
+ end
29
+
30
+ # Fetch a class for the target of the mapper.
31
+ #
32
+ # @return [Class] class
33
+ def target_class
34
+ (target_class_name || default_target_class_name).constantize
35
+ end
36
+
37
+ # Return target class name based on name of the ancestor mapper
38
+ # that is closest to {FlatMap::Mapper}, which may be +self+.
39
+ #
40
+ # class VehicleMapper
41
+ # # some definitions
42
+ # end
43
+ #
44
+ # class CarMapper < VehicleMapper
45
+ # # some more definitions
46
+ # end
47
+ #
48
+ # CarMapper.target_class # => Vehicle
49
+ #
50
+ # @return [String]
51
+ def default_target_class_name
52
+ ancestor_classes = ancestors.select{ |ancestor| ancestor.is_a? Class }
53
+ base_mapper_index = ancestor_classes.index(::FlatMap::ModelMapper)
54
+ ancestor_classes[base_mapper_index - 1].name[/^([\w:]+)Mapper.*$/, 1]
55
+ end
56
+ end
57
+
58
+ # Return a 'mapper' string as a model_name. Used by Rails FormBuilder.
59
+ #
60
+ # @return [String]
61
+ def model_name
62
+ 'mapper'
63
+ end
64
+
65
+ # Delegate to the target's #to_key method.
66
+ # @return [String]
67
+ def to_key
68
+ target.to_key
69
+ end
70
+
71
+ # Write a passed set of +params+. Then try to save the model if +self+
72
+ # passes validation. Saving is performed in a transaction.
73
+ #
74
+ # @param [Hash] params
75
+ # @return [Boolean]
76
+ def apply(params)
77
+ write(params)
78
+ res = if valid?
79
+ ActiveRecord::Base.transaction do
80
+ save
81
+ end
82
+ end
83
+ !!res
84
+ end
85
+
86
+ # Save +target+
87
+ #
88
+ # @return [Boolean]
89
+ def save_target
90
+ return true if owned?
91
+ target.respond_to?(:save) ? target.save(:validate => false) : true
92
+ end
93
+
94
+ # Delegate persistence to target.
95
+ #
96
+ # @return [Boolean]
97
+ def persisted?
98
+ target.respond_to?(:persisted?) ? target.persisted? : false
99
+ end
100
+
101
+ # Delegate #id to target, if possible.
102
+ #
103
+ # @return [Fixnum, nil]
104
+ def id
105
+ target.id if target.respond_to?(:id)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,45 @@
1
+ module FlatMap
2
+ # This helper module slightly enhances ofunctionality of the
3
+ # {FlatMap::OpenMapper::Skipping} module for most commonly
4
+ # used +ActiveRecord+ targets.
5
+ module ModelMapper::Skipping
6
+ # Extend original #skip! method for Rails-models-based targets
7
+ #
8
+ # Note that this will mark the target record as
9
+ # destroyed if it is a new record. Thus, this
10
+ # record will not be a subject of Rails associated
11
+ # validation procedures, and will not be saved as an
12
+ # associated record.
13
+ #
14
+ # @return [Object]
15
+ def skip!
16
+ super
17
+ if target.is_a?(ActiveRecord::Base)
18
+ if target.new_record?
19
+ # Using the instance variable directly as {ActiveRecord::Base#delete}
20
+ # will freeze the record.
21
+ target.instance_variable_set('@destroyed', true)
22
+ else
23
+ # Using reload instead of reset_changes! to reset associated nested
24
+ # model changes also
25
+ target.reload
26
+ end
27
+ end
28
+ end
29
+
30
+ # Extend original #use! method for Rails-models-based targets, as
31
+ # acoompanied to #skip! method.
32
+ #
33
+ # @return [Object]
34
+ def use!
35
+ super
36
+ if target.is_a?(ActiveRecord::Base)
37
+ if target.new_record?
38
+ target.instance_variable_set('@destroyed', false)
39
+ else
40
+ all_nested_mountings.each(&:use!)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,113 @@
1
+ module FlatMap
2
+ # Base Mapper that can be used for mounting other mappers, handling business logic,
3
+ # etc. For the intentional usage of mappers, pleas see {ModelMapper}
4
+ class OpenMapper
5
+ # Raised when mapper is initialized with no target defined
6
+ class NoTargetError < ArgumentError
7
+ # Initializes exception with a name of mapper class.
8
+ #
9
+ # @param [Class] mapper_class class of mapper being initialized
10
+ def initialize(mapper_class)
11
+ super("Target object is required to initialize #{mapper_class.name}")
12
+ end
13
+ end
14
+
15
+ extend ActiveSupport::Autoload
16
+
17
+ autoload :Mapping
18
+ autoload :Mounting
19
+ autoload :Traits
20
+ autoload :Factory
21
+ autoload :AttributeMethods
22
+ autoload :Persistence
23
+ autoload :Skipping
24
+
25
+ include Mapping
26
+ include Mounting
27
+ include Traits
28
+ include AttributeMethods
29
+ include ActiveModel::Validations
30
+ include Persistence
31
+ include Skipping
32
+
33
+ attr_writer :host, :suffix
34
+ attr_reader :target, :traits
35
+ attr_accessor :owner, :name
36
+
37
+ # Callback to dup mappings and mountings on inheritance.
38
+ # The values are cloned from actual mappers (i.e. something
39
+ # like CustomerAccountMapper, since it is useless to clone
40
+ # empty values of FlatMap::Mapper).
41
+ #
42
+ # Note: those class attributes are defined in {Mapping}
43
+ # and {Mounting} modules.
44
+ def self.inherited(subclass)
45
+ subclass.mappings = mappings.dup
46
+ subclass.mountings = mountings.dup
47
+ end
48
+
49
+ # Initializes +mapper+ with +target+ and +traits+, which are
50
+ # used to fetch proper list of mounted mappers. Raises error
51
+ # if target is not specified.
52
+ #
53
+ # @param [Object] target Target of mapping
54
+ # @param [*Symbol] traits List of traits
55
+ # @raise [FlatMap::Mapper::NoTargetError]
56
+ def initialize(target, *traits)
57
+ raise NoTargetError.new(self.class) unless target
58
+
59
+ @target, @traits = target, traits
60
+
61
+ if block_given?
62
+ singleton_class.trait :extension, &Proc.new
63
+ end
64
+ end
65
+
66
+ # Return a simple string representation of +mapper+. Done so to
67
+ # avoid really long inspection of internal objects (target -
68
+ # usually AR model, mountings and mappings)
69
+ # @return [String]
70
+ def inspect
71
+ to_s
72
+ end
73
+
74
+ # Return +true+ if +mapper+ is owned. This means that current
75
+ # mapper is actually a trait. Thus, it is a part of an owner
76
+ # mapper.
77
+ #
78
+ # @return [Boolean]
79
+ def owned?
80
+ owner.present?
81
+ end
82
+
83
+ # If mapper was mounted by another mapper, host is the one who
84
+ # mounted +self+.
85
+ #
86
+ # @return [FlatMap::Mapper]
87
+ def host
88
+ owned? ? owner.host : @host
89
+ end
90
+
91
+ # Return +true+ if mapper is hosted, i.e. it is mounted by another
92
+ # mapper.
93
+ #
94
+ # @return [Boolean]
95
+ def hosted?
96
+ host.present?
97
+ end
98
+
99
+ # +suffix+ reader. Delegated to owner for owned mappers.
100
+ #
101
+ # @return [String, nil]
102
+ def suffix
103
+ owned? ? owner.suffix : @suffix
104
+ end
105
+
106
+ # Return +true+ if +suffix+ is present.
107
+ #
108
+ # @return [Boolean]
109
+ def suffixed?
110
+ suffix.present?
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,55 @@
1
+ module FlatMap
2
+ # This module allows mappers to return and assign values via method calls
3
+ # which names correspond to names of mappings defined within the mapper.
4
+ #
5
+ # This methods are defined within anonymous module that will extend
6
+ # mapper on first usage of this methods.
7
+ #
8
+ # NOTE: :to_ary method is called internally by Ruby 1.9.3 when we call
9
+ # something like [mapper].flatten. And we DO want default behavior
10
+ # for handling this missing method.
11
+ module OpenMapper::AttributeMethods
12
+ # Lazily define reader and writer methods for all mappings available
13
+ # to the mapper, and extend +self+ with it.
14
+ def method_missing(name, *args, &block)
15
+ if name == :to_ary ||
16
+ @attribute_methods_defined ||
17
+ self.class.protected_instance_methods.include?(name)
18
+ return super
19
+ end
20
+
21
+ mappings = all_mappings
22
+ valid_names = mappings.map do |mapping|
23
+ full_name = mapping.full_name
24
+ [full_name, "#{full_name}=".to_sym]
25
+ end
26
+ valid_names.flatten!
27
+
28
+ return super unless valid_names.include?(name)
29
+
30
+ extend attribute_methods(mappings)
31
+ @attribute_methods_defined = true
32
+ send(name, *args, &block)
33
+ end
34
+
35
+ # Define anonymous module with reader and writer methods for
36
+ # all the +mappings+ being passed.
37
+ #
38
+ # @param [Array<FlatMap::Mapping>] mappings list of mappings
39
+ # @return [Module] module with method definitions
40
+ def attribute_methods(mappings)
41
+ Module.new do
42
+ mappings.each do |mapping|
43
+ full_name = mapping.full_name
44
+
45
+ define_method(full_name){ |*args| mapping.read(*args) }
46
+
47
+ define_method("#{full_name}=") do |value|
48
+ mapping.write(value)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ private :attribute_methods
54
+ end
55
+ end
@@ -0,0 +1,244 @@
1
+ module FlatMap
2
+ # Mapper factory objects are used to store mounting and trait definitions
3
+ # and to instantiate and setup corresponding mapper objects thereafter.
4
+ # Factory objects are stored by mapper classes in opposite to actual
5
+ # mounted mappers that are stored by mapper objects themselves.
6
+ class OpenMapper::Factory
7
+ # Initializes factory with an identifier (name of a mounted mapper,
8
+ # or the actual class for a trait) and a set of options. Those args
9
+ # are used to create actual mapper object for the host mapper.
10
+ #
11
+ # @param [Symbol, Class] identifier name of a mapper or mapper class
12
+ # itself
13
+ # @param [Hash] options
14
+ def initialize(identifier, options = {}, &block)
15
+ @identifier, @options, @extension = identifier, options, block
16
+ end
17
+
18
+ # Return +true+ if factory defines a trait.
19
+ #
20
+ # @return [Boolean]
21
+ def traited?
22
+ @identifier.is_a?(Class)
23
+ end
24
+
25
+ # Return the name of the mapper being defined by the factory.
26
+ # Return +nil+ for the traited factory.
27
+ #
28
+ # @return [Symbol, nil]
29
+ def name
30
+ traited? ? nil : @identifier
31
+ end
32
+
33
+ # Return the trait name if the factory defines a trait.
34
+ def trait_name
35
+ @options[:trait_name] if traited?
36
+ end
37
+
38
+ # Return the list of traits that should be applied for a mapper being
39
+ # mounted on a host mapper.
40
+ #
41
+ # @return [Array<Symbol>] list of traits
42
+ def traits
43
+ Array(@options[:traits]).compact
44
+ end
45
+
46
+ # Return the anonymous trait class if the factory defines a trait.
47
+ # Fetch and return the class of a mapper defined by a symbol.
48
+ #
49
+ # @return [Class] ancestor of {FlatMap::OpenMapper}
50
+ def mapper_class
51
+ @mapper_class ||= begin
52
+ case
53
+ when traited? then @identifier
54
+ when @options[:open] then open_mapper_class
55
+ when @options[:mapper_class] then @options[:mapper_class]
56
+ else
57
+ class_name = @options[:mapper_class_name] || "#{name.to_s.camelize}Mapper"
58
+ class_name.constantize
59
+ end
60
+ end
61
+ end
62
+
63
+ # Return descendant of +OpenMapper+ class to be used when mounting
64
+ # open mappers.
65
+ #
66
+ # @return [Class]
67
+ def open_mapper_class
68
+ klass = Class.new(OpenMapper)
69
+ klass_name = "#{name.to_s.camelize}Mapper"
70
+ klass.singleton_class.send(:define_method, :name){ klass_name }
71
+ klass
72
+ end
73
+
74
+ # Return +true+ if mapper to me mounted is +ModelMapper+
75
+ #
76
+ # @return [Boolean]
77
+ def model_mount?
78
+ mapper_class < ModelMapper
79
+ end
80
+
81
+ # Return +true+ if mapper to me mounted is not +ModelMapper+, i.e. +OpenMapper+.
82
+ #
83
+ # @return [Boolean]
84
+ def open_mount?
85
+ !model_mount?
86
+ end
87
+
88
+ # Fetch the target for the mapper being created based on target of a host mapper.
89
+ #
90
+ # @param [FlatMap::OpenMapper] mapper Host mapper
91
+ # @return [Object] target for new mapper
92
+ def fetch_target_from(mapper)
93
+ owner_target = mapper.target
94
+
95
+ return owner_target if traited?
96
+
97
+ if open_mount?
98
+ explicit_target(mapper) || new_target
99
+ else
100
+ explicit_target(mapper) ||
101
+ target_from_association(owner_target) ||
102
+ target_from_name(owner_target) ||
103
+ new_target
104
+ end
105
+ end
106
+
107
+ # Return new instance of +target_class+ of the mapper.
108
+ #
109
+ # @return [Object]
110
+ def new_target
111
+ mapper_class.target_class.new
112
+ end
113
+
114
+ # Try to use explicit target definition passed in options to fetch a
115
+ # target. If this value is a +Proc+, will call it with owner target as
116
+ # argument.
117
+ #
118
+ # @param [FlatMap::OpenMapper] mapper
119
+ # @return [Object, nil] target for new mapper.
120
+ def explicit_target(mapper)
121
+ if @options.key?(:target)
122
+ target = @options[:target]
123
+ case target
124
+ when Proc then target.call(mapper.target)
125
+ when Symbol then mapper.send(target)
126
+ else target
127
+ end
128
+ end
129
+ end
130
+
131
+ # Try to fetch the target for a new mapper being mounted, based on
132
+ # correspondence of the mounting name and presence of the association
133
+ # with a similar name in the host mapper.
134
+ #
135
+ # For example:
136
+ # class Foo < ActiveRecord::Base
137
+ # has_one :baz
138
+ # has_many :bars
139
+ # end
140
+ #
141
+ # class FooMapper < FlatMap::Mapper
142
+ # # target of this mapper is the instance of Foo. Lets reference it as 'foo'
143
+ # mount :baz # This will look for BazMapper, and will try to fetch a target for
144
+ # # it based on :has_one association, i.e. foo.baz || foo.build_baz
145
+ #
146
+ # mount :bar # This will look for BarMapper, and will try to fetch a target for
147
+ # # it based on :has_many association, i.e. foo.bars.build
148
+ # end
149
+ def target_from_association(owner_target)
150
+ return unless owner_target.is_a?(ActiveRecord::Base)
151
+
152
+ reflection = reflection_from_target(owner_target)
153
+ return unless reflection.present?
154
+
155
+ reflection_macro = reflection.macro
156
+ case
157
+ when reflection_macro == :has_one && reflection.options[:is_current]
158
+ owner_target.send("effective_#{name}")
159
+ when reflection_macro == :has_one || reflection_macro == :belongs_to
160
+ owner_target.send(name) || owner_target.send("build_#{name}")
161
+ when reflection_macro == :has_many
162
+ owner_target.association(reflection.name).build
163
+ end
164
+ end
165
+
166
+ # Try to retreive an association reflection that has a name corresponding
167
+ # to the one of +self+
168
+ #
169
+ # @param [ActiveRecord::Base] target
170
+ # @return [ActiveRecord::Reflection::AssociationReflection, nil]
171
+ def reflection_from_target(target)
172
+ return unless name.present? && target.is_a?(ActiveRecord::Base)
173
+ target_class = target.class
174
+ reflection = target_class.reflect_on_association(name)
175
+ reflection || target_class.reflect_on_association(name.to_s.pluralize.to_sym)
176
+ end
177
+
178
+ # Send the name of the mounting to the target of the host mapper, and use
179
+ # return value as a target for a mapper being created.
180
+ #
181
+ # @return [Object]
182
+ def target_from_name(target)
183
+ target.send(name)
184
+ end
185
+
186
+ # Return order relative to target of the passed +mapper+ in which mapper to
187
+ # be created should be saved. In particular, targets of <tt>:belongs_to</tt>
188
+ # associations should be saved before target of +mapper+ is saved.
189
+ #
190
+ # @param [FlatMap::OpenMapper] mapper
191
+ # @return [Symbol]
192
+ def fetch_save_order(mapper)
193
+ return :after unless mapper.is_a?(ModelMapper)
194
+
195
+ reflection = reflection_from_target(mapper.target)
196
+ return unless reflection.present?
197
+ reflection.macro == :belongs_to ? :before : :after
198
+ end
199
+
200
+ # Create a new mapper object for mounting. If the factory is traited,
201
+ # the new mapper is a part of a host mapper, and is 'owned' by it.
202
+ # Otherwise, assign the name of the factory to it to be able to find it
203
+ # later on.
204
+ #
205
+ # @param [FlatMap::OpenMapper] mapper Host mapper
206
+ # @param [*Symbol] owner_traits List of traits to be applied to a newly created mapper
207
+ def create(mapper, *owner_traits)
208
+ save_order = @options[:save] || fetch_save_order(mapper) || :after
209
+ new_one = mapper_class.new(fetch_target_from(mapper), *(traits + owner_traits).uniq, &@extension)
210
+ if traited?
211
+ new_one.owner = mapper
212
+ else
213
+ new_one.host = mapper
214
+ new_one.name = @identifier
215
+ new_one.save_order = save_order
216
+
217
+ if (suffix = @options[:suffix] || mapper.suffix).present?
218
+ new_one.suffix = suffix
219
+ new_one.name = :"#{@identifier}_#{suffix}"
220
+ else
221
+ new_one.name = @identifier
222
+ end
223
+ end
224
+ new_one
225
+ end
226
+
227
+ # Return +true+ if the factory is required to be able to apply a trait
228
+ # for the host mapper.
229
+ # For example, it is required if its name is listed in +traits+.
230
+ # It is also required if it has nested traits with names listed in +traits+.
231
+ #
232
+ # @param [Array<Symbol>] traits list of traits
233
+ # @return [Boolean]
234
+ def required_for_any_trait?(traits)
235
+ return true unless traited?
236
+
237
+ traits.include?(trait_name) ||
238
+ mapper_class.mountings.any? do |factory|
239
+ factory.traited? &&
240
+ factory.required_for_any_trait?(traits)
241
+ end
242
+ end
243
+ end
244
+ end