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