priloo 0.1.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 +7 -0
- data/lib/preloader.rb +11 -0
- data/lib/priloo.rb +31 -0
- data/lib/priloo/active_record_support.rb +42 -0
- data/lib/priloo/dependencies.rb +25 -0
- data/lib/priloo/preloadable.rb +123 -0
- data/lib/priloo/preloaders/ar_association_preloader.rb +32 -0
- data/lib/priloo/preloaders/base_preloader.rb +31 -0
- data/lib/priloo/preloaders/bm_injector_preloader.rb +55 -0
- data/lib/priloo/preloaders/collection_preloader.rb +19 -0
- data/lib/priloo/preloaders/generic_preloader.rb +49 -0
- data/lib/priloo/preloaders/navigating_preloader.rb +24 -0
- data/lib/priloo/preloaders/nil_preloader.rb +17 -0
- data/lib/priloo/recursive_injector.rb +175 -0
- data/lib/priloo/version.rb +5 -0
- metadata +170 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 27caed3d2c1d94e1656f03f7a386f08743bc171a756c947e740fab37cc72dfcc
|
4
|
+
data.tar.gz: 97ac72a743a851842af6857114d6da6432c5b3b0c019f82d168013090a0997f7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 00f271d5ce1ade1f0759eae909ab030791e39125f23ff05a2e961e149641f75443b79762677c2d47facb099fd6b77280d7debab81f5add72bfa45c4b76863025
|
7
|
+
data.tar.gz: 1271bc3bacd58921bf52d69d3f2e324bd8bdb5b0dc92ead5deaed107608046130d653398a6bc7d6c10595a9740159e7dbf3613c8ddfb7700bc194a988af1f2e7
|
data/lib/preloader.rb
ADDED
data/lib/priloo.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
require 'priloo/version'
|
6
|
+
require 'priloo/recursive_injector'
|
7
|
+
require 'priloo/preloadable'
|
8
|
+
require 'priloo/dependencies'
|
9
|
+
require 'priloo/preloaders/base_preloader'
|
10
|
+
require 'priloo/preloaders/ar_association_preloader'
|
11
|
+
require 'priloo/preloaders/bm_injector_preloader'
|
12
|
+
require 'priloo/preloaders/collection_preloader'
|
13
|
+
require 'priloo/preloaders/generic_preloader'
|
14
|
+
require 'priloo/preloaders/navigating_preloader'
|
15
|
+
require 'priloo/preloaders/nil_preloader'
|
16
|
+
|
17
|
+
ActiveSupport.on_load(:active_record) do
|
18
|
+
require 'priloo/active_record_support'
|
19
|
+
end
|
20
|
+
|
21
|
+
module Priloo
|
22
|
+
def self.preload(*params)
|
23
|
+
RecursiveInjector.new.inject(*params)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Enumerable
|
28
|
+
def priload(*args)
|
29
|
+
Priloo.preload(self, args)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
module Priloo
|
6
|
+
module ActiveRecordSupport
|
7
|
+
def method_missing(method, *)
|
8
|
+
generate_priloo_method(method) || super
|
9
|
+
end
|
10
|
+
|
11
|
+
def respond_to_missing?(method, _)
|
12
|
+
!generate_priloo_method(method).nil? || super
|
13
|
+
end
|
14
|
+
|
15
|
+
PRILOO_SUFFIX_REGEX = /__priloo__\z/
|
16
|
+
|
17
|
+
def generate_priloo_method(method)
|
18
|
+
index = method =~ PRILOO_SUFFIX_REGEX
|
19
|
+
return unless index
|
20
|
+
|
21
|
+
property = method.slice(0, index).to_sym
|
22
|
+
|
23
|
+
preloader =
|
24
|
+
case
|
25
|
+
when self.class.reflect_on_association(property)
|
26
|
+
Preloaders::ArAssociationPreloader.new(self.class, property)
|
27
|
+
when self.class.respond_to?("#{property}_sql")
|
28
|
+
Preloaders::BmInjectorPreloader.new(self.class, property)
|
29
|
+
end
|
30
|
+
|
31
|
+
self.class.define_method(method) { preloader }
|
32
|
+
|
33
|
+
preloader
|
34
|
+
end
|
35
|
+
|
36
|
+
def _preloadable_target
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
ActiveRecord::Base.include(ActiveRecordSupport)
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tsort'
|
4
|
+
|
5
|
+
module Priloo
|
6
|
+
class Dependencies
|
7
|
+
include TSort
|
8
|
+
|
9
|
+
def initialize(defs)
|
10
|
+
@defs = defs
|
11
|
+
end
|
12
|
+
|
13
|
+
def tsort_each_node(&block)
|
14
|
+
@defs.each_key(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def tsort_each_child(node, &block)
|
18
|
+
@defs.fetch(node).each(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.resolve(defs)
|
22
|
+
new(defs).tsort
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'decors'
|
4
|
+
|
5
|
+
module Priloo
|
6
|
+
module Preloadable
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
# This method in only called in Preloaders::GenericPreloader
|
12
|
+
#
|
13
|
+
# Its purpose is to make sure instance variables are injected in the right instance,
|
14
|
+
# if it is behind a proxy (for instance, a SimpleDelegator). We cannot be 100% sure
|
15
|
+
# that the proxy will forward Object#instance_variable_set() properly
|
16
|
+
def _preloadable_target
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def priloo_clearall
|
21
|
+
return unless instance_variable_defined?(Preloaders::GenericPreloader::PRIVATE_STORE_NAME)
|
22
|
+
|
23
|
+
remove_instance_variable(Preloaders::GenericPreloader::PRIVATE_STORE_NAME)
|
24
|
+
end
|
25
|
+
|
26
|
+
class PreloadDependencies < ::Decors::DecoratorBase
|
27
|
+
def initialize(*)
|
28
|
+
super
|
29
|
+
|
30
|
+
@preloader = Preloaders::GenericPreloader.new(
|
31
|
+
decorated_method_name,
|
32
|
+
dependencies: [*decorator_args, **decorator_kwargs]
|
33
|
+
) { |list| list.map { |inst| undecorated_method.bind(inst._preloadable_target).call } }
|
34
|
+
|
35
|
+
decorated_class.declare_preloader(decorated_method_name, preloader)
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(instance, *)
|
39
|
+
return preloader.extract(instance) if preloader.injected?(instance)
|
40
|
+
|
41
|
+
preloader.preload([instance].bm_preload(*decorator_args, **decorator_kwargs)).first
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :preloader
|
47
|
+
end
|
48
|
+
|
49
|
+
class PreloadBatch < ::Decors::DecoratorBase
|
50
|
+
def initialize(*)
|
51
|
+
super
|
52
|
+
|
53
|
+
clazz = ObjectSpace.each_object(decorated_class).first
|
54
|
+
|
55
|
+
preloader = Preloaders::GenericPreloader.new(
|
56
|
+
decorated_method_name,
|
57
|
+
dependencies: [*decorator_args, **decorator_kwargs]
|
58
|
+
) { |list| undecorated_method.bind(clazz).call(list) }
|
59
|
+
|
60
|
+
clazz.declare_preloader(decorated_method_name, preloader)
|
61
|
+
|
62
|
+
clazz.send(:define_method, decorated_method_name) do
|
63
|
+
return preloader.extract(self) if preloader.injected?(self)
|
64
|
+
|
65
|
+
preloader.preload([self]).first
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module ClassMethods
|
71
|
+
def preload_define(name, *dependencies, &block)
|
72
|
+
preloader = Preloaders::GenericPreloader.new(name, dependencies: dependencies, &block)
|
73
|
+
declare_preloader(name, preloader)
|
74
|
+
|
75
|
+
define_method(name) do
|
76
|
+
return preloader.extract(self) if preloader.injected?(self)
|
77
|
+
|
78
|
+
preloader.preload([self]).first
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def preload_delegate(*names, to:, allow_nil: false, prefix: false)
|
83
|
+
names.each do |name|
|
84
|
+
prefixed_name = prefix ? :"#{to}_#{name}" : name
|
85
|
+
|
86
|
+
preload_define prefixed_name, to => name do |items|
|
87
|
+
items.map do |item|
|
88
|
+
through = item.send(to)
|
89
|
+
raise "Cannot delegate #{name} to #{to} because Nil is not allowed" if !allow_nil && through.nil?
|
90
|
+
|
91
|
+
through&.send(name)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def preload_ar(name, primary_key, ar_class_name, foreign_key)
|
98
|
+
preload_define name do |items|
|
99
|
+
ids = items.map { |item| item.send(primary_key) }.compact.uniq
|
100
|
+
data = ar_class_name.constantize.where(foreign_key => ids).map { |c| [c.send(foreign_key), c] }.to_h
|
101
|
+
items.map { |item| data[item.send(primary_key)] }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def preload_many_ar(name, primary_keys, ar_class_name, foreign_key)
|
106
|
+
preload_define name do |items|
|
107
|
+
ids = items.map { |item| item.send(primary_keys) }.flatten.compact.uniq
|
108
|
+
data = ar_class_name.constantize.where(foreign_key => ids).map { |c| [c.send(foreign_key), c] }.to_h
|
109
|
+
items.map { |item| item.send(primary_keys)&.map { |pk| data[pk] } }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
extend Decors::DecoratorDefinition
|
114
|
+
|
115
|
+
define_mixin_decorator :PreloadDependencies, PreloadDependencies
|
116
|
+
define_mixin_decorator :PreloadBatch, PreloadBatch
|
117
|
+
|
118
|
+
def declare_preloader(name, preloader)
|
119
|
+
define_method(:"#{name}__priloo__") { preloader }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Priloo
|
4
|
+
module Preloaders
|
5
|
+
# This class implements preloading for ActiveRecord associations
|
6
|
+
class ArAssociationPreloader < BasePreloader
|
7
|
+
attr_reader :ar_class, :name
|
8
|
+
|
9
|
+
def initialize(ar_class, name)
|
10
|
+
super([self.class, ar_class, name])
|
11
|
+
|
12
|
+
@name = name
|
13
|
+
@ar_class = ar_class
|
14
|
+
end
|
15
|
+
|
16
|
+
def injected?(target)
|
17
|
+
target.association_cached?(name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def extract(target)
|
21
|
+
target.send(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def preload(ar_list)
|
25
|
+
# Rails does not provide any way to preload an association without immediately
|
26
|
+
# storing the result in the instances.
|
27
|
+
ActiveRecord::Associations::Preloader.new.preload(ar_list.map(&:_preloadable_target), name)
|
28
|
+
ar_list.map(&name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Priloo
|
4
|
+
module Preloaders
|
5
|
+
class BasePreloader
|
6
|
+
attr_reader :merge_key
|
7
|
+
|
8
|
+
def initialize(merge_key)
|
9
|
+
@merge_key = merge_key
|
10
|
+
end
|
11
|
+
|
12
|
+
def multiplicity
|
13
|
+
0
|
14
|
+
end
|
15
|
+
|
16
|
+
def injected?(_target)
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def preload(instances)
|
21
|
+
instances
|
22
|
+
end
|
23
|
+
|
24
|
+
def inject(_target, _value); end
|
25
|
+
|
26
|
+
def dependencies
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Priloo
|
4
|
+
module Preloaders
|
5
|
+
# This class implements preloading for Bannerman's custom ActiveRecord injectors
|
6
|
+
class BmInjectorPreloader < BasePreloader
|
7
|
+
attr_reader :ar_class, :name
|
8
|
+
|
9
|
+
def initialize(ar_class, name)
|
10
|
+
super([self.class, ar_class, name])
|
11
|
+
|
12
|
+
@name = name
|
13
|
+
@ar_class = ar_class
|
14
|
+
end
|
15
|
+
|
16
|
+
def preload(ar_list)
|
17
|
+
ids = ar_list.map { |ar| ar.send(primary_key) }
|
18
|
+
injector_sql = ar_class.send("#{name}_sql")
|
19
|
+
sql_query = Arel.sql <<~SQL
|
20
|
+
#{quote_table_column(ar_class.table_name, primary_key)} AS id,
|
21
|
+
(#{injector_sql}) AS val
|
22
|
+
SQL
|
23
|
+
|
24
|
+
fetched_data = ar_class.where(primary_key => ids.uniq).pluck(sql_query).to_h
|
25
|
+
ar_list.map { |ar| fetched_data[ar.send(primary_key)] }
|
26
|
+
end
|
27
|
+
|
28
|
+
def inject(target, value)
|
29
|
+
target._preloadable_target.send(:write_attribute_without_type_cast, name, value)
|
30
|
+
end
|
31
|
+
|
32
|
+
def injected?(target)
|
33
|
+
target.has_attribute?(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract(target)
|
37
|
+
target.instance_variable_get(:@attributes).[](name).value
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def quote_identifier(str)
|
43
|
+
ar_class.connection.quote_column_name str.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def quote_table_column(table, column)
|
47
|
+
quote_identifier(table) + '.' + quote_identifier(column)
|
48
|
+
end
|
49
|
+
|
50
|
+
def primary_key
|
51
|
+
@primary_key ||= ar_class.primary_key.to_sym
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module Priloo
|
6
|
+
module Preloaders
|
7
|
+
class CollectionPreloader < BasePreloader
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
super(self.class)
|
12
|
+
end
|
13
|
+
|
14
|
+
def multiplicity
|
15
|
+
1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Priloo
|
4
|
+
module Preloaders
|
5
|
+
class GenericPreloader < BasePreloader
|
6
|
+
attr_reader :name, :block, :dependencies
|
7
|
+
|
8
|
+
PRIVATE_STORE_NAME = :@preloader_private_store
|
9
|
+
|
10
|
+
def initialize(name, dependencies: [], &block)
|
11
|
+
super(self)
|
12
|
+
@name = name
|
13
|
+
@dependencies = dependencies
|
14
|
+
@block = block
|
15
|
+
end
|
16
|
+
|
17
|
+
def preload(instances)
|
18
|
+
block[instances]
|
19
|
+
end
|
20
|
+
|
21
|
+
def injected?(target)
|
22
|
+
fetch_store(target)&.key?(name) || false
|
23
|
+
end
|
24
|
+
|
25
|
+
def extract(target)
|
26
|
+
fetch_store(target).fetch(name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def inject(target, value)
|
30
|
+
fetch_or_create_store(target)[name] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def fetch_store(target)
|
36
|
+
target._preloadable_target.instance_variable_get(PRIVATE_STORE_NAME)
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch_or_create_store(target)
|
40
|
+
obj = target._preloadable_target
|
41
|
+
return fetch_store(target) if obj.instance_variable_defined?(PRIVATE_STORE_NAME)
|
42
|
+
|
43
|
+
store = {}
|
44
|
+
obj.instance_variable_set(PRIVATE_STORE_NAME, store)
|
45
|
+
store
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Priloo
|
4
|
+
module Preloaders
|
5
|
+
class NavigatingPreloader < BasePreloader
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
super([self.class, name])
|
10
|
+
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def preload(instances)
|
15
|
+
instances.map { |inst| extract(inst) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def extract(target)
|
19
|
+
return target[name] if target.is_a?(Hash)
|
20
|
+
return target.send(name) if target.respond_to?(name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Priloo
|
4
|
+
module Preloaders
|
5
|
+
class NilPreloader < BasePreloader
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def preload(instances)
|
13
|
+
Array.new(instances.size)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Priloo
|
4
|
+
class RecursiveInjector
|
5
|
+
def inject(list, *path)
|
6
|
+
inject_deep(list.to_a, canonicalize(path))
|
7
|
+
list
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Little wrapper for 'inject_deep' implementing support for list-of-lists.
|
13
|
+
# The input list will be flattened 'multiplicity' times.
|
14
|
+
#
|
15
|
+
# 1- Flatten the input 'list' such that it can be handled by 'inject_deep'
|
16
|
+
# 2- Call 'inject_deep'
|
17
|
+
# 3- Un-flatten the preloaded output
|
18
|
+
def inject_deep_flat(list, remaining_path, multiplicity)
|
19
|
+
return inject_deep(list, remaining_path) if multiplicity == 0
|
20
|
+
|
21
|
+
flat_input = list.flat_map { |v| v.nil? ? [] : v }
|
22
|
+
flat_output = inject_deep_flat(flat_input, remaining_path, multiplicity - 1)
|
23
|
+
item_index = -1
|
24
|
+
list.map { |v| v&.map { flat_output[item_index += 1] } }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Extract the necessary preloads & resolve their dependencies.
|
28
|
+
#
|
29
|
+
# Return:
|
30
|
+
# - A new 'resolved_paths' including all recursively resolved dependencies, ordered appropriately.
|
31
|
+
# - A two-level list of preloaders[key_index][item_index] (every key/item couple gets its own preloader)
|
32
|
+
def resolve_dependencies(list, paths)
|
33
|
+
dependencies = {}
|
34
|
+
preloaders = {}
|
35
|
+
undiscovered = paths.keys
|
36
|
+
|
37
|
+
until undiscovered.empty?
|
38
|
+
undiscovered.each do |dep|
|
39
|
+
dependencies[dep] = Set.new
|
40
|
+
preloaders[dep] = list.map { |item| find_preloader(item, dep) }
|
41
|
+
|
42
|
+
deps = preloaders[dep].uniq(&:merge_key)
|
43
|
+
.reject { |preloader| preloader.is_a?(Preloaders::NilPreloader) }
|
44
|
+
.map { |p| canonicalize(p.dependencies) }
|
45
|
+
.uniq
|
46
|
+
|
47
|
+
if deps.size > 1
|
48
|
+
# This is a limitation of the current implementation, and at some point we will have to
|
49
|
+
# fix it otherwise it could prevent some things to be done.
|
50
|
+
raise NotImplementedError, 'Different dependencies at the same level are not supported'
|
51
|
+
end
|
52
|
+
|
53
|
+
deps.each do |d|
|
54
|
+
dependencies[dep] += d.keys
|
55
|
+
paths = paths.deep_merge(d)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
undiscovered = paths.keys - dependencies.keys
|
60
|
+
end
|
61
|
+
|
62
|
+
order = Dependencies.resolve(dependencies)
|
63
|
+
|
64
|
+
resolved_paths = order.map { |key| [key, paths[key]] }.to_h
|
65
|
+
preloaders = order.map { |key| preloaders[key] }
|
66
|
+
|
67
|
+
[resolved_paths, preloaders]
|
68
|
+
end
|
69
|
+
|
70
|
+
PreloadedValue = Struct.new(:multiplicity, :index, :value)
|
71
|
+
|
72
|
+
def preload_single_key(list, preloaders)
|
73
|
+
list.each_with_index
|
74
|
+
.group_by { |_item, item_idx| preloaders[item_idx].merge_key }
|
75
|
+
.flat_map do |_merge_key, indexed_items|
|
76
|
+
preloader = preloaders[indexed_items.first.last]
|
77
|
+
items = indexed_items.map(&:first)
|
78
|
+
preload_items(preloader, items).each_with_index.map do |value, value_idx|
|
79
|
+
PreloadedValue.new(preloader.multiplicity, indexed_items[value_idx][1], value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def preload_items(preloader, items)
|
85
|
+
map_by_group items,
|
86
|
+
group_by: ->(x) { preloader.injected?(x) },
|
87
|
+
map_to: ->(injected, filtered_items) {
|
88
|
+
next filtered_items.map { |x| preloader.extract(x) } if injected
|
89
|
+
|
90
|
+
preloader.preload(filtered_items).tap do |preloaded_values|
|
91
|
+
filtered_items.each_with_index do |item, index|
|
92
|
+
preloader.inject(item, preloaded_values[index])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def preload_next_level(preloaded_values, next_level)
|
99
|
+
next_values = Array.new(preloaded_values.size)
|
100
|
+
|
101
|
+
preloaded_values.group_by(&:multiplicity)
|
102
|
+
.flat_map do |multiplicity, grouped_preloaded_values|
|
103
|
+
values = grouped_preloaded_values.map(&:value)
|
104
|
+
results = inject_deep_flat(values, next_level, multiplicity)
|
105
|
+
|
106
|
+
results.each_with_index do |result, result_index|
|
107
|
+
next_values[grouped_preloaded_values[result_index].index] = result
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
next_values
|
112
|
+
end
|
113
|
+
|
114
|
+
# Preload dependencies at top level and recursively preload next levels
|
115
|
+
def inject_deep(list, paths)
|
116
|
+
resolved_paths, preloaders = resolve_dependencies(list, paths)
|
117
|
+
|
118
|
+
resolved_paths.each_with_index do |(_key, next_level), key_idx|
|
119
|
+
# We preload the key for every item
|
120
|
+
preloaded_values = preload_single_key(list, preloaders[key_idx])
|
121
|
+
|
122
|
+
# We recursively preload the next level
|
123
|
+
preload_next_level(preloaded_values, next_level)
|
124
|
+
end
|
125
|
+
|
126
|
+
list
|
127
|
+
end
|
128
|
+
|
129
|
+
def map_by_group(list, group_by: proc { nil }, map_to: proc { |_g, v| v })
|
130
|
+
output = Array.new(list.size)
|
131
|
+
|
132
|
+
list.each_with_index.group_by { |item, _idx| group_by[item] }
|
133
|
+
.each do |group_key, input_items|
|
134
|
+
input_items_values = input_items.map(&:first)
|
135
|
+
result = map_to[group_key, input_items_values]
|
136
|
+
|
137
|
+
raise 'Mapper should return same number of rows' unless result.size == input_items.size
|
138
|
+
|
139
|
+
result.each_with_index do |result_item, result_idx|
|
140
|
+
input_item = input_items[result_idx]
|
141
|
+
input_item_idx = input_item.last
|
142
|
+
|
143
|
+
output[input_item_idx] = result_item
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
output
|
148
|
+
end
|
149
|
+
|
150
|
+
# Find a preloader for any object
|
151
|
+
def find_preloader(item, property)
|
152
|
+
return Preloaders::NilPreloader.instance if item.nil?
|
153
|
+
|
154
|
+
method = :"#{property}__priloo__"
|
155
|
+
preloader = item.send(method) if item.respond_to? method
|
156
|
+
preloader ||= Preloaders::CollectionPreloader.instance if property == :__each__ && item.is_a?(Enumerable)
|
157
|
+
preloader ||= Preloaders::NavigatingPreloader.new(property)
|
158
|
+
raise "Cannot find any preloader for property '#{property}' of #{item}" unless preloader
|
159
|
+
|
160
|
+
preloader
|
161
|
+
end
|
162
|
+
|
163
|
+
# Transform a user-friendly path into a canonicalized form more suitable to further processing (idempotent)
|
164
|
+
#
|
165
|
+
# User-friendly: [:a, :b, {c: [:d], b: :x}]
|
166
|
+
# Canonicalized: {:a=>{}, :b=>{:x=>{}}, :c=>{:d=>{}}}
|
167
|
+
def canonicalize(path)
|
168
|
+
case path
|
169
|
+
when Array then path.reduce({}) { |a, p| a.merge(canonicalize(p)) }
|
170
|
+
when Hash then path.map { |k, v| [k.to_sym, canonicalize(v)] }.to_h
|
171
|
+
else { path.to_sym => {} }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: priloo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Frederic Terrazzoni
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bm-typed
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: coveralls
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.8'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.59.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.59.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activerecord
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 5.2.1
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 5.2.1
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: decors
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.3'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.3'
|
125
|
+
description: A generalized Rails-like preloader
|
126
|
+
email:
|
127
|
+
- frederic.terrazzoni@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- lib/preloader.rb
|
133
|
+
- lib/priloo.rb
|
134
|
+
- lib/priloo/active_record_support.rb
|
135
|
+
- lib/priloo/dependencies.rb
|
136
|
+
- lib/priloo/preloadable.rb
|
137
|
+
- lib/priloo/preloaders/ar_association_preloader.rb
|
138
|
+
- lib/priloo/preloaders/base_preloader.rb
|
139
|
+
- lib/priloo/preloaders/bm_injector_preloader.rb
|
140
|
+
- lib/priloo/preloaders/collection_preloader.rb
|
141
|
+
- lib/priloo/preloaders/generic_preloader.rb
|
142
|
+
- lib/priloo/preloaders/navigating_preloader.rb
|
143
|
+
- lib/priloo/preloaders/nil_preloader.rb
|
144
|
+
- lib/priloo/recursive_injector.rb
|
145
|
+
- lib/priloo/version.rb
|
146
|
+
homepage: https://github.com/getbannerman/priloo
|
147
|
+
licenses:
|
148
|
+
- MIT
|
149
|
+
metadata: {}
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 2.7.6
|
167
|
+
signing_key:
|
168
|
+
specification_version: 4
|
169
|
+
summary: A generalized Rails-like preloader
|
170
|
+
test_files: []
|