HornsAndHooves-flat_map 0.4.0 → 0.6.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.
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