HornsAndHooves-flat_map 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 23416b70e99b971920d90bd08042a9eb6546c491
4
- data.tar.gz: 3cbae8bc6cae945dcddf3227747c01f2828661a4
2
+ SHA256:
3
+ metadata.gz: 4db8b5781fc2f7f28c54c20e3700d40c9b10f32e6cdb19d37a208a3bfc717f7a
4
+ data.tar.gz: e8af14f2b65a782da9c337f6121902b66bcf26e2168d6a4cfc3bacbc8a300323
5
5
  SHA512:
6
- metadata.gz: 7842dba339d91147a548440e9fe725a6646349854f59c1723e3683bef6fde7a2b6c956bf550496331dcf378e4281b0264f7530355d94caef59ffcf62715feaae
7
- data.tar.gz: 848349e424a794e646cedee5e1ac3cdfc23129bd695d61914222ff1a29fe41079f54284f1f632fabe4f866bb28f4d1e69092f83ca52222716b20f3dacdb086e9
6
+ metadata.gz: 3467cd650843ef17dac92e7ae48ddfcae67fee7ce459cfe11f86fc814f09f1d325e1173c1c38aea454cfdfafc0dde3b4336013d827b31ed3cfd50bf06c5e87a9
7
+ data.tar.gz: 027eeff4f646833fcafd9efeefa0efee4c11b9b93d523a82d407c6ca905d920f497f0283fe69282cfe8d372fd3255655b8d5d5208a37b729c4afa29c63886afd
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.2
1
+ 2.6.6
data/Gemfile CHANGED
@@ -3,19 +3,16 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in flat_map.gemspec
4
4
  gemspec
5
5
 
6
- group :development do
7
- gem 'redcarpet'
8
- gem 'yard'
9
- end
6
+ gem "activerecord"
7
+ gem "activesupport"
10
8
 
11
- group :development, :test do
12
- # code metrics:
13
- gem "metric_fu"
14
- gem "pry"
15
- end
9
+ gem 'redcarpet'
10
+ gem 'yard'
16
11
 
17
- group :test do
18
- gem 'colorize' , :require => false
19
- gem 'simplecov' , :require => false
20
- gem 'simplecov-rcov-text', :require => false
21
- end
12
+ # code metrics:
13
+ gem "metric_fu"
14
+ gem "pry"
15
+
16
+ gem 'colorize' , :require => false
17
+ gem 'simplecov' , :require => false
18
+ gem 'simplecov-rcov-text', :require => false
@@ -19,11 +19,11 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  # specify any dependencies here; for example:
22
- s.add_dependency(%q<activesupport>, ["~> 4.2"])
23
- s.add_dependency(%q<activerecord>, ["~> 4.2"])
24
- s.add_dependency(%q<yard>, [">= 0"])
22
+ s.add_dependency "activesupport", ">= 5", "< 7.0.0"
23
+ s.add_dependency "activerecord", ">= 5", "< 7.0.0"
24
+ s.add_dependency "yard"
25
25
 
26
- s.add_development_dependency "rspec", ['~> 3.5']
26
+ s.add_development_dependency "rspec", '~> 3.5'
27
27
  s.add_development_dependency "rspec-its"
28
28
  s.add_development_dependency "rake"
29
29
  end
@@ -0,0 +1,148 @@
1
+ module FlatMap
2
+ # This module enhances and modifies original FlatMap::OpenMapper::Persistence
3
+ # functionality for ActiveRecord models as targets.
4
+ module ModelMapper::Associations
5
+ extend ActiveSupport::Concern
6
+
7
+ # Raised when there is no active record association between models.
8
+ class AssociationError < StandardError; end
9
+
10
+ # ModelMethods class macros
11
+ module ClassMethods
12
+ # Build relation for given traits.
13
+ # Allows to create relation which loads data from all tables related to given traits.
14
+ # For example:
15
+ #
16
+ # class CommentMapper < FlatMap::ModelMapper
17
+ # end
18
+ #
19
+ # class ArticleMapper < FlatMap::ModelMapper
20
+ # trait :with_comments do
21
+ # mount :comments, mapper_class: CommentMapper
22
+ # end
23
+ # end
24
+ #
25
+ # Following call:
26
+ # ArticleMapper.relation(%i[with_comments]).where(...)
27
+ #
28
+ # is same as:
29
+ # Article.includes(:comments).where(...)
30
+ #
31
+ #
32
+ # @param traits [Array<Symbol>]
33
+ # @return [ActiveRecord::Relation]
34
+ def relation(traits)
35
+ trait_associations = associations(traits)
36
+ target_class.includes(trait_associations).references(trait_associations)
37
+ end
38
+
39
+ # Return associations list for given traits based on current mapper mounting.
40
+ # This method allows to receive associations list for given traits.
41
+ # Then associations list could be used as parameters for .joins method
42
+ # to build active record relation to select data form tables related to traits.
43
+ # For example:
44
+ #
45
+ # class AuthorMapper < FlatMap::ModelMapper
46
+ # end
47
+ #
48
+ # class TagMapper < FlatMap::ModelMapper
49
+ # end
50
+ #
51
+ # class CommentMapper < FlatMap::ModelMapper
52
+ # trait :with_author do
53
+ # mount :author, mapper_class: AuthorMapper
54
+ # end
55
+ # end
56
+ #
57
+ # class ArticleMapper < FlatMap::ModelMapper
58
+ # trait :with_comments do
59
+ # mount :comments, mapper_class: CommentMapper
60
+ # end
61
+ #
62
+ # trait :with_tags do
63
+ # mount :tags, mapper_class: TagMapper
64
+ # end
65
+ # end
66
+ #
67
+ # ArticleMapper.associations(%i[ with_comments ])
68
+ # => :comments
69
+ #
70
+ # ArticleMapper.associations(%i[ with_tags ])
71
+ # => :tags
72
+ #
73
+ # ArticleMapper.associations(%i[ with_comments with_tags ])
74
+ # => [:comments, :tags]
75
+ #
76
+ # ArticleMapper.associations(%i[ with_comments with_author ])
77
+ # => { comments: :author }
78
+ #
79
+ #
80
+ # @param traits [Array<Symbol>]
81
+ # @return [Array,Hash]
82
+ def associations(traits)
83
+ build_associations(traits, target_class, false)
84
+ end
85
+
86
+ # Return associations list for given traits based on current mapper mounting.
87
+ #
88
+ # @param traits [Array<Symbol>]
89
+ # @param context [ActiveRecord::Base]
90
+ # @param include_self [Boolean]
91
+ # @return [Array,Hash]
92
+ protected def build_associations(traits, context, include_self)
93
+ classes_list = find_dependency_classes(traits)
94
+
95
+ map_classes_to_associations(context, classes_list, include_self)
96
+ end
97
+
98
+ # Return associations list for given traits based on current mapper mounting.
99
+ #
100
+ # @param traits [Array<Symbol>]
101
+ # @return [Array<Symbol>]
102
+ private def find_dependency_classes(traits)
103
+ mountings.map do |mounting|
104
+ mapper_class = mounting.mapper_class
105
+
106
+ if mounting.traited?
107
+ mapper_class.build_associations(traits, target_class, false) if traits.include?(mounting.trait_name)
108
+ else
109
+ mapper_class.build_associations(traits, target_class, true)
110
+ end
111
+ end.compact
112
+ end
113
+
114
+ # Map given classes to association object names.
115
+ #
116
+ # @param context [ActiveRecord::Base]
117
+ # @param classes_list [Array<ActiveRecord::Base>]
118
+ # @param include_self [Boolean]
119
+ # @return [Symbol,Array,Hash,nil]
120
+ private def map_classes_to_associations(context, classes_list, include_self)
121
+ if classes_list.empty?
122
+ include_self ? association_for_class(context) : nil
123
+ else
124
+ classes_list = classes_list.first if classes_list.length == 1
125
+
126
+ include_self ? { association_for_class(context) => classes_list } : classes_list
127
+ end
128
+ end
129
+
130
+ # Return association name for target_class in given context.
131
+ #
132
+ # @param context [ActiveRecord::Base]
133
+ # #return [Symbol,nil]
134
+ private def association_for_class(context)
135
+ reflection = context.reflections.find do |_, reflection|
136
+ reflection.klass == target_class
137
+ end
138
+
139
+ unless reflection
140
+ raise AssociationError, "No association between #{context.name} and #{target_class.name} models."
141
+ end
142
+
143
+ reflection.first.to_sym
144
+ end
145
+ end
146
+ end
147
+ end
148
+
@@ -19,12 +19,24 @@ module FlatMap
19
19
  # Find a record of the +target_class+ by +id+ and use it as a
20
20
  # target for a new mapper, with a list of passed +traits+ applied
21
21
  # to it.
22
+ # When `preload: true` option passed calls mapper `relation` method
23
+ # with traits list. This allows to load all related objects in one query.
22
24
  #
23
25
  # @param [#to_i] id of the record
24
26
  # @param [*Symbol] traits
25
27
  # @return [FlatMap::Mapper] mapper
26
28
  def find(id, *traits, &block)
27
- new(target_class.find(id), *traits, &block)
29
+ options = { preload: false }
30
+
31
+ options.merge!(traits.extract_options!)
32
+
33
+ if options[:preload]
34
+ record = relation(traits).find(id)
35
+ else
36
+ record = target_class.find(id)
37
+ end
38
+
39
+ new(record, *traits, &block)
28
40
  end
29
41
 
30
42
  # Fetch a class for the target of the mapper.
@@ -184,9 +184,11 @@ module FlatMap
184
184
  class ModelMapper < OpenMapper
185
185
  extend ActiveSupport::Autoload
186
186
 
187
+ autoload :Associations
187
188
  autoload :Persistence
188
189
  autoload :Skipping
189
190
 
191
+ include Associations
190
192
  include Persistence
191
193
  include Skipping
192
194
 
@@ -0,0 +1,19 @@
1
+ module FlatMap
2
+ # This module enhances and modifies original FlatMap::OpenMapper::Persistence
3
+ # functionality for ActiveRecord models as targets.
4
+ module OpenMapper::Associations
5
+ extend ActiveSupport::Concern
6
+
7
+ # ModelMethods class macros
8
+ module ClassMethods
9
+ # Return association list for given traits.
10
+ # Used in ModelMapper.
11
+ #
12
+ # @return [nil]
13
+ def associations(*)
14
+ nil
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -133,7 +133,7 @@ module FlatMap
133
133
  # @return [Array<ActiveModel::Errors>]
134
134
  def consolidate_errors!
135
135
  mountings.map(&:errors).each do |errs|
136
- errors.messages.merge!(errs.to_hash){ |key, old, new| old.concat(new) }
136
+ errors.merge!(errs)
137
137
  end
138
138
  end
139
139
  private :consolidate_errors!
@@ -16,6 +16,7 @@ module FlatMap
16
16
 
17
17
  autoload :Mapping
18
18
  autoload :Mounting
19
+ autoload :Associations
19
20
  autoload :Traits
20
21
  autoload :Factory
21
22
  autoload :AttributeMethods
@@ -24,6 +25,7 @@ module FlatMap
24
25
 
25
26
  include Mapping
26
27
  include Mounting
28
+ include Associations
27
29
  include Traits
28
30
  include AttributeMethods
29
31
  include ActiveModel::Validations
@@ -1,3 +1,3 @@
1
1
  module FlatMap # :nodoc:
2
- VERSION = "0.4.0" # :nodoc:
2
+ VERSION = "0.6.0" # :nodoc:
3
3
  end
@@ -54,10 +54,21 @@ module FlatMap
54
54
  describe '.find' do
55
55
  let(:target){ PersistenceSpec::TargetClass.new('a', 'b') }
56
56
 
57
- it 'should delegate to target class to find object for mapper' do
58
- expect(PersistenceSpec::TargetClass).to receive(:find).with(1).and_return(target)
59
- expect(PersistenceSpec::TargetClassMapper).to receive(:new).with(target, :used_trait)
60
- PersistenceSpec::TargetClassMapper.find(1, :used_trait)
57
+ context "with preload" do
58
+ it 'should preload tables and find object for mapper' do
59
+ expect(PersistenceSpec::TargetClassMapper).to receive_message_chain(:relation, :find).
60
+ with([:used_trait]).with(1).and_return(target)
61
+ expect(PersistenceSpec::TargetClassMapper).to receive(:new).with(target, :used_trait)
62
+ PersistenceSpec::TargetClassMapper.find(1, :used_trait, preload: true)
63
+ end
64
+ end
65
+
66
+ context "without preload" do
67
+ it 'should delegate to target class to find object for mapper' do
68
+ expect(PersistenceSpec::TargetClass).to receive(:find).with(1).and_return(target)
69
+ expect(PersistenceSpec::TargetClassMapper).to receive(:new).with(target, :used_trait)
70
+ PersistenceSpec::TargetClassMapper.find(1, :used_trait)
71
+ end
61
72
  end
62
73
  end
63
74
 
@@ -54,10 +54,21 @@ module FlatMap
54
54
  describe '.find' do
55
55
  let(:target){ ModelMethodsSpec::TargetClass.new('a', 'b') }
56
56
 
57
- it 'should delegate to target class to find object for mapper' do
58
- expect(ModelMethodsSpec::TargetClass).to receive(:find).with(1).and_return(target)
59
- expect(ModelMethodsSpec::TargetClassMapper).to receive(:new).with(target, :used_trait)
60
- ModelMethodsSpec::TargetClassMapper.find(1, :used_trait)
57
+ context "with preload" do
58
+ it 'should preload tables and find object for mapper' do
59
+ expect(ModelMethodsSpec::TargetClassMapper).to receive_message_chain(:relation, :find).
60
+ with([:used_trait]).with(1).and_return(target)
61
+ expect(ModelMethodsSpec::TargetClassMapper).to receive(:new).with(target, :used_trait)
62
+ ModelMethodsSpec::TargetClassMapper.find(1, :used_trait, preload: true)
63
+ end
64
+ end
65
+
66
+ context "without preload" do
67
+ it 'should delegate to target class to find object for mapper' do
68
+ expect(ModelMethodsSpec::TargetClass).to receive(:find).with(1).and_return(target)
69
+ expect(ModelMethodsSpec::TargetClassMapper).to receive(:new).with(target, :used_trait)
70
+ ModelMethodsSpec::TargetClassMapper.find(1, :used_trait)
71
+ end
61
72
  end
62
73
  end
63
74
 
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ module FlatMap
4
+ module AssociationsListSpec
5
+ # Simulate active record models.
6
+ # Only .reflections method is used.
7
+ Reflection = Struct.new(:klass)
8
+
9
+ class Author
10
+ def self.reflections; {}; end
11
+ end
12
+
13
+ class Link
14
+ def self.reflections; {}; end
15
+ end
16
+
17
+ class Tag
18
+ def self.reflections; {}; end
19
+ end
20
+
21
+ class Category
22
+ def self.reflections; {}; end
23
+ end
24
+
25
+ class Comment
26
+ # belongs_to :author
27
+ # has_many :links
28
+
29
+ def self.reflections
30
+ {
31
+ "author" => Reflection.new(Author),
32
+ "links" => Reflection.new(Link)
33
+ }
34
+ end
35
+ end
36
+
37
+ class Article
38
+ # has_many :comments
39
+ # has_many :tags
40
+
41
+ def self.reflections
42
+ {
43
+ "comments" => Reflection.new(Comment),
44
+ "tags" => Reflection.new(Tag)
45
+ }
46
+ end
47
+
48
+ def self.joins(associations); end
49
+ end
50
+
51
+ # Mappers for models.
52
+ class AuthorMapper < FlatMap::ModelMapper; end
53
+
54
+ class LinkMapper < FlatMap::ModelMapper; end
55
+
56
+ class TagMapper < FlatMap::ModelMapper; end
57
+
58
+ class CategoryMapper < FlatMap::ModelMapper; end
59
+
60
+ class CommentMapper < FlatMap::ModelMapper
61
+ trait :with_author do
62
+ mount :author, mapper_class: AuthorMapper
63
+ end
64
+
65
+ trait :with_links do
66
+ mount :links, mapper_class: LinkMapper
67
+ end
68
+ end
69
+
70
+ class ArticleMapper < FlatMap::ModelMapper
71
+ trait :with_comments do
72
+ mount :comments, mapper_class: CommentMapper
73
+ end
74
+
75
+ trait :with_tags do
76
+ mount :tags, mapper_class: TagMapper
77
+ end
78
+
79
+ trait :with_category do
80
+ mount :category, mapper_class: CategoryMapper
81
+ end
82
+ end
83
+
84
+ describe ".relation" do
85
+ let(:relation) { double }
86
+ subject { ArticleMapper.relation(%i[ with_comments with_author ]) }
87
+
88
+ it "generates active record relation with correct associations" do
89
+ expect(Article).to receive_message_chain(:includes, :references).
90
+ with({ comments: :author }).with({ comments: :author }).
91
+ and_return(relation)
92
+
93
+ is_expected.to eq relation
94
+ end
95
+ end
96
+
97
+ describe ".associations" do
98
+ subject { ArticleMapper.associations(traits) }
99
+
100
+ context "one mounted class" do
101
+ let(:traits) { %i[ with_comments ] }
102
+
103
+ it { is_expected.to eq :comments }
104
+ end
105
+
106
+ context "nested mounted class" do
107
+ let(:traits) { %i[ with_comments with_author ] }
108
+
109
+ it { is_expected.to eq({ comments: :author }) }
110
+ end
111
+
112
+ context "mounted and nested mounted classes" do
113
+ let(:traits) { %i[ with_comments with_author with_tags ] }
114
+
115
+ it { is_expected.to eq([{ comments: :author }, :tags]) }
116
+ end
117
+
118
+ context "two nested mounted classes" do
119
+ let(:traits) { %i[ with_comments with_author with_links ] }
120
+
121
+ it { is_expected.to eq({ comments: [:author, :links] }) }
122
+ end
123
+
124
+ context "no relation between models" do
125
+ let(:traits) { %i[ with_category ] }
126
+
127
+ it do
128
+ expect { subject }.to raise_error(FlatMap::ModelMapper::Associations::AssociationError,
129
+ "No association between FlatMap::AssociationsListSpec::Article " \
130
+ "and FlatMap::AssociationsListSpec::Category models.")
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: HornsAndHooves-flat_map
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - HornsAndHooves
@@ -9,39 +9,51 @@ authors:
9
9
  - Zachary Belzer
10
10
  - Sergey Potapov
11
11
  - Bruce Burdick
12
- autorequire:
12
+ autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2016-07-18 00:00:00.000000000 Z
15
+ date: 2022-05-27 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activesupport
19
19
  requirement: !ruby/object:Gem::Requirement
20
20
  requirements:
21
- - - "~>"
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: '5'
24
+ - - "<"
22
25
  - !ruby/object:Gem::Version
23
- version: '4.2'
26
+ version: 7.0.0
24
27
  type: :runtime
25
28
  prerelease: false
26
29
  version_requirements: !ruby/object:Gem::Requirement
27
30
  requirements:
28
- - - "~>"
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '5'
34
+ - - "<"
29
35
  - !ruby/object:Gem::Version
30
- version: '4.2'
36
+ version: 7.0.0
31
37
  - !ruby/object:Gem::Dependency
32
38
  name: activerecord
33
39
  requirement: !ruby/object:Gem::Requirement
34
40
  requirements:
35
- - - "~>"
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '5'
44
+ - - "<"
36
45
  - !ruby/object:Gem::Version
37
- version: '4.2'
46
+ version: 7.0.0
38
47
  type: :runtime
39
48
  prerelease: false
40
49
  version_requirements: !ruby/object:Gem::Requirement
41
50
  requirements:
42
- - - "~>"
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '5'
54
+ - - "<"
43
55
  - !ruby/object:Gem::Version
44
- version: '4.2'
56
+ version: 7.0.0
45
57
  - !ruby/object:Gem::Dependency
46
58
  name: yard
47
59
  requirement: !ruby/object:Gem::Requirement
@@ -137,9 +149,11 @@ files:
137
149
  - lib/flat_map/mapping/writer/method.rb
138
150
  - lib/flat_map/mapping/writer/proc.rb
139
151
  - lib/flat_map/model_mapper.rb
152
+ - lib/flat_map/model_mapper/associations.rb
140
153
  - lib/flat_map/model_mapper/persistence.rb
141
154
  - lib/flat_map/model_mapper/skipping.rb
142
155
  - lib/flat_map/open_mapper.rb
156
+ - lib/flat_map/open_mapper/associations.rb
143
157
  - lib/flat_map/open_mapper/attribute_methods.rb
144
158
  - lib/flat_map/open_mapper/factory.rb
145
159
  - lib/flat_map/open_mapper/mapping.rb
@@ -169,6 +183,7 @@ files:
169
183
  - spec/flat_map/mapping/writer/method_spec.rb
170
184
  - spec/flat_map/mapping/writer/proc_spec.rb
171
185
  - spec/flat_map/mapping_spec.rb
186
+ - spec/flat_map/model_mapper/associations_spec.rb
172
187
  - spec/flat_map/open_mapper_spec.rb
173
188
  - spec/spec_helper.rb
174
189
  - tmp/metric_fu/_data/20131218.yml
@@ -177,7 +192,7 @@ homepage: https://github.com/HornsAndHooves/flat_map
177
192
  licenses:
178
193
  - LICENSE
179
194
  metadata: {}
180
- post_install_message:
195
+ post_install_message:
181
196
  rdoc_options: []
182
197
  require_paths:
183
198
  - lib
@@ -192,9 +207,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
207
  - !ruby/object:Gem::Version
193
208
  version: '0'
194
209
  requirements: []
195
- rubyforge_project:
196
- rubygems_version: 2.4.6
197
- signing_key:
210
+ rubygems_version: 3.0.9
211
+ signing_key:
198
212
  specification_version: 4
199
213
  summary: Deep object graph to a plain properties mapper
200
214
  test_files:
@@ -219,5 +233,6 @@ test_files:
219
233
  - spec/flat_map/mapping/writer/method_spec.rb
220
234
  - spec/flat_map/mapping/writer/proc_spec.rb
221
235
  - spec/flat_map/mapping_spec.rb
236
+ - spec/flat_map/model_mapper/associations_spec.rb
222
237
  - spec/flat_map/open_mapper_spec.rb
223
238
  - spec/spec_helper.rb