HornsAndHooves-flat_map 0.2.0

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 +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