flatter 0.1.0 → 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.
data/flatter.gemspec CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency "bundler", "~> 1.10"
30
30
  spec.add_development_dependency "rake", "~> 10.0"
31
31
  spec.add_development_dependency "rspec"
32
+ spec.add_development_dependency "rspec-its"
32
33
  spec.add_development_dependency "simplecov", ">= 0.9"
33
34
  spec.add_development_dependency "pry"
34
35
  spec.add_development_dependency "pry-nav"
@@ -1,7 +1,8 @@
1
1
  module Flatter
2
2
  module Mapper::AttributeMethods
3
3
  def respond_to_missing?(name, *)
4
- mapping_names.map{ |name| [name, :"#{name}="] }.flatten.include?(name) || super
4
+ acceptable = mapping_names.map{ |name| [name, "#{name}="] }.flatten + mounting_names
5
+ acceptable.uniq.map(&:to_sym).include?(name) || super
5
6
  end
6
7
 
7
8
  def method_missing(name, *args, &block)
@@ -13,13 +14,79 @@ module Flatter
13
14
  send(name, *args, &block)
14
15
  end
15
16
 
17
+ def mounting(name)
18
+ find_mounting(name.to_s)
19
+ end
20
+
21
+ def find_mounting(name)
22
+ local_mountings.each do |mounting|
23
+ if mounting.name == name || (mounting.pluralized? && mounting.name.pluralize == name)
24
+ return mounting
25
+ end
26
+ nested = mounting.find_mounting(name)
27
+ return nested if nested.present?
28
+ end
29
+ nil
30
+ end
31
+ protected :find_mounting
32
+
33
+ def find_mounting_with(mapping_name)
34
+ mapping_name = mapping_name.to_s
35
+
36
+ match = local_mappings.any? do |mapping|
37
+ if collection? || pluralized?
38
+ mapping.name.pluralize == mapping_name
39
+ else
40
+ mapping.name == mapping_name
41
+ end
42
+ end
43
+
44
+ return self if match
45
+
46
+ local_mountings.each do |mounting|
47
+ nested = mounting.find_mounting_with(mapping_name)
48
+ return nested if nested.present?
49
+ end
50
+
51
+ nil
52
+ end
53
+ protected :find_mounting_with
54
+
16
55
  def attribute_methods
17
- names = mapping_names
56
+ _mapping_names = mapping_names
57
+ _mounting_names = mounting_names - _mapping_names
58
+
18
59
  Module.new do
19
- names.each do |name|
20
- define_method(name){ |*args| mapping(name).read(*args) }
60
+ _mounting_names.each do |name|
61
+ define_method(name) do
62
+ mount = find_mounting(name)
63
+ if mount.collection?
64
+ mount.read[name.to_s]
65
+ elsif mount.pluralized?
66
+ Array(mountings[mount.name]).map(&:read)
67
+ else
68
+ mount.read
69
+ end
70
+ end
71
+ end
72
+
73
+ _mapping_names.each do |name|
74
+ define_method(name) do |*args|
75
+ mount = find_mounting_with(name)
76
+ if mount.collection? || mount.pluralized?
77
+ Array(mapping(name.singularize)).map{ |map| map.read(*args) }
78
+ else
79
+ mapping(name).read(*args)
80
+ end
81
+ end
21
82
 
22
- define_method(:"#{name}="){ |value| mapping(name).write(value) }
83
+ define_method("#{name}=") do |value|
84
+ mount = find_mounting_with(name)
85
+ if mount.collection? || mount.pluralized?
86
+ fail RuntimeError, "Cannot directly write to a collection"
87
+ end
88
+ mapping(name).write(value)
89
+ end
23
90
  end
24
91
  end
25
92
  end
@@ -0,0 +1,193 @@
1
+ module Flatter
2
+ module Mapper::Collection
3
+ NonUniqKeysError = Class.new(RuntimeError)
4
+
5
+ def self.prepended(base)
6
+ base.send(:include, Concern)
7
+ end
8
+
9
+ module FactoryMethods
10
+ def create(*)
11
+ super.tap do |mapper|
12
+ mapper.options.merge!(collection: collection?)
13
+ end
14
+ end
15
+
16
+ def default_mapper_class_name
17
+ collection? ? "#{name.singularize.camelize}Mapper" : super
18
+ end
19
+
20
+ def collection?
21
+ options[:collection] == true ||
22
+ (options[:collection] != false && name == name.pluralize)
23
+ end
24
+ end
25
+
26
+ module Concern
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ mapper_options.push(:collection, :item_index)
31
+ attr_accessor :item_index
32
+ end
33
+
34
+ module ClassMethods
35
+ def key(arg = nil)
36
+ args = []
37
+ options = {writer: false}
38
+
39
+ case arg
40
+ when String, Symbol
41
+ options[:key] = arg.to_sym
42
+ when Proc
43
+ args << :key
44
+ options[:reader] = arg
45
+ else
46
+ fail ArgumentError, "Cannot use '#{arg}' as collection key"
47
+ end
48
+
49
+ map *args, **options
50
+ end
51
+ end
52
+
53
+ def remove_items(keys)
54
+ collection.reject! do |item|
55
+ (item[:key].nil? || keys.include?(item[:key])) &&
56
+ delete_target_item(item.target)
57
+ end
58
+ end
59
+ private :remove_items
60
+
61
+ def delete_target_item(item)
62
+ !!target.delete(item)
63
+ end
64
+
65
+ def update_item(key, params)
66
+ collection.find{ |item| item[:key] == key }.write(params)
67
+ end
68
+
69
+ def add_item(params)
70
+ collection << clone.tap do |mapper|
71
+ item = target_class.new
72
+ mapper.reset_locals!
73
+ mapper.set_target!(item)
74
+ mapper.item_index = collection.length
75
+ mapper.write(params)
76
+ add_target_item(item)
77
+ end
78
+ end
79
+
80
+ def add_target_item(item)
81
+ target << item
82
+ end
83
+ end
84
+
85
+ def read
86
+ return super unless collection?
87
+
88
+ values = collection.map(&:read)
89
+
90
+ assert_key_uniqueness!(values)
91
+
92
+ {name => values}
93
+ end
94
+
95
+ def write(params)
96
+ return super unless collection?
97
+ return unless params.key?(name)
98
+
99
+ data = params[name]
100
+ assert_collection!(data)
101
+
102
+ keys = collection.map(&:key)
103
+ remove_items(keys - data.map{ |p| p[:key] })
104
+
105
+ data.each do |params|
106
+ if params.key?(:key)
107
+ update_item(params[:key], params.except(:key))
108
+ else
109
+ add_item(params)
110
+ end
111
+ end
112
+ end
113
+
114
+ def pluralize!
115
+ @_pluralized = true
116
+ end
117
+
118
+ def pluralized?
119
+ !!@_pluralized
120
+ end
121
+
122
+ def mapping_names
123
+ super.map{ |name| collection? || pluralized? ? name.pluralize : name }
124
+ end
125
+
126
+ def mounting_names
127
+ super.map{ |name| pluralized? ? name.pluralize : name }
128
+ end
129
+
130
+ def local_mountings
131
+ super.each{ |mapper| mapper.pluralize! if collection? || pluralized? }
132
+ end
133
+ protected :local_mountings
134
+
135
+ def assert_key_uniqueness!(values)
136
+ keys = values.map{ |v| v['key'] }.compact
137
+ keys == keys.uniq or
138
+ fail NonUniqKeysError, "All keys in collection '#{name}' should be uniq, but were not"
139
+ end
140
+ private :assert_key_uniqueness!
141
+
142
+ def assert_collection!(data)
143
+ unless data.respond_to?(:each)
144
+ fail ArgumentError, "Cannot write to '#{name}': argument is not a collection"
145
+ end
146
+ end
147
+ private :assert_collection!
148
+
149
+ def collection
150
+ return nil unless collection?
151
+
152
+ @collection ||= target.each.with_index.map do |item, index|
153
+ clone.tap do |mapper|
154
+ mapper.reset_locals!
155
+ mapper.set_target! item
156
+ mapper.item_index = index
157
+ end
158
+ end
159
+ end
160
+
161
+ def prefix
162
+ return super if mounter.nil?
163
+
164
+ [mounter.prefix, item_name].compact.join(?.).presence
165
+ end
166
+ protected :prefix
167
+
168
+ def item_name
169
+ "#{name}.#{item_index}" if item_index.present?
170
+ end
171
+ protected :item_name
172
+
173
+ def as_inner_mountings
174
+ if collection?
175
+ ensure_target!
176
+ collection.map{ |item| item.as_inner_mountings }
177
+ else
178
+ super
179
+ end
180
+ end
181
+ protected :as_inner_mountings
182
+
183
+ def reset_locals!
184
+ @_local_mappings = nil
185
+ @_local_mountings = nil
186
+ end
187
+ protected :reset_locals!
188
+
189
+ def collection?
190
+ options[:collection] && item_index.nil?
191
+ end
192
+ end
193
+ end
@@ -4,6 +4,9 @@ module Flatter
4
4
  prepend Flatter::Mapper::Mounting::FactoryMethods
5
5
  prepend Flatter::Mapper::Traits::FactoryMethods
6
6
  prepend Flatter::Mapper::Options::FactoryMethods
7
+ prepend Flatter::Mapper::Collection::FactoryMethods
8
+
9
+ NoTargetError = Class.new(RuntimeError)
7
10
 
8
11
  attr_reader :name, :options
9
12
 
@@ -16,19 +19,36 @@ module Flatter
16
19
  end
17
20
 
18
21
  def mapper_class_name
19
- options[:mapper_class_name] || "#{name.to_s.camelize}Mapper"
22
+ options[:mapper_class_name] || modulize(default_mapper_class_name)
23
+ end
24
+
25
+ def default_mapper_class_name
26
+ "#{name.camelize}Mapper"
27
+ end
28
+
29
+ def create(*)
30
+ mapper_class.new.tap{ |mapper| mapper.factory = self }
20
31
  end
21
32
 
22
- def create(mapper)
23
- mapper_class.new(fetch_target_from(mapper))
33
+ def modulize(class_name)
34
+ if i = options[:mounter_name].rindex('::')
35
+ "#{options[:mounter_name][0...i]}::#{class_name}"
36
+ else
37
+ class_name
38
+ end
24
39
  end
40
+ private :modulize
25
41
 
26
42
  def fetch_target_from(mapper)
27
43
  default_target_from(mapper)
28
44
  end
29
45
 
30
46
  def default_target_from(mapper)
31
- mapper.target.public_send(name) if mapper.target.respond_to?(name)
47
+ if mapper.target.respond_to?(name)
48
+ mapper.target.public_send(name)
49
+ else
50
+ fail NoTargetError, "Unable to implicitly fetch target for '#{name}' from #{mapper}"
51
+ end
32
52
  end
33
53
  private :default_target_from
34
54
  end
@@ -37,15 +37,13 @@ module Flatter
37
37
  end
38
38
 
39
39
  def write(params)
40
- params = params.with_indifferent_access
41
40
  local_mappings.each{ |mapping| mapping.write_from_params(params) }
42
-
43
- params
44
41
  end
45
42
 
46
43
  def local_mappings
47
44
  @_local_mappings ||= self.class.mappings.values.map{ |factory| factory.create(self) }
48
45
  end
46
+ protected :local_mappings
49
47
 
50
48
  def mappings
51
49
  local_mappings.each_with_object({}) do |mapping, res|
@@ -54,7 +52,7 @@ module Flatter
54
52
  end
55
53
 
56
54
  def mapping_names
57
- @_mapping_names ||= mappings.keys
55
+ local_mappings.map(&:name)
58
56
  end
59
57
 
60
58
  def writable_mapping_names
@@ -11,9 +11,14 @@ module Flatter
11
11
  end
12
12
  end
13
13
 
14
+ included do
15
+ class_attribute :label
16
+ end
17
+
14
18
  module ClassMethods
15
- def mount(name, *args)
16
- mountings[name.to_s] = Flatter::Mapper::Factory.new(name, *args)
19
+ def mount(name, **opts)
20
+ factory_options = opts.reverse_merge(mounter_name: self.name || label)
21
+ mountings[name.to_s] = Flatter::Mapper::Factory.new(name, **factory_options)
17
22
  end
18
23
 
19
24
  def mountings
@@ -35,26 +40,30 @@ module Flatter
35
40
  super.tap do |mappings|
36
41
  inner_mountings.each do |mounting|
37
42
  mounting.local_mappings.each do |mapping|
38
- mappings[mapping.name] = mapping
43
+ mappings.merge!(mapping.name => mapping, &merging_proc)
39
44
  end
40
45
  end
41
46
  end
42
47
  end
43
48
 
49
+ def mapping_names
50
+ super + local_mountings.map(&:mapping_names).flatten
51
+ end
52
+
44
53
  def read
45
- inner_mountings.map(&:read).inject(super, :merge)
54
+ local_mountings.map(&:read).inject(super, :merge)
46
55
  end
47
56
 
48
57
  def write(params)
49
- super.tap do
50
- inner_mountings.each{ |mapper| mapper.write(params) }
51
- end
58
+ super
59
+ local_mountings.each{ |mapper| mapper.write(params) }
60
+ @_inner_mountings = nil
52
61
  end
53
62
 
54
63
  def local_mountings
55
64
  class_mountings_for(self.class)
56
65
  end
57
- private :local_mountings
66
+ protected :local_mountings
58
67
 
59
68
  def class_mountings_for(klass)
60
69
  class_mountings(klass).map{ |factory| factory.create(self) }
@@ -67,18 +76,33 @@ module Flatter
67
76
  private :class_mountings
68
77
 
69
78
  def mountings
70
- @mountings ||= inner_mountings.each_with_object({}) do |mapper, res|
71
- res[mapper.full_name] = mapper
79
+ @mountings ||= inner_mountings.inject({}) do |res, mapper|
80
+ res.merge(mapper.full_name => mapper, &merging_proc)
72
81
  end
73
82
  end
74
83
 
75
- def mounting(name)
76
- mountings[name.to_s]
84
+ def mounting_names
85
+ local_mounting_names + local_mountings.map(&:mounting_names).flatten
77
86
  end
78
87
 
88
+ def local_mounting_names
89
+ local_mountings.map(&:name)
90
+ end
91
+ private :local_mounting_names
92
+
79
93
  def inner_mountings
80
- @_inner_mountings ||= local_mountings.map{ |mount| [mount, mount.inner_mountings] }.flatten
94
+ @_inner_mountings ||= local_mountings.map{ |mount| mount.as_inner_mountings }.flatten
81
95
  end
82
96
  protected :inner_mountings
97
+
98
+ def as_inner_mountings
99
+ [self, inner_mountings]
100
+ end
101
+ protected :as_inner_mountings
102
+
103
+ def merging_proc
104
+ proc { |_, old, new| Array(old).push(new) }
105
+ end
106
+ private :merging_proc
83
107
  end
84
108
  end
@@ -19,7 +19,7 @@ module Flatter
19
19
  attr_reader :options
20
20
 
21
21
  def initialize(*, **options)
22
- @options = options
22
+ @options = options
23
23
  end
24
24
  end
25
25
  end
@@ -61,12 +61,21 @@ module Flatter
61
61
  private :self_mountings
62
62
 
63
63
  def consolidate_errors!
64
- root_mountings.map(&:errors).each do |errs|
65
- errors.messages.merge!(errs.to_hash){ |key, old, new| old + new }
64
+ root_mountings.each do |mounting|
65
+ prefix = mounting.prefix
66
+ mounting.errors.to_hash.each do |name, errs|
67
+ error_key = [prefix, name].compact.join('.')
68
+ errors.messages.merge!(error_key.to_sym => errs){ |key, old, new| old + new }
69
+ end
66
70
  end
67
71
  end
68
72
  private :consolidate_errors!
69
73
 
74
+ def prefix
75
+ nil
76
+ end
77
+ protected :prefix
78
+
70
79
  def errors
71
80
  trait? ? mounter.errors : super
72
81
  end
@@ -1,7 +1,13 @@
1
1
  module Flatter
2
2
  module Mapper::Target
3
+ extend ActiveSupport::Concern
4
+
3
5
  NoTargetError = Class.new(ArgumentError)
4
6
 
7
+ included do
8
+ mapper_options << :target_class_name
9
+ end
10
+
5
11
  module FactoryMethods
6
12
  def fetch_target_from(mapper)
7
13
  return super unless options.key?(:target)
@@ -19,20 +25,60 @@ module Flatter
19
25
  end
20
26
  end
21
27
 
22
- attr_reader :target
23
-
24
- def initialize(target, *)
25
- unless target.present?
26
- fail NoTargetError, "Target object is required to initialize #{self.class.name}"
27
- end
28
+ attr_accessor :factory
28
29
 
30
+ def initialize(target = nil, *)
29
31
  super
32
+ set_target!(target) if target.present?
33
+ end
30
34
 
31
- @target = target
35
+ def target
36
+ ensure_target!
37
+ @target
38
+ end
39
+
40
+ def ensure_target!
41
+ initialize_target unless target_initialized?
42
+ end
43
+ protected :ensure_target!
44
+
45
+ def initialize_target
46
+ return set_target!(mounter.target) if trait?
47
+
48
+ _mounter = mounter.trait? ? mounter.mounter : mounter
49
+ set_target!(factory.fetch_target_from(_mounter))
32
50
  end
51
+ private :initialize_target
33
52
 
34
53
  def set_target(target)
54
+ if trait?
55
+ mounter.set_target!(target)
56
+ else
57
+ set_target!(target)
58
+ trait_mountings.each{ |trait| trait.set_target!(target) }
59
+ end
60
+ end
61
+
62
+ def set_target!(target)
63
+ fail NoTargetError, "Cannot set nil target for #{self.class.name}" if target.nil?
64
+ @_target_initialized = true
35
65
  @target = target
36
66
  end
67
+
68
+ def target_initialized?
69
+ !!@_target_initialized
70
+ end
71
+
72
+ def target_class
73
+ target_class_name.constantize
74
+ end
75
+
76
+ def target_class_name
77
+ options[:target_class_name] || default_target_class_name
78
+ end
79
+
80
+ def default_target_class_name
81
+ self.class.name.sub 'Mapper', ''
82
+ end
37
83
  end
38
84
  end
@@ -20,10 +20,6 @@ module Flatter
20
20
  mounting.extend_with(extension) if extension.present?
21
21
  end
22
22
  end
23
-
24
- def fetch_target_from(mapper)
25
- trait? ? mapper.target : super
26
- end
27
23
  end
28
24
 
29
25
  module ClassMethods
@@ -31,9 +27,11 @@ module Flatter
31
27
  super.tap{ |f| f.extension = block }
32
28
  end
33
29
 
34
- def trait(name, &block)
30
+ def trait(name, label = nil, &block)
35
31
  trait_name = "#{name}_trait"
36
- mapper_class = Class.new(Flatter::Mapper, &block)
32
+ mapper_class = Class.new(Flatter::Mapper)
33
+ mapper_class.label = self.name || label
34
+ mapper_class.class_eval(&block)
37
35
 
38
36
  if self.name.present?
39
37
  mapper_class_name = trait_name.camelize
@@ -46,7 +44,7 @@ module Flatter
46
44
  end
47
45
  end
48
46
 
49
- def initialize(target, *traits, **, &block)
47
+ def initialize(_, *traits, **, &block)
50
48
  super
51
49
 
52
50
  set_traits(traits)
@@ -54,22 +52,8 @@ module Flatter
54
52
  end
55
53
 
56
54
  def extend_with(extension)
57
- singleton_class.trait :extension, &extension
58
- end
59
-
60
- def set_target(target)
61
- if trait?
62
- mounter.set_target(target)
63
- else
64
- super
65
- trait_mountings.each{ |trait| trait.set_target!(target) }
66
- end
67
- end
68
-
69
- def set_target!(target)
70
- @target = target
55
+ singleton_class.trait :extension, self.class.name, &extension
71
56
  end
72
- protected :set_target!
73
57
 
74
58
  def full_name
75
59
  if name == 'extension_trait'
@@ -80,7 +64,7 @@ module Flatter
80
64
  end
81
65
 
82
66
  def local_mountings
83
- @local_mountings ||= class_mountings_for(singleton_class) + super
67
+ @_local_mountings ||= class_mountings_for(singleton_class) + super
84
68
  end
85
69
  private :local_mountings
86
70
 
@@ -126,6 +110,11 @@ module Flatter
126
110
  @trait = true
127
111
  end
128
112
 
113
+ def local_mounting_names
114
+ super.reject{ |name| trait_mountings.any?{ |mount| mount.name == name } }
115
+ end
116
+ private :local_mounting_names
117
+
129
118
  def trait_mountings
130
119
  @_trait_mountings ||= local_mountings.select(&:trait?)
131
120
  end
@@ -0,0 +1,8 @@
1
+ module Flatter
2
+ module Mapper::WriteWithIndifferentAccess
3
+ def write(params)
4
+ super(params.with_indifferent_access)
5
+ params
6
+ end
7
+ end
8
+ end
@@ -4,23 +4,27 @@ module Flatter
4
4
 
5
5
  autoload :Factory
6
6
  autoload :Options
7
- autoload :Target
8
7
  autoload :Mapping
9
8
  autoload :Mounting
10
9
  autoload :Traits
10
+ autoload :Target
11
11
  autoload :AttributeMethods
12
12
  autoload :Persistence
13
13
  autoload :ModelName
14
+ autoload :Collection
15
+ autoload :WriteWithIndifferentAccess
14
16
 
15
17
  include Options
16
- include Target
17
18
  include Mapping
18
19
  include Mounting
19
20
  include Traits
21
+ include Target
20
22
  include AttributeMethods
21
23
  include ActiveModel::Validations
22
24
  include Persistence
23
25
  prepend ModelName
26
+ prepend Collection
27
+ prepend WriteWithIndifferentAccess
24
28
 
25
29
  def self.inherited(subclass)
26
30
  subclass.mappings = mappings.dup
@@ -1,3 +1,3 @@
1
1
  module Flatter
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end