smart_collection 0.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/lib/smart_collection/cache_manager.rb +28 -16
- data/lib/smart_collection/config.rb +47 -2
- data/lib/smart_collection/instance_methods.rb +19 -0
- data/lib/smart_collection/mixin.rb +59 -61
- data/lib/smart_collection/version.rb +1 -1
- metadata +12 -10
- data/lib/smart_collection/cache_manager/cache_store.rb +0 -42
- data/lib/smart_collection/cache_manager/table.rb +0 -44
- data/lib/smart_collection/scope_builder.rb +0 -46
- data/lib/smart_collection/validator.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ab885e83fde2cc95d9864c47930a00c074228be053f02a4c115188fa815be2c0
|
4
|
+
data.tar.gz: 83f2cec29972fa10282e8aa35a41577afded2dff176f85c4db627c820996069e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 05a2b4e9d08c201646a72db23941074e7d6a41aff1dee36bad1013349759e5ba7b5a38ee4024650e1b14b720ddc3d14fe646e5b8d1f31518733795274f1a56a9
|
7
|
+
data.tar.gz: 46afdfe614697e6ae5efebe1f46e7fc8179c0dad073ffc63cc65b7be570ab6a1a7c02e2ce05b4d719881887c6f92ff2a842ba383009770c84bd74ebd42f8dfab
|
@@ -1,34 +1,46 @@
|
|
1
1
|
module SmartCollection
|
2
2
|
class CacheManager
|
3
|
-
|
4
|
-
def self.determine_class config
|
5
|
-
case
|
6
|
-
when config.dig(:cached_by, :table)
|
7
|
-
SmartCollection::CacheManager::Table
|
8
|
-
when config.dig(:cached_by, :cache_store)
|
9
|
-
SmartCollection::CacheManager::CacheStore
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
3
|
def initialize model:, config:
|
14
4
|
@model = model
|
15
5
|
@config = config
|
6
|
+
define_cache_association_for model
|
7
|
+
end
|
8
|
+
|
9
|
+
def define_cache_association_for model
|
10
|
+
config = @config
|
11
|
+
cached_item_model = nil
|
12
|
+
model.class_eval do
|
13
|
+
cached_item_model = Class.new ActiveRecord::Base do
|
14
|
+
self.table_name = config.cache_table_name
|
15
|
+
belongs_to config.item_name, class_name: config.item_class_name, foreign_key: :item_id
|
16
|
+
end
|
17
|
+
const_set("CachedItem", cached_item_model)
|
18
|
+
|
19
|
+
has_many :cached_items, class_name: cached_item_model.name, foreign_key: :collection_id
|
20
|
+
has_many "cached_#{config.items_name}".to_sym, class_name: config.item_class_name, through: :cached_items, source: config.item_name
|
21
|
+
end
|
22
|
+
@cache_model = cached_item_model
|
16
23
|
end
|
17
24
|
|
18
25
|
def update owner
|
19
|
-
|
26
|
+
scopes = @config.scopes_proc.(owner)
|
27
|
+
@cache_model.where(collection_id: owner.id).delete_all
|
28
|
+
#scopes.each do |scope|
|
29
|
+
# sql = scope.select(owner.id, :id).to_sql
|
30
|
+
# @cache_model.connection.execute "INSERT INTO #{@cache_model.table_name} (collection_id, item_id) #{sql}"
|
31
|
+
#end
|
32
|
+
@cache_model.connection.execute "INSERT INTO #{@cache_model.table_name} (collection_id, item_id) #{owner.smart_collection_mixin.uncached_scope(owner).select(owner.id, :id).to_sql}"
|
33
|
+
owner.update_column(:cache_expires_at, Time.now + @config.cache_expires_in_proc.(owner))
|
20
34
|
end
|
21
35
|
|
22
36
|
def read_scope owner
|
23
|
-
|
37
|
+
cache_association = owner.association("cached_#{@config.items_name}")
|
38
|
+
cache_association.scope
|
24
39
|
end
|
25
40
|
|
26
41
|
def cache_exists? owner
|
27
|
-
|
42
|
+
owner.cache_expires_at && owner.cache_expires_at > Time.now
|
28
43
|
end
|
29
44
|
|
30
|
-
def expires_in
|
31
|
-
@config.cache_config[:expires_in] || 1.hour
|
32
|
-
end
|
33
45
|
end
|
34
46
|
end
|
@@ -5,10 +5,11 @@ module SmartCollection
|
|
5
5
|
|
6
6
|
def initialize raw_config
|
7
7
|
@raw_config = raw_config
|
8
|
+
check_config
|
8
9
|
end
|
9
10
|
|
10
11
|
def items_name
|
11
|
-
@raw_config[:
|
12
|
+
@raw_config[:item_association]
|
12
13
|
end
|
13
14
|
|
14
15
|
def item_name
|
@@ -16,7 +17,7 @@ module SmartCollection
|
|
16
17
|
end
|
17
18
|
|
18
19
|
def item_class_name
|
19
|
-
@raw_config[:
|
20
|
+
@raw_config[:item_class_name]
|
20
21
|
end
|
21
22
|
|
22
23
|
def item_class
|
@@ -26,5 +27,49 @@ module SmartCollection
|
|
26
27
|
def cache_config
|
27
28
|
@raw_config[:cached_by]
|
28
29
|
end
|
30
|
+
|
31
|
+
def scopes_proc
|
32
|
+
case @raw_config[:scopes]
|
33
|
+
when Proc
|
34
|
+
@raw_config[:scopes]
|
35
|
+
when Symbol
|
36
|
+
-> (owner) { owner.send(@raw_config[:scopes]) }
|
37
|
+
else
|
38
|
+
raise "scopes option only accepts a Proc / Symbol"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def inverse_association
|
43
|
+
@raw_config[:inverse_association]
|
44
|
+
end
|
45
|
+
|
46
|
+
def cache_table_name
|
47
|
+
if @raw_config[:cache_table] == :default
|
48
|
+
:smart_collection_cached_items
|
49
|
+
else
|
50
|
+
@raw_config[:cache_table]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def cache_expires_in_proc
|
55
|
+
case @raw_config[:cache_expires_in]
|
56
|
+
when Proc
|
57
|
+
@raw_config[:cache_expires_in]
|
58
|
+
when ActiveSupport::Duration
|
59
|
+
-> (owner) { @raw_config[:cache_expires_in] }
|
60
|
+
when Symbol
|
61
|
+
-> (owner) { owner.send(@raw_config[:cache_expires_in]) }
|
62
|
+
else
|
63
|
+
raise "cache_expires_in option only accepts a Proc / ActiveSupport::Duration / Symbol"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_config
|
68
|
+
raise "items option must be provided" if items_name.nil?
|
69
|
+
raise "item_class option must be provided" if item_class_name.nil?
|
70
|
+
raise "scopes option must be provided" if @raw_config[:scopes].nil?
|
71
|
+
raise "cache_table option must be provided" if @raw_config[:cache_table].nil?
|
72
|
+
raise "cache_expires_in option must be provided" if @raw_config[:cache_expires_in].nil?
|
73
|
+
end
|
29
74
|
end
|
30
75
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
module InstanceMethods
|
3
|
+
def update_cache
|
4
|
+
smart_collection_mixin.config.cache_manager.update self
|
5
|
+
end
|
6
|
+
|
7
|
+
def expire_cache
|
8
|
+
update_column(:cache_expires_at, Time.now - 1)
|
9
|
+
end
|
10
|
+
|
11
|
+
def cache_exists?
|
12
|
+
smart_collection_mixin.config.cache_manager.cache_exists? self
|
13
|
+
end
|
14
|
+
|
15
|
+
def smart_collection_mixin
|
16
|
+
self.class.smart_collection_mixin
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,23 +1,5 @@
|
|
1
1
|
module SmartCollection
|
2
2
|
class Mixin < Module
|
3
|
-
module InstanceMethods
|
4
|
-
def update_cache
|
5
|
-
smart_collection_mixin.config.cache_manager.update self
|
6
|
-
end
|
7
|
-
|
8
|
-
def expire_cache
|
9
|
-
update_column(:cache_expires_at, nil)
|
10
|
-
end
|
11
|
-
|
12
|
-
def cache_exists?
|
13
|
-
smart_collection_mixin.config.cache_manager.cache_exists? self
|
14
|
-
end
|
15
|
-
|
16
|
-
def smart_collection_mixin
|
17
|
-
self.class.smart_collection_mixin
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
3
|
module ClassMethods
|
22
4
|
def smart_collection_mixin
|
23
5
|
@__smart_collection_mixin ||= ancestors.find do |x|
|
@@ -33,71 +15,87 @@ module SmartCollection
|
|
33
15
|
end
|
34
16
|
|
35
17
|
def uncached_scope owner
|
36
|
-
|
18
|
+
scopes = @config.scopes_proc.(owner)
|
19
|
+
raise unless scopes.all?{|x| x.klass == @config.item_class}
|
20
|
+
if scopes.empty?
|
21
|
+
@config.item_class.where('1 = 0')
|
22
|
+
else
|
23
|
+
scopes = scopes.map do |scope|
|
24
|
+
if !scope.select_values.empty?
|
25
|
+
new_scope = scope.dup
|
26
|
+
new_scope.select_values = []
|
27
|
+
new_scope
|
28
|
+
else
|
29
|
+
scope
|
30
|
+
end
|
31
|
+
end
|
32
|
+
@config.item_class.from("(#{scopes.map{|x| "#{x.to_sql}"}.join(' UNION ')}) as #{@config.item_class.table_name}")
|
33
|
+
end
|
37
34
|
end
|
38
35
|
|
39
36
|
def cached_scope owner
|
40
37
|
@config.cache_manager.read_scope owner
|
41
38
|
end
|
42
39
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
reflection_options = {smart_collection: config}
|
47
|
-
if config.item_class_name
|
48
|
-
reflection_options[:class_name] = config.item_class_name
|
49
|
-
end
|
50
|
-
|
51
|
-
base.include(InstanceMethods)
|
52
|
-
base.extend(ClassMethods)
|
53
|
-
|
54
|
-
if cache_class = CacheManager.determine_class(@raw_config)
|
55
|
-
config.cache_manager = cache_class.new(model: base, config: config)
|
56
|
-
end
|
40
|
+
def define_association base
|
41
|
+
config = @config
|
42
|
+
config.cache_manager = CacheManager.new(model: base, config: config)
|
57
43
|
|
58
44
|
mixin_options = {
|
59
45
|
name: config.items_name,
|
60
46
|
scope: -> owner {
|
61
|
-
if
|
47
|
+
if owner.new_record?
|
48
|
+
uncached_scope(owner)
|
49
|
+
else
|
50
|
+
cache_manager = config.cache_manager
|
62
51
|
unless cache_manager.cache_exists? owner
|
63
52
|
owner.update_cache
|
64
53
|
end
|
65
54
|
cached_scope(owner)
|
66
|
-
else
|
67
|
-
uncached_scope(owner)
|
68
55
|
end
|
69
56
|
},
|
70
57
|
type: :collection
|
71
58
|
}
|
72
59
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
}
|
83
|
-
when cache_class == SmartCollection::CacheManager::Table
|
84
|
-
cached_name = "cached_#{config.items_name}".to_sym
|
85
|
-
mixin_options[:preloader] = -> owners {
|
86
|
-
owners.reject(&:cache_exists?).each(&:update_cache)
|
87
|
-
Associationist.preload(owners, cached_items: config.item_name)
|
88
|
-
owners.map do |owner|
|
89
|
-
[owner, owner.cached_items.map{|item| item.send(config.item_name)}]
|
90
|
-
end.to_h
|
91
|
-
}
|
92
|
-
else
|
93
|
-
mixin_options[:preloader] = -> _ {
|
94
|
-
raise RuntimeError, "Turn on cache to enable preloading."
|
95
|
-
}
|
96
|
-
end
|
60
|
+
cached_name = "cached_#{config.items_name}".to_sym
|
61
|
+
mixin_options[:preloader] = -> owners {
|
62
|
+
owners.reject(&:cache_exists?).each(&:update_cache)
|
63
|
+
Associationist.preload(owners, cached_items: config.item_name)
|
64
|
+
owners.map do |owner|
|
65
|
+
[owner, owner.cached_items.map{|item| item.send(config.item_name)}]
|
66
|
+
end.to_h
|
67
|
+
}
|
68
|
+
|
97
69
|
base.include Associationist::Mixin.new(mixin_options)
|
70
|
+
end
|
71
|
+
|
72
|
+
def expired_scope base
|
73
|
+
base.where(base.arel_table[:cache_expires_at].lt(Time.now))
|
74
|
+
end
|
75
|
+
|
76
|
+
def define_inverse_association base
|
77
|
+
return unless @config.inverse_association
|
98
78
|
|
99
|
-
|
79
|
+
mixin_options = {
|
80
|
+
name: @config.inverse_association,
|
81
|
+
scope: -> owner {
|
82
|
+
expired = base.joins(:cached_items).where(@config.cache_table_name => {item_id: owner.id}).merge(expired_scope base)
|
83
|
+
expired.each(&:update_cache)
|
84
|
+
base.joins(:cached_items).where(@config.cache_table_name => {item_id: owner.id})
|
85
|
+
},
|
86
|
+
type: :collection
|
87
|
+
}
|
88
|
+
@config.item_class.include Associationist::Mixin.new(mixin_options)
|
89
|
+
end
|
90
|
+
|
91
|
+
def included base
|
92
|
+
@config = config = SmartCollection::Config.new(@raw_config)
|
93
|
+
|
94
|
+
base.include(InstanceMethods)
|
95
|
+
base.extend(ClassMethods)
|
100
96
|
|
97
|
+
define_association base
|
98
|
+
define_inverse_association base
|
101
99
|
end
|
102
100
|
end
|
103
101
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_collection
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- CicholGricenchos
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-07-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: associationist
|
@@ -16,14 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.1
|
19
|
+
version: '0.1'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.1.8
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - "~>"
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.1
|
29
|
+
version: '0.1'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.1.8
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: pry
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -89,12 +95,9 @@ extra_rdoc_files: []
|
|
89
95
|
files:
|
90
96
|
- lib/smart_collection.rb
|
91
97
|
- lib/smart_collection/cache_manager.rb
|
92
|
-
- lib/smart_collection/cache_manager/cache_store.rb
|
93
|
-
- lib/smart_collection/cache_manager/table.rb
|
94
98
|
- lib/smart_collection/config.rb
|
99
|
+
- lib/smart_collection/instance_methods.rb
|
95
100
|
- lib/smart_collection/mixin.rb
|
96
|
-
- lib/smart_collection/scope_builder.rb
|
97
|
-
- lib/smart_collection/validator.rb
|
98
101
|
- lib/smart_collection/version.rb
|
99
102
|
homepage: https://github.com/CicholGricenchos/smart_collection
|
100
103
|
licenses:
|
@@ -115,8 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
118
|
- !ruby/object:Gem::Version
|
116
119
|
version: '0'
|
117
120
|
requirements: []
|
118
|
-
|
119
|
-
rubygems_version: 2.5.2.3
|
121
|
+
rubygems_version: 3.2.15
|
120
122
|
signing_key:
|
121
123
|
specification_version: 4
|
122
124
|
summary: collections by rules
|
@@ -1,42 +0,0 @@
|
|
1
|
-
module SmartCollection
|
2
|
-
class CacheManager
|
3
|
-
class CacheStore < CacheManager
|
4
|
-
|
5
|
-
def initialize model:, config:
|
6
|
-
super
|
7
|
-
|
8
|
-
@cache_key_proc = @config.cache_config[:cache_key_proc] || -> (owner) { "_smart_collection_#{owner.class.name}_#{owner.id}" }
|
9
|
-
end
|
10
|
-
|
11
|
-
def cache_store
|
12
|
-
@config.cache_config[:cache_store]
|
13
|
-
end
|
14
|
-
|
15
|
-
def cache_key owner
|
16
|
-
@cache_key_proc.(owner)
|
17
|
-
end
|
18
|
-
|
19
|
-
def update owner
|
20
|
-
cache_store.write(cache_key(owner), Marshal.dump(owner.smart_collection_mixin.uncached_scope(owner).pluck(:id)))
|
21
|
-
owner.update(cache_expires_at: Time.now + expires_in)
|
22
|
-
end
|
23
|
-
|
24
|
-
def read_scope owner
|
25
|
-
@config.item_class.where(id: Marshal.load(cache_store.read(cache_key owner)))
|
26
|
-
end
|
27
|
-
|
28
|
-
def read_multi owners
|
29
|
-
cache_keys = owners.map{|owner| cache_key owner}
|
30
|
-
loaded = cache_store.read_multi(*cache_keys)
|
31
|
-
owners.map.with_index do |owner, index|
|
32
|
-
[owner, Marshal.load(loaded[cache_keys[index]])]
|
33
|
-
end.to_h
|
34
|
-
end
|
35
|
-
|
36
|
-
def cache_exists? owner
|
37
|
-
owner.cache_expires_at && owner.cache_expires_at > Time.now && cache_store.exist?(cache_key owner)
|
38
|
-
end
|
39
|
-
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
module SmartCollection
|
2
|
-
class CacheManager
|
3
|
-
class Table < CacheManager
|
4
|
-
|
5
|
-
def initialize model:, config:
|
6
|
-
super
|
7
|
-
|
8
|
-
define_cache_association_for model
|
9
|
-
end
|
10
|
-
|
11
|
-
def define_cache_association_for model
|
12
|
-
config = @config
|
13
|
-
cached_item_model = nil
|
14
|
-
model.class_eval do
|
15
|
-
cached_item_model = Class.new ActiveRecord::Base do
|
16
|
-
self.table_name = 'smart_collection_cached_items'
|
17
|
-
belongs_to config.item_name, class_name: config.item_class_name, foreign_key: :item_id
|
18
|
-
end
|
19
|
-
const_set("CachedItem", cached_item_model)
|
20
|
-
|
21
|
-
has_many :cached_items, class_name: cached_item_model.name, foreign_key: :collection_id
|
22
|
-
has_many "cached_#{config.items_name}".to_sym, class_name: config.item_class_name, through: :cached_items, source: config.item_name
|
23
|
-
end
|
24
|
-
@cache_model = cached_item_model
|
25
|
-
end
|
26
|
-
|
27
|
-
def update owner
|
28
|
-
@cache_model.where(collection_id: owner.id).delete_all
|
29
|
-
@cache_model.connection.execute "INSERT INTO #{@cache_model.table_name} (collection_id, item_id) #{owner.smart_collection_mixin.uncached_scope(owner).select(owner.id, :id).to_sql}"
|
30
|
-
owner.update_column(:cache_expires_at, Time.now + expires_in)
|
31
|
-
end
|
32
|
-
|
33
|
-
def read_scope owner
|
34
|
-
cache_association = owner.association("cached_#{@config.items_name}")
|
35
|
-
cache_association.scope
|
36
|
-
end
|
37
|
-
|
38
|
-
def cache_exists? owner
|
39
|
-
owner.cache_expires_at && owner.cache_expires_at > Time.now
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
module SmartCollection
|
2
|
-
class ScopeBuilder
|
3
|
-
def initialize rule, klass
|
4
|
-
@rule = rule
|
5
|
-
@klass = klass
|
6
|
-
@klass_hash = {}
|
7
|
-
end
|
8
|
-
|
9
|
-
def build
|
10
|
-
rule_to_bulk_queries @rule
|
11
|
-
bulk_load
|
12
|
-
rule_to_scope @rule
|
13
|
-
end
|
14
|
-
|
15
|
-
def bulk_load
|
16
|
-
@klass_hash = @klass_hash.map do |klass_name, ids|
|
17
|
-
[klass_name, Object.const_get(klass_name).where(id: ids).map{|x| [x.id, x]}.to_h]
|
18
|
-
end.to_h
|
19
|
-
end
|
20
|
-
|
21
|
-
def rule_to_bulk_queries rule
|
22
|
-
case
|
23
|
-
when arr = (rule['or'] || rule['and'])
|
24
|
-
arr.each{|x| rule_to_bulk_queries x}
|
25
|
-
when assoc = rule['association']
|
26
|
-
ids = @klass_hash[assoc['class_name']] ||= []
|
27
|
-
ids << assoc['id']
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def rule_to_scope rule
|
32
|
-
case
|
33
|
-
when ors = rule['or']
|
34
|
-
ors.map{|x| rule_to_scope x}.inject(:or)
|
35
|
-
when ands = rule['and']
|
36
|
-
ands.map{|x| rule_to_scope x}.inject(:merge)
|
37
|
-
when assoc = rule['association']
|
38
|
-
@klass_hash[assoc['class_name']][assoc['id']].association(assoc['source']).scope
|
39
|
-
when cond = rule['condition']
|
40
|
-
scope = @klass
|
41
|
-
scope = scope.joins(cond['joins'].to_sym) if cond['joins']
|
42
|
-
scope.where(cond['where'])
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
@@ -1,10 +0,0 @@
|
|
1
|
-
module SmartCollection
|
2
|
-
class Validator < ActiveModel::Validator
|
3
|
-
def validate record
|
4
|
-
# try to build scope
|
5
|
-
record.association(record.smart_collection_mixin.config.items_name).scope
|
6
|
-
rescue Exception => e
|
7
|
-
record.errors.add(:failed_to_build_scope, e)
|
8
|
-
end
|
9
|
-
end
|
10
|
-
end
|