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,123 @@
1
+ module FlatMap
2
+ # This module hosts all definitions required to define and use mapping
3
+ # functionality within mapper classes. This includes mapping definition
4
+ # methods and basic reading and writing methods.
5
+ module OpenMapper::Mapping
6
+ extend ActiveSupport::Concern
7
+
8
+ # Mapping class macros
9
+ module ClassMethods
10
+ # Mapping-modifier options to distinguish options from mappings
11
+ # themselves:
12
+ MAPPING_OPTIONS = [:reader, :writer, :format, :multiparam].freeze
13
+
14
+ # Define single or multiple mappings at a time. Usually, a Hash
15
+ # is passed in a form !{mapping_name => target_attribute}. All keys
16
+ # that are listed under {MAPPING_OPTIONS} will be extracted and used
17
+ # as modifiers for new mappings.
18
+ #
19
+ # Also, mapping names may be listed as an array preceding the hash.
20
+ # In that case, its elements are treated as !{mapping_name => mapping_name}
21
+ # mapping elements.
22
+ #
23
+ # Example:
24
+ # map :brand, :account_source => :source, :format => :enum
25
+ # # is equivalent to:
26
+ # map :brand => :brand, :format => :enum
27
+ # map :account_source => :source, :format => :enum
28
+ def map(*args)
29
+ mapping_options = args.extract_options!
30
+ mappings = mapping_options.slice!(*MAPPING_OPTIONS)
31
+ mappings_from_array = args.zip(args).flatten
32
+ mappings.merge!(Hash[*mappings_from_array]) unless mappings_from_array.empty?
33
+
34
+ define_mappings(mappings, mapping_options)
35
+ end
36
+
37
+ # Define a set of +mappings+, passed as a {Hash} with +options+ as modifiers.
38
+ # Eventually, adds a mapping factories to list of class mappings. Those
39
+ # factory objects are used to create actual mappings for specific mapper
40
+ # object.
41
+ #
42
+ # @param [Hash] mappings
43
+ # @param [Hash] options
44
+ # @return [Array<FlatMap::Mapping::Factory>] list of mappings
45
+ def define_mappings(mappings, options)
46
+ mappings.each do |name, target_attribute|
47
+ self.mappings << FlatMap::Mapping::Factory.new(name, target_attribute, options)
48
+ end
49
+ end
50
+ private :define_mappings
51
+
52
+ # List of class mappings (mapping factories).
53
+ #
54
+ # @return [Array<FlatMap::Mapping::Factory>]
55
+ def mappings
56
+ @mappings ||= []
57
+ end
58
+
59
+ # Writer for mappings.
60
+ def mappings=(val)
61
+ @mappings = val
62
+ end
63
+ end
64
+
65
+ # Send passed +params+ +write_from_params+ method of each
66
+ # of the mappings of +self+.
67
+ #
68
+ # Overloaded in {OpenMapper::Mounting}.
69
+ #
70
+ # @param [Hash] params
71
+ # @return [Hash] params
72
+ def write(params)
73
+ mappings.each do |mapping|
74
+ mapping.write_from_params(params)
75
+ end
76
+ params
77
+ end
78
+
79
+ # Send +read_as_params+ method to all mappings associated with
80
+ # self. And consolidate results in a single hash.
81
+ #
82
+ # @return [Hash] set of read values
83
+ def read
84
+ mappings.inject({}) do |params, mapping|
85
+ params.merge(mapping.read_as_params)
86
+ end
87
+ end
88
+
89
+ # Retrieve mapping value via its name, which might differ from its
90
+ # full_name, if suffix was used.
91
+ #
92
+ # @param [Symbol] name
93
+ def [](name)
94
+ mapping(name).try(:read)
95
+ end
96
+
97
+ # Write value to mapping specified by name, which might differ from its
98
+ # full_name, if suffix was used.
99
+ #
100
+ # @param [Symbol] name
101
+ # @param [Object] value
102
+ def []=(name, value)
103
+ mapping(name).try(:write, value)
104
+ end
105
+
106
+ # Lookup mapping by its name, which might differ from its
107
+ # full_name, if suffix was used.
108
+ #
109
+ # @param [Symbol] name
110
+ # @return [FlatMap::Mapping]
111
+ def mapping(name)
112
+ mappings.find{ |mapping| mapping.name == name }
113
+ end
114
+
115
+ # Return a list of mappings associated to +self+.
116
+ #
117
+ # @return [FlatMap::Mapping]
118
+ def mappings
119
+ @mappings ||= self.class.mappings.map{ |factory| factory.create(self) }
120
+ end
121
+ private :mappings
122
+ end
123
+ end
@@ -0,0 +1,168 @@
1
+ module FlatMap
2
+ # This module hosts definitions required for mounting functionality
3
+ # of the mappers. This includes mounting definition methods, overloaded
4
+ # +read+ and +write+ methods to make them aware of mounted mappers and
5
+ # other methods.
6
+ #
7
+ # Also, the +method_missing+ method is defined here to delegate the missing
8
+ # method to the very first mounted mapper that responds to it.
9
+ module OpenMapper::Mounting
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ attr_accessor :save_order
14
+ end
15
+
16
+ # Mounting class macros.
17
+ module ClassMethods
18
+ # Add a mounting factory to a list of factories of a class
19
+ # These factories are used to create actual mounted objects,
20
+ # which are mappers themselves, associated to a particular
21
+ # mapper.
22
+ #
23
+ # @param [*Object] args
24
+ # @return [Array<FlatMap::OpenMapper::Factory>]
25
+ def mount(*args, &block)
26
+ mountings << FlatMap::OpenMapper::Factory.new(*args, &block)
27
+ end
28
+
29
+ # List of mountings (factories) of a class.
30
+ #
31
+ # @return [Array<FlatMap::OpenMapper>]
32
+ def mountings
33
+ @mountings ||= []
34
+ end
35
+
36
+ # Writer for @mountings.
37
+ def mountings=(val)
38
+ @mountings = val
39
+ end
40
+ end
41
+
42
+ # Extend original {Mapping#read} method to take
43
+ # into account mountings of mounted mappers.
44
+ #
45
+ # @return [Hash] read values
46
+ def read
47
+ mountings.inject(super) do |result, mapper|
48
+ result.merge(mapper.read)
49
+ end
50
+ end
51
+
52
+ # Extend original {Mapping#write} method to pass
53
+ # +params+ to mounted mappers.
54
+ #
55
+ # Overridden in {Persistence}. Left here for consistency.
56
+ #
57
+ # @param [Hash] params
58
+ # @return [Hash] params
59
+ def write(params)
60
+ super
61
+
62
+ mountings.each do |mapper|
63
+ mapper.write(params)
64
+ end
65
+
66
+ params
67
+ end
68
+
69
+ # Return list of mappings to be saved before saving target of +self+
70
+ #
71
+ # @return [Array<FlatMap::OpenMapper>]
72
+ def before_save_mountings
73
+ nearest_mountings.select{ |mount| mount.save_order == :before }
74
+ end
75
+
76
+ # Return list of mappings to be saved after target of +self+ was saved
77
+ #
78
+ # @return [Array<FlatMap::OpenMapper>]
79
+ def after_save_mountings
80
+ nearest_mountings.reject{ |mount| mount.save_order == :before }
81
+ end
82
+
83
+ # Return all mountings that are mouted on +self+ directly or through
84
+ # traits.
85
+ #
86
+ # @return [Array<FlatMap::OpenMapper>]
87
+ def nearest_mountings
88
+ mountings.map{ |mount| mount.owned? ? mount.nearest_mountings : mount }.flatten
89
+ end
90
+
91
+ # Return a list of all mountings (mapper objects) associated with +self+.
92
+ #
93
+ # Overridden in {Traits}. Left here for consistency.
94
+ #
95
+ # @return [Array<FlatMap::OpenMapper>]
96
+ def mountings
97
+ @mountings ||= self.class.mountings.map{ |factory| factory.create(self) }
98
+ end
99
+
100
+ # Return a mapping with the name that corresponds to passed +mounting_name+,
101
+ # if it exists.
102
+ #
103
+ # @return [FlatMap::Mapping, nil]
104
+ def mounting(mounting_name, is_deep = true)
105
+ list = is_deep ? all_mountings : mountings
106
+ list.find{ |mount| mount.name == mounting_name }
107
+ end
108
+
109
+ # Return a list of all mounted mappers. If +self+ is a trait, return a
110
+ # list of all mountings of the owner. This will allow separate traits
111
+ # to share methods via method_missing pattern.
112
+ #
113
+ # @return [Array<FlatMap::OpenMapper>] mounted mappers (including traits)
114
+ def all_mountings
115
+ return all_nested_mountings.unshift(self) unless owned?
116
+ owner.all_mountings
117
+ end
118
+ protected :all_mountings
119
+
120
+ # Return a list of mountings that are accessible by a named mapper.
121
+ #
122
+ # @return [Array<FlatMap::OpenMapper>]
123
+ def all_nested_mountings
124
+ mountings.dup.concat(mountings.map{ |mount| mount.send(:all_nested_mountings) }).flatten
125
+ end
126
+ protected :all_nested_mountings
127
+
128
+ # Return a list of all mappings, i.e. mappings associated to +self+,
129
+ # plus mappings of all deeply mounted mappers. If +self+ is the owner,
130
+ # that means it is a part (a trait) of a host mapper. That means that all
131
+ # mappings of it actually correspond to all mappings of a host mapper.
132
+ # This allows to define things like validation in a trait where access
133
+ # to top-level mappings is required.
134
+ #
135
+ # @return [Array<FlatMap::Mapping>]
136
+ def all_mappings
137
+ return all_nested_mappings unless owned?
138
+ owner.all_mappings
139
+ end
140
+ protected :all_mappings
141
+
142
+ # Return a list of all mappings, i.e. mappings that associated to +self+
143
+ # plus mappings of all deeply mounted mappers.
144
+ #
145
+ # @return [Array<FlatMap::Mapping>]
146
+ def all_nested_mappings
147
+ (mappings + mountings.map{ |mount| mount.send(:all_nested_mappings) }).flatten
148
+ end
149
+ protected :all_nested_mappings
150
+
151
+ # Delegate missing method to any mounted mapping that respond to it,
152
+ # unless those methods are protected methods of FlatMap::OpenMapper.
153
+ #
154
+ # NOTE: :to_ary method is called internally by Ruby 1.9.3 when we call
155
+ # something like [mapper].flatten. And we DO want default behavior
156
+ # for handling this missing method.
157
+ def method_missing(name, *args, &block)
158
+ return super if name == :to_ary ||
159
+ self.class.protected_instance_methods.include?(name)
160
+
161
+ return self[name] if mapping(name).present?
162
+
163
+ mounting = all_mountings.find{ |mount| mount.respond_to?(name) }
164
+ return super if mounting.nil?
165
+ mounting.send(name, *args, &block)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,178 @@
1
+ module FlatMap
2
+ # This module provides some integration between mapper and its target,
3
+ # which is usually an ActiveRecord model, as well as some integration
4
+ # between mapper and Rails forms.
5
+ #
6
+ # In particular, validation and save methods are defined here. And
7
+ # the <tt>save</tt> method itself is defined as a callback. Also, Rails
8
+ # multiparam attributes extraction is defined within this module.
9
+ module OpenMapper::Persistence
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ define_callbacks :save
14
+ end
15
+
16
+ # ModelMethods class macros
17
+ module ClassMethods
18
+ # Create a new mapper object wrapped around new instance of its
19
+ # +target_class+, with a list of passed +traits+ applied to it.
20
+ #
21
+ # @param [*Symbol] traits
22
+ # @return [FlatMap::OpenMapper] mapper
23
+ def build(*traits, &block)
24
+ new(target_class.new, *traits, &block)
25
+ end
26
+
27
+ # Default target class for OpenMapper is OpenStruct.
28
+ #
29
+ # @return [Class] class
30
+ def target_class
31
+ OpenStruct
32
+ end
33
+ end
34
+
35
+ # Write a passed set of +params+. Then try to save the model if +self+
36
+ # passes validation. Saving is performed in a transaction.
37
+ #
38
+ # @param [Hash] params
39
+ # @return [Boolean]
40
+ def apply(params)
41
+ write(params)
42
+ valid? && save
43
+ end
44
+
45
+ # Extract the multiparam values from the passed +params+. Then use the
46
+ # resulting hash to assign values to the target. Assignment is performed
47
+ # by sending writer methods to +self+ that correspond to keys in the
48
+ # resulting +params+ hash.
49
+ #
50
+ # @param [Hash] params
51
+ # @return [Hash] params
52
+ def write(params)
53
+ extract_multiparams!(params)
54
+
55
+ params.each do |name, value|
56
+ self.send("#{name}=", value)
57
+ end
58
+ end
59
+
60
+ # Try to save the target and send a +save+ method to all mounted mappers.
61
+ #
62
+ # The order in which mappings are saved is important, since we save
63
+ # records with :validate => false option. Since Rails will perform
64
+ # auto-saving on associations (and it in its turn will try to save associated
65
+ # record with :validate => true option. To be more precise, with
66
+ # :validate => !autosave option, where autosave corresponds to that option
67
+ # of reflection, which is usually not specified, i.e. nil), we might come to
68
+ # a situation of saving a record with nil foreign key for belongs_to association,
69
+ # which will raise exception. Thus, we want to explicitly save records in
70
+ # order which will allow them to be saved.
71
+ # Return +false+ if that chain of +save+ calls returns +true+ on any of
72
+ # its elements. Return +true+ otherwise.
73
+ #
74
+ # Saving is performed as a callback.
75
+ #
76
+ # @return [Boolean]
77
+ def save
78
+ before_res = save_mountings(before_save_mountings)
79
+ target_res = self_mountings.map{ |mount| mount.shallow_save }.all?
80
+ after_res = save_mountings(after_save_mountings)
81
+
82
+ before_res && target_res && after_res
83
+ end
84
+
85
+ # Return +true+ since OpenStruct is always 'saved'.
86
+ #
87
+ # @return [true]
88
+ def save_target
89
+ true
90
+ end
91
+
92
+ # Perform target save with callbacks call
93
+ #
94
+ # @return [Boolean]
95
+ def shallow_save
96
+ run_callbacks(:save){ save_target }
97
+ end
98
+
99
+ # Return +true+ if target was updated.
100
+ #
101
+ # @return [Boolean]
102
+ def persisted?
103
+ target != OpenStruct.new
104
+ end
105
+
106
+ # Send <tt>:save</tt> method to all mountings in list. Will return +true+
107
+ # only if all savings are positive.
108
+ #
109
+ # @param [Array<FlatMap::OpenMapper>] mountings
110
+ # @return [Boolean]
111
+ def save_mountings(mountings)
112
+ mountings.map{ |mount| mount.save }.all?
113
+ end
114
+ private :save_mountings
115
+
116
+ # Return +true+ if the mapper is valid, i.e. if it is valid itself, and if
117
+ # all mounted mappers (traits and other mappers) are also valid.
118
+ #
119
+ # Accepts any parameters, but doesn't use them to be compatible with
120
+ # ActiveRecord calls.
121
+ #
122
+ # @return [Boolean]
123
+ def valid?(*)
124
+ res = trait_mountings.map(&:valid?).all?
125
+ res = super && res # we do want to call super
126
+ mounting_res = mapper_mountings.map(&:valid?).all?
127
+ consolidate_errors!
128
+ res && mounting_res
129
+ end
130
+
131
+ # Consolidate the errors of all mounted mappers to a set of errors of +self+.
132
+ #
133
+ # @return [Array<ActiveModel::Errors>]
134
+ def consolidate_errors!
135
+ mountings.map(&:errors).each do |errs|
136
+ errors.messages.merge!(errs.to_hash){ |key, old, new| old.concat(new) }
137
+ end
138
+ end
139
+ private :consolidate_errors!
140
+
141
+ # Overridden to use {FlatMap::Errors} instead of ActiveModel ones.
142
+ #
143
+ # @return [FlatMap::Errors]
144
+ def errors
145
+ @errors ||= FlatMap::Errors.new(self)
146
+ end
147
+
148
+ # Extract Rails multiparam parameters from the +params+, modifying
149
+ # original hash. Behaves somewhat like
150
+ # {ActiveRecord::AttributeAssignment#extract_callstack_for_multiparameter_attributes}
151
+ # See this method for more details.
152
+ #
153
+ # @param [Hash] params
154
+ # @return [Array<FlatMap::Mapping>] return value is not used, original
155
+ # +params+ hash is modified instead and used later on.
156
+ def extract_multiparams!(params)
157
+ all_mappings.select(&:multiparam?).each do |mapping|
158
+ param_keys = params.keys.
159
+ select { |key| key.to_s =~ /#{mapping.full_name}\(\d+[isf]\)/ }.
160
+ sort_by{ |key| key.to_s[/\((\d+)\w*\)/, 1].to_i }
161
+
162
+ next if param_keys.empty?
163
+
164
+ args = param_keys.inject([]) do |values, key|
165
+ value = params.delete key
166
+ type = key[/\(\d+(\w*)\)/, 1]
167
+ value = value.send("to_#{type}") unless type.blank?
168
+
169
+ values.push value
170
+ values
171
+ end
172
+
173
+ params[mapping.name] = mapping.multiparam.new(*args) rescue nil
174
+ end
175
+ end
176
+ private :extract_multiparams!
177
+ end
178
+ end