flatter 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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