miscellany 0.1.19 → 0.1.20

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
2
  SHA256:
3
- metadata.gz: e919c60e03780d5fab1a21ae6d528482db21d5a4fcfbc176b4808cc1d9acdc77
4
- data.tar.gz: c9ecbbbb69e28e8fbef7aa9efe93db1cab9576634ede5ad2dc698180c1743dda
3
+ metadata.gz: 31028be35ab95451d190d0a669321bd265bc82230f8c07ddab8c52a4e526fcf0
4
+ data.tar.gz: ee7c51e33f2147ab49e05f3c0783dd5a8f938a931a33468d1289214233b34a4e
5
5
  SHA512:
6
- metadata.gz: 7468e0d150c79e5ebfb5760648e97699c7b88340c68b7eaa530c58e512ee982eb1d2b44c2599b374a7348a5b9cefe7d7037603a98aa5a8a895f18bac6d4efe84
7
- data.tar.gz: 0a7dfb9250e3f686d88f7c2932cbd1a8b07f0a38ae4d3e491e98ee4c14a22126bcf3defd58227130fe3a43f8cd7d0b5dbaf002f6ded24fd187706b445fcce28e
6
+ metadata.gz: 9f94c830dc69bf00eb3b0c9e423741ce5d70caac4f7cad67e85e4df9c9b929fc60ad2034516ed9924c2f37ce2dab6cbb56f92d02a45b7cd6179b753479f278d8
7
+ data.tar.gz: 7cfdc3d21e2c648c9031ce061a59529db42236734b50a00bfc058fafde17039ef7660def4f8e8a8775d4a0b65cf30317fc4bbd87a01900b61d6866823904ba19
@@ -47,146 +47,186 @@ module Miscellany
47
47
  qs = qs.merge(source_refl.scope_for(model.unscoped)) if source_refl.scope
48
48
  qs
49
49
  }
50
+ pass_opts = source_refl.options.merge(
51
+ class_name: source_refl.class_name,
52
+ inverse_of: nil,
53
+ arbitrary_source_reflection: source_refl,
54
+ )
55
+ if source_refl.is_a?(ActiveRecord::Reflection::ThroughReflection)
56
+ pass_opts[:source] = source_refl.source_reflection_name
57
+ end
50
58
  ActiveRecord::Reflection.create(
51
59
  options[:type],
52
60
  @target_attribute,
53
61
  scope,
54
- source_refl.options.merge(
55
- class_name: source_refl.class_name,
56
- inverse_of: nil
57
- ),
62
+ pass_opts,
58
63
  model
59
64
  )
60
65
  end
61
66
  end
62
67
  end
63
68
 
64
- module ActiveRecordBasePatch
65
- extend ActiveSupport::Concern
69
+ module ActiveRecordPatches
70
+ module BasePatch
71
+ extend ActiveSupport::Concern
66
72
 
67
- included do
68
- class << self
69
- delegate :prefetch, to: :all
73
+ included do
74
+ class << self
75
+ delegate :prefetch, to: :all
76
+ end
70
77
  end
71
78
  end
72
- end
73
79
 
74
- module ActiveRecordRelationPatch
75
- def exec_queries
76
- return super if loaded?
80
+ module RelationPatch
81
+ def exec_queries
82
+ return super if loaded?
83
+
84
+ records = super
85
+ (@values[:prefetches] || {}).each do |_key, opts|
86
+ pfc = PrefetcherContext.new(model, opts)
87
+ pfc.link_models(records)
88
+
89
+ unless defined?(Goldiloader) && Goldiloader.enabled?
90
+ if PRE_RAILS_6_2
91
+ ::ActiveRecord::Associations::Preloader.new.preload(records, [opts[:attribute]])
92
+ else
93
+ ::ActiveRecord::Associations::Preloader.new(records: records, associations: [opts[:attribute]]).call
94
+ end
95
+ end
96
+ end
97
+ records
98
+ end
77
99
 
78
- records = super
79
- (@values[:prefetches] || {}).each do |_key, opts|
80
- pfc = PrefetcherContext.new(model, opts)
81
- pfc.link_models(records)
100
+ def prefetch(**kwargs)
101
+ spawn.add_prefetches!(kwargs)
102
+ end
82
103
 
83
- unless defined?(Goldiloader) && Goldiloader.enabled?
84
- if PRE_RAILS_6_2
85
- ::ActiveRecord::Associations::Preloader.new.preload(records, [opts[:attribute]])
86
- else
87
- ::ActiveRecord::Associations::Preloader.new(records: records, associations: [opts[:attribute]]).call
88
- end
104
+ def add_prefetches!(kwargs)
105
+ return unless kwargs.present?
106
+
107
+ assert_mutability!
108
+ @values[:prefetches] ||= {}
109
+ kwargs.each do |attr, opts|
110
+ @values[:prefetches][attr] = normalize_prefetch_options(attr, opts)
89
111
  end
112
+ self
90
113
  end
91
- records
92
- end
93
114
 
94
- def prefetch(**kwargs)
95
- spawn.add_prefetches!(kwargs)
96
- end
115
+ def normalize_prefetch_options(attr, opts)
116
+ norm = if opts.is_a?(Array)
117
+ { relation: opts[0], queryset: opts[1] }
118
+ elsif opts.is_a?(ActiveRecord::Relation)
119
+ rel_name = opts.model.name.underscore
120
+ rel = (model.reflections[rel_name] || model.reflections[rel_name.pluralize])&.name
121
+ { relation: rel, queryset: opts }
122
+ else
123
+ opts
124
+ end
97
125
 
98
- def add_prefetches!(kwargs)
99
- return unless kwargs.present?
126
+ norm[:attribute] = attr
127
+ norm[:type] ||= (attr.to_s.pluralize == attr.to_s) ? :has_many : :has_one
100
128
 
101
- assert_mutability!
102
- @values[:prefetches] ||= {}
103
- kwargs.each do |attr, opts|
104
- @values[:prefetches][attr] = normalize_prefetch_options(attr, opts)
129
+ norm
105
130
  end
106
- self
107
131
  end
108
132
 
109
- def normalize_prefetch_options(attr, opts)
110
- norm = if opts.is_a?(Array)
111
- { relation: opts[0], queryset: opts[1] }
112
- elsif opts.is_a?(ActiveRecord::Relation)
113
- rel_name = opts.model.name.underscore
114
- rel = (model.reflections[rel_name] || model.reflections[rel_name.pluralize])&.name
115
- { relation: rel, queryset: opts }
116
- else
117
- opts
118
- end
133
+ module Relation
134
+ module MergerPatch
135
+ def merge
136
+ super.tap do
137
+ merge_prefetches
138
+ end
139
+ end
119
140
 
120
- norm[:attribute] = attr
121
- norm[:type] ||= (attr.to_s.pluralize == attr.to_s) ? :has_many : :has_one
141
+ private
122
142
 
123
- norm
143
+ def merge_prefetches
144
+ relation.add_prefetches!(other.values[:prefetches])
145
+ end
146
+ end
124
147
  end
125
- end
126
148
 
127
- module ActiveRecordMergerPatch
128
- def merge
129
- super.tap do
130
- merge_prefetches
149
+ module Associations
150
+ if ACTIVE_RECORD_VERSION >= ::Gem::Version.new('7.0.0')
151
+ module Preloader
152
+ module BranchPatch
153
+ def grouped_records
154
+ h = {}
155
+ polymorphic_parent = !root? && parent.polymorphic?
156
+ source_records.each do |record|
157
+ next unless record
158
+ reflection = record.class._reflect_on_association(association)
159
+ reflection ||= record.association(association)&.reflection rescue nil
160
+ next if polymorphic_parent && !reflection || !record.association(association).klass
161
+ (h[reflection] ||= []) << record
162
+ end
163
+ h
164
+ end
165
+ end
166
+ end
167
+ elsif ACTIVE_RECORD_VERSION >= ::Gem::Version.new('6.0.0')
168
+ module PreloaderPatch
169
+ def grouped_records(association, records, polymorphic_parent)
170
+ h = {}
171
+ records.each do |record|
172
+ next unless record
173
+ reflection = record.class._reflect_on_association(association)
174
+ reflection ||= record.association(association)&.reflection rescue nil
175
+ next if polymorphic_parent && !reflection || !record.association(association).klass
176
+ (h[reflection] ||= []) << record
177
+ end
178
+ h
179
+ end
180
+ end
131
181
  end
132
182
  end
133
183
 
134
- private
184
+ module Reflection
185
+ module AssociationReflectionPatch
186
+ def check_preloadable!
187
+ return if scope && scope.arity < 0
188
+ super
189
+ end
190
+ end
135
191
 
136
- def merge_prefetches
137
- relation.add_prefetches!(other.values[:prefetches])
192
+ module ThroughReflectionPatch
193
+ def check_validity!
194
+ return if options[:arbitrary_source_reflection] # Rails already checked the base relation, we're good
195
+ super
196
+ end
197
+ end
138
198
  end
139
199
  end
140
200
 
141
- module ActiveRecordPreloaderPatch
142
- def grouped_records(association, records, polymorphic_parent)
143
- h = {}
144
- records.each do |record|
145
- next unless record
146
- reflection = record.class._reflect_on_association(association)
147
- reflection ||= record.association(association)&.reflection rescue nil
148
- next if polymorphic_parent && !reflection || !record.association(association).klass
149
- (h[reflection] ||= []) << record
201
+ def self.apply_patches(mod, install_base, base_module: nil)
202
+ return unless mod.is_a?(Module)
203
+
204
+ base_module ||= mod
205
+
206
+ if mod.name.ends_with? "Patch"
207
+ base_full_name = base_module.to_s
208
+ mod_full_name = mod.to_s
209
+ mod_rel_name = mod_full_name.sub(base_full_name, '')
210
+ mod_rel_bits = mod_rel_name.split('::').select(&:present?).map do |bit|
211
+ bit.ends_with?('Patch') ? bit[0..-6] : bit
150
212
  end
151
- h
152
- end
153
- end
213
+ final_mod_name = [install_base, *mod_rel_bits].select(&:present?).join("::")
214
+ install_mod = final_mod_name.constantize
154
215
 
155
- module ActiveRecordPreloaderBranchPatch
156
- def grouped_records
157
- h = {}
158
- polymorphic_parent = !root? && parent.polymorphic?
159
- source_records.each do |record|
160
- next unless record
161
- reflection = record.class._reflect_on_association(association)
162
- reflection ||= record.association(association)&.reflection rescue nil
163
- next if polymorphic_parent && !reflection || !record.association(association).klass
164
- (h[reflection] ||= []) << record
216
+ if mod.is_a?(ActiveSupport::Concern)
217
+ install_mod.include(mod)
218
+ else
219
+ install_mod.prepend(mod)
165
220
  end
166
- h
167
221
  end
168
- end
169
222
 
170
- module ActiveRecordReflectionPatch
171
- def check_preloadable!
172
- return if scope && scope.arity < 0
173
- super
223
+ mod.constants.map {|const| mod.const_get(const) }.each do |const|
224
+ apply_patches(const, install_base, base_module: base_module || mod)
174
225
  end
175
226
  end
176
227
 
177
228
  def self.install
178
- ::ActiveRecord::Base.include(ActiveRecordBasePatch)
179
-
180
- ::ActiveRecord::Relation.prepend(ActiveRecordRelationPatch)
181
- ::ActiveRecord::Relation::Merger.prepend(ActiveRecordMergerPatch)
182
-
183
- if ACTIVE_RECORD_VERSION >= ::Gem::Version.new('7.0.0')
184
- ::ActiveRecord::Associations::Preloader::Branch.prepend(ActiveRecordPreloaderBranchPatch)
185
- elsif ACTIVE_RECORD_VERSION >= ::Gem::Version.new('6.0.0')
186
- ::ActiveRecord::Associations::Preloader.prepend(ActiveRecordPreloaderPatch)
187
- end
188
-
189
- ::ActiveRecord::Reflection::AssociationReflection.prepend(ActiveRecordReflectionPatch)
229
+ apply_patches(ActiveRecordPatches, ::ActiveRecord)
190
230
 
191
231
  return unless defined? ::Goldiloader
192
232
 
@@ -47,6 +47,7 @@ module Miscellany
47
47
  end
48
48
  psql += " LIMIT #{length} OFFSET #{start}"
49
49
  records = ActiveRecord::Base.connection.exec_query(psql).to_a
50
+ records.map!(&:with_indifferent_access)
50
51
  augment_batch(records)
51
52
  records
52
53
  end
@@ -62,6 +63,7 @@ module Miscellany
62
63
  batch = ActiveRecord::Base.connection.exec_query(
63
64
  "SELECT * FROM #{tbl} LIMIT #{of} OFFSET #{offset}",
64
65
  )
66
+ batch.map!(&:with_indifferent_access)
65
67
  augment_batch(batch)
66
68
  yield batch
67
69
  offset += of
@@ -187,6 +187,7 @@ module Miscellany
187
187
  def sliced_items
188
188
  @sliced_items ||= begin
189
189
  if items.is_a?(Array)
190
+ # TODO Can we apply sorting?
190
191
  start, finish = slice_bounds
191
192
  if start && finish
192
193
  items[start...finish]
@@ -194,6 +195,7 @@ module Miscellany
194
195
  items
195
196
  end
196
197
  elsif items.is_a?(Proc)
198
+ # TODO Can we apply sorting?
197
199
  items.call(self)
198
200
  elsif items.is_a?(ActiveRecord::Relation)
199
201
  offset, limit = slice_bounds
@@ -218,8 +220,7 @@ module Miscellany
218
220
  end
219
221
 
220
222
  def sort_sql
221
- sorts = [ *Array(self.sort) ]
222
- sorts.push(*options[:sort_parser]&.default_sorts)
223
+ sorts = [ *Array(self.sort), *options[:sort_parser]&.default_sorts]
223
224
  sorts.compact!
224
225
 
225
226
  return nil unless sorts.present?
@@ -1,3 +1,3 @@
1
1
  module Miscellany
2
- VERSION = "0.1.19".freeze
2
+ VERSION = "0.1.20".freeze
3
3
  end
@@ -2,84 +2,139 @@
2
2
  require 'spec_helper'
3
3
 
4
4
  describe Miscellany::ArbitraryPrefetch do
5
- with_model :Post do
6
- table do |t|
7
- t.string :title
8
- t.timestamps null: false
5
+ shared_examples "general specs" do
6
+ it 'generally works' do
7
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
8
+ expect(posts.count).to eq 10
9
+ expect(posts[0].favorite_comment).to be
9
10
  end
10
11
 
11
- model do
12
- has_many :comments
12
+ it 'works with Goldiloader active' do
13
+ Goldiloader.enabled do
14
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
15
+ expect(posts.count).to eq 10
16
+ expect(posts[0].favorite_comment).to be
17
+ end
13
18
  end
14
- end
15
19
 
16
- with_model :Comment do
17
- table do |t|
18
- t.belongs_to :post
19
- t.string :title
20
- t.boolean :favorite
21
- t.timestamps null: false
20
+ it 'works with Goldiloader disabled' do
21
+ Goldiloader.disabled do
22
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
23
+ expect(posts.count).to eq 10
24
+ expect(posts[0].favorite_comment).to be
25
+ end
22
26
  end
23
27
 
24
- model do
25
- belongs_to :post
28
+ context 'prefetch is singluar' do
29
+ it 'returns a single object' do
30
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
31
+ expect(posts[0].favorite_comment).to be_a Comment
32
+ end
33
+
34
+ it 'with multiple items returns a single object' do
35
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: nil))
36
+ expect(posts[0].favorite_comment).to be_a Comment
37
+ end
26
38
  end
27
- end
28
39
 
29
- let!(:posts) { 10.times.map{|i| Post.create!(title: "Post #{i}") } }
40
+ context 'prefetch is plural' do
41
+ it 'returns an Array' do
42
+ posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: nil))
43
+ expect(posts[0].non_favorite_comments).to respond_to :[]
44
+ expect(posts[0].non_favorite_comments.length).to eq 4
45
+ end
30
46
 
31
- before :each do
32
- posts.each do |p|
33
- 5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
34
- p.comments.last.update!(favorite: true)
47
+ it 'with 1 item returns an Array' do
48
+ posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: true))
49
+ expect(posts[0].non_favorite_comments).to respond_to :[]
50
+ expect(posts[0].non_favorite_comments.length).to eq 1
51
+ end
35
52
  end
36
53
  end
37
54
 
38
- it 'generally works' do
39
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
40
- expect(posts.count).to eq 10
41
- expect(posts[0].favorite_comment).to be
42
- end
55
+ context "normal association" do
56
+ with_model :Post do
57
+ table do |t|
58
+ t.string :title
59
+ t.timestamps null: false
60
+ end
43
61
 
44
- it 'works with Goldiloader active' do
45
- Goldiloader.enabled do
46
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
47
- expect(posts.count).to eq 10
48
- expect(posts[0].favorite_comment).to be
62
+ model do
63
+ has_many :comments
64
+ end
49
65
  end
50
- end
51
66
 
52
- it 'works with Goldiloader disabled' do
53
- Goldiloader.disabled do
54
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
55
- expect(posts.count).to eq 10
56
- expect(posts[0].favorite_comment).to be
67
+ with_model :Comment do
68
+ table do |t|
69
+ t.belongs_to :post
70
+ t.string :title
71
+ t.boolean :favorite
72
+ t.timestamps null: false
73
+ end
74
+
75
+ model do
76
+ belongs_to :post
77
+ end
57
78
  end
79
+
80
+ let!(:posts) { 10.times.map{|i| Post.create!(title: "Post #{i}") } }
81
+
82
+ before :each do
83
+ posts.each do |p|
84
+ 5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
85
+ p.comments.last.update!(favorite: true)
86
+ end
87
+ end
88
+
89
+ include_examples "general specs"
58
90
  end
59
91
 
60
- context 'prefetch is singluar' do
61
- it 'returns a single object' do
62
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
63
- expect(posts[0].favorite_comment).to be_a Comment
92
+ context "through: association" do
93
+ with_model :Post do
94
+ table do |t|
95
+ t.string :title
96
+ t.timestamps null: false
97
+ end
98
+
99
+ model do
100
+ has_many :interims
101
+ has_many :comments, through: :interims
102
+ end
64
103
  end
65
104
 
66
- it 'with multiple items returns a single object' do
67
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: nil))
68
- expect(posts[0].favorite_comment).to be_a Comment
105
+ with_model :Interim do
106
+ table do |t|
107
+ t.belongs_to :post
108
+ t.belongs_to :comment
109
+ t.timestamps null: false
110
+ end
111
+
112
+ model do
113
+ belongs_to :post
114
+ belongs_to :comment
115
+ end
69
116
  end
70
- end
71
117
 
72
- context 'prefetch is plural' do
73
- it 'returns an Array' do
74
- posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: nil))
75
- expect(posts[0].non_favorite_comments).to respond_to :[]
76
- expect(posts[0].non_favorite_comments.length).to eq 4
118
+ with_model :Comment do
119
+ table do |t|
120
+ t.string :title
121
+ t.boolean :favorite
122
+ t.timestamps null: false
123
+ end
124
+
125
+ model do
126
+ end
77
127
  end
78
128
 
79
- it 'with 1 item returns an Array' do
80
- posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: true))
81
- expect(posts[0].non_favorite_comments).to respond_to :[]
82
- expect(posts[0].non_favorite_comments.length).to eq 1
129
+ let!(:posts) { 10.times.map{|i| Post.create!(title: "Post #{i}") } }
130
+
131
+ before :each do
132
+ posts.each do |p|
133
+ 5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
134
+ p.comments.last.update!(favorite: true)
135
+ end
83
136
  end
137
+
138
+ include_examples "general specs"
84
139
  end
85
140
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miscellany
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.19
4
+ version: 0.1.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan Knapp
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-02 00:00:00.000000000 Z
11
+ date: 2023-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -114,7 +114,7 @@ dependencies:
114
114
  - - ">="
115
115
  - !ruby/object:Gem::Version
116
116
  version: '0'
117
- description:
117
+ description:
118
118
  email:
119
119
  - eknapp@instructure.com
120
120
  executables: []
@@ -154,7 +154,7 @@ files:
154
154
  homepage: https://instructure.com
155
155
  licenses: []
156
156
  metadata: {}
157
- post_install_message:
157
+ post_install_message:
158
158
  rdoc_options: []
159
159
  require_paths:
160
160
  - lib
@@ -170,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
170
  version: '0'
171
171
  requirements: []
172
172
  rubygems_version: 3.1.6
173
- signing_key:
173
+ signing_key:
174
174
  specification_version: 4
175
175
  summary: Gem for a bunch of random, re-usable Rails Concerns & Helpers
176
176
  test_files: