HornsAndHooves-flat_map 0.5.0 → 0.7.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
2
  SHA256:
3
- metadata.gz: 7902561902733c34e2f37b66ae25ed17fded33ff38f6f0f013630137a87eacb6
4
- data.tar.gz: ba3167200d89dad368a42d23dbe4bebcfc147f15879ef8a78835a609127aa20c
3
+ metadata.gz: fd4f2b02c245613b8d3f3346d2355ae2108a4342ddb9f9cd1e268ef87310f1b8
4
+ data.tar.gz: fdf3bf9c4a4ffaa3c6647d907d0e02846eabbc60f5d9b593555d1904f371e72d
5
5
  SHA512:
6
- metadata.gz: 44f53062529bafb8ae96504dbaa6576e6e44c453749a01012c37a723726aa459a31f1f097e3e364a47ad1453d1609e1da7e87099d4cd27d0f2a2d8f365463bff
7
- data.tar.gz: 5d8861f359ea9c460e0cbac177726143cf87b4d18299f43052a845fb0c74faf2aef483e828bbd7be06160a5d9c7719c6cd55c64c505f0a9d99fede9bf142feaa
6
+ metadata.gz: ded272e0095638c4173d099651186dedd3116e1ac4532c0473d7dd6f126227026b55c6346728630fffe2580f483b5c0521c982a8bb0231eecc0917d498fa56e5
7
+ data.tar.gz: 20037626c6382fd471dff4db1a41f7ec09f81e7a5bbbe0ff80ffa6b1990bda10a1992dbf989da33e77ad1c0da7e0a9767bfff69a8ade32dfaf0abb08b9bda2e5
@@ -49,7 +49,7 @@ module FlatMap
49
49
  end
50
50
 
51
51
  # Overridden to add suffixing support for mappings of mappers with name suffix
52
- def add(attr, *args)
52
+ def add(attr, type, **options)
53
53
  attr = :"#{attr}_#{@base.suffix}" if attr != :base && @base.suffixed?
54
54
  super
55
55
  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
+
@@ -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
@@ -53,13 +55,13 @@ module FlatMap
53
55
  # @param [Object] target Target of mapping
54
56
  # @param [*Symbol] traits List of traits
55
57
  # @raise [FlatMap::Mapper::NoTargetError]
56
- def initialize(target, *traits)
58
+ def initialize(target, *traits, &block)
57
59
  raise NoTargetError.new(self.class) unless target
58
60
 
59
61
  @target, @traits = target, traits
60
62
 
61
63
  if block_given?
62
- singleton_class.trait :extension, &Proc.new
64
+ singleton_class.trait :extension, &block
63
65
  end
64
66
  end
65
67
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FlatMap # :nodoc:
2
- VERSION = "0.5.0" # :nodoc:
4
+ VERSION = "0.7.0" # :nodoc:
3
5
  end
@@ -35,10 +35,10 @@ module FlatMap
35
35
  with({:attr_a => :attr_a, :mapped_attr_b => :attr_b}, {:writer => false}).
36
36
  and_call_original
37
37
  expect(Mapping::Factory).to receive(:new).
38
- with(:attr_a, :attr_a, :writer => false).
38
+ with(:attr_a, :attr_a, { :writer => false }).
39
39
  and_call_original
40
40
  expect(Mapping::Factory).to receive(:new).
41
- with(:mapped_attr_b, :attr_b, :writer => false).
41
+ with(:mapped_attr_b, :attr_b, { :writer => false }).
42
42
  and_call_original
43
43
 
44
44
  MappingSpec::EmptyMapper.class_eval do
@@ -76,7 +76,7 @@ module FlatMap
76
76
  context 'defining mountings' do
77
77
  it "should use Factory for defining mappings" do
78
78
  expect(Mapper::Factory).to receive(:new).
79
- with(:foo, :mapper_class_name => 'FooMapper').
79
+ with(:foo, { :mapper_class_name => 'FooMapper' }).
80
80
  and_call_original
81
81
 
82
82
  expect{ MountingSpec::EmptyMapper.mount(:foo, :mapper_class_name => 'FooMapper') }.
@@ -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.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - HornsAndHooves
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2021-10-04 00:00:00.000000000 Z
15
+ date: 2024-01-09 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activesupport
@@ -149,9 +149,11 @@ files:
149
149
  - lib/flat_map/mapping/writer/method.rb
150
150
  - lib/flat_map/mapping/writer/proc.rb
151
151
  - lib/flat_map/model_mapper.rb
152
+ - lib/flat_map/model_mapper/associations.rb
152
153
  - lib/flat_map/model_mapper/persistence.rb
153
154
  - lib/flat_map/model_mapper/skipping.rb
154
155
  - lib/flat_map/open_mapper.rb
156
+ - lib/flat_map/open_mapper/associations.rb
155
157
  - lib/flat_map/open_mapper/attribute_methods.rb
156
158
  - lib/flat_map/open_mapper/factory.rb
157
159
  - lib/flat_map/open_mapper/mapping.rb
@@ -181,6 +183,7 @@ files:
181
183
  - spec/flat_map/mapping/writer/method_spec.rb
182
184
  - spec/flat_map/mapping/writer/proc_spec.rb
183
185
  - spec/flat_map/mapping_spec.rb
186
+ - spec/flat_map/model_mapper/associations_spec.rb
184
187
  - spec/flat_map/open_mapper_spec.rb
185
188
  - spec/spec_helper.rb
186
189
  - tmp/metric_fu/_data/20131218.yml
@@ -204,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
207
  - !ruby/object:Gem::Version
205
208
  version: '0'
206
209
  requirements: []
207
- rubygems_version: 3.0.9
210
+ rubygems_version: 3.1.6
208
211
  signing_key:
209
212
  specification_version: 4
210
213
  summary: Deep object graph to a plain properties mapper
@@ -230,5 +233,6 @@ test_files:
230
233
  - spec/flat_map/mapping/writer/method_spec.rb
231
234
  - spec/flat_map/mapping/writer/proc_spec.rb
232
235
  - spec/flat_map/mapping_spec.rb
236
+ - spec/flat_map/model_mapper/associations_spec.rb
233
237
  - spec/flat_map/open_mapper_spec.rb
234
238
  - spec/spec_helper.rb