smart_collection 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 206884a5b82cfb9a32a8084b9e28602e0d06aef1
4
+ data.tar.gz: 4cfc1bd05e9b4e49d026e3388bb1409b9b92f1db
5
+ SHA512:
6
+ metadata.gz: e8f54ca5265ded99d18f0a41036fde1fb2052d6c1c26662482b409ecb7c9689bf14bd08f09fa9e10a7fa1cec331c979f88165e79d4fd1d7db0c52b7da5843599
7
+ data.tar.gz: de95c2aabb00d01f30fc5f60b826d2b8071d9ecd04e76a245466845a3d9f0a31a927bfc0db9d602ea12b100c8a81e916ad25044684e352c6423032c63ef461a7
@@ -0,0 +1,63 @@
1
+ module SmartCollection
2
+ module Associations
3
+ module Preloader
4
+ class SmartCollectionCachedByTable < ActiveRecord::Associations::Preloader::HasManyThrough
5
+ def through_reflection
6
+ owners.first.class.reflect_on_association(:cached_items)
7
+ end
8
+
9
+ def source_reflection
10
+ association_name = reflection.options[:smart_collection].items_name.to_s.singularize.to_sym
11
+ through_reflection.klass.reflect_on_association(association_name)
12
+ end
13
+
14
+ def associated_records_by_owner preloader
15
+ owners.reject(&:cache_exists?).each(&:update_cache)
16
+ super
17
+ end
18
+
19
+ private
20
+
21
+ end
22
+
23
+ class SmartCollectionCachedByCacheStore < ActiveRecord::Associations::Preloader::CollectionAssociation
24
+ def associated_records_by_owner preloader
25
+ owners.reject(&:cache_exists?).each(&:update_cache)
26
+ loaded = reflection.options[:smart_collection].cache_manager.read_multi(owners)
27
+ records = reflection.options[:smart_collection].item_class.where(id: loaded.values.flatten.uniq).map{|x| [x.id, x]}.to_h
28
+ loaded.map do |owner, ids|
29
+ [owner, ids.map{|x| records[x]}]
30
+ end
31
+ end
32
+
33
+ def preload(preloader)
34
+ associated_records_by_owner(preloader).each do |owner, records|
35
+ association = owner.association(reflection.name)
36
+ association.loaded!
37
+ association.target.concat(records)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ module ActiveRecordPreloaderPatch
45
+ def preloader_for(reflection, owners, rhs_klass)
46
+ if reflection.options[:smart_collection]
47
+ unless reflection.options[:smart_collection].cache_manager
48
+ raise RuntimeError, "Turn on cache to enable preloading."
49
+ end
50
+ case reflection.options[:smart_collection].cache_manager
51
+ when SmartCollection::CacheManager::CacheStore
52
+ SmartCollection::Associations::Preloader::SmartCollectionCachedByCacheStore
53
+ when SmartCollection::CacheManager::Table
54
+ SmartCollection::Associations::Preloader::SmartCollectionCachedByTable
55
+ end
56
+ else
57
+ super
58
+ end
59
+ end
60
+ end
61
+
62
+ ActiveRecord::Associations::Preloader.prepend ActiveRecordPreloaderPatch
63
+ end
@@ -0,0 +1,75 @@
1
+ module SmartCollection
2
+ module Associations
3
+ class SmartCollectionAssociation < ::ActiveRecord::Associations::HasManyAssociation
4
+
5
+ class ScopeBuilder
6
+ def initialize rule, klass
7
+ @rule = rule
8
+ @klass = klass
9
+ @klass_hash = {}
10
+ end
11
+
12
+ def build
13
+ rule_to_bulk_queries @rule
14
+ bulk_load
15
+ rule_to_scope @rule
16
+ end
17
+
18
+ def bulk_load
19
+ @klass_hash = @klass_hash.map do |klass_name, ids|
20
+ [klass_name, Object.const_get(klass_name).where(id: ids).map{|x| [x.id, x]}.to_h]
21
+ end.to_h
22
+ end
23
+
24
+ def rule_to_bulk_queries rule
25
+ case
26
+ when arr = (rule['or'] || rule['and'])
27
+ arr.each{|x| rule_to_bulk_queries x}
28
+ when assoc = rule['association']
29
+ ids = @klass_hash[assoc['class_name']] ||= []
30
+ ids << assoc['id']
31
+ end
32
+ end
33
+
34
+ def rule_to_scope rule
35
+ case
36
+ when ors = rule['or']
37
+ ors.map{|x| rule_to_scope x}.inject(:or)
38
+ when ands = rule['and']
39
+ ands.map{|x| rule_to_scope x}.inject(:merge)
40
+ when assoc = rule['association']
41
+ @klass_hash[assoc['class_name']][assoc['id']].association(assoc['source']).scope
42
+ when cond = rule['condition']
43
+ scope = @klass
44
+ scope = scope.joins(cond['joins'].to_sym) if cond['joins']
45
+ scope.where(cond['where'])
46
+ end
47
+ end
48
+ end
49
+
50
+ def association_scope
51
+ if cache_manager = reflection.options[:smart_collection].cache_manager
52
+ unless cache_manager.cache_exists? owner
53
+ owner.update_cache
54
+ end
55
+ cached_scope
56
+ else
57
+ uncached_scope
58
+ end
59
+ end
60
+
61
+ def skip_statement_cache?
62
+ true
63
+ end
64
+
65
+ def uncached_scope
66
+ ScopeBuilder.new(owner.rule, reflection.klass).build
67
+ end
68
+
69
+ def cached_scope
70
+ reflection.options[:smart_collection].cache_manager.read owner
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,26 @@
1
+ module SmartCollection
2
+ module Builder
3
+ class SmartCollectionAssociation < ::ActiveRecord::Associations::Builder::CollectionAssociation
4
+ def self.macro
5
+ :has_many
6
+ end
7
+
8
+ def self.valid_options(options)
9
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors, :smart_collection]
10
+ end
11
+
12
+ def self.valid_dependent_options
13
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
14
+ end
15
+
16
+ def self.create_reflection(model, name, scope, options, extension = nil)
17
+ raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
18
+
19
+ validate_options(options)
20
+
21
+ scope = build_scope(scope, extension)
22
+ Reflection::SmartCollectionReflection.new(name, scope, options, model)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ module SmartCollection
2
+ class CacheManager
3
+ class CacheStore < CacheManager
4
+
5
+ def cache_store
6
+ @config.cache_config[:cache_store]
7
+ end
8
+
9
+ def cache_key owner
10
+ "_smart_collection_#{owner.class.name}_#{owner.id}"
11
+ end
12
+
13
+ def update owner
14
+ association = owner.association(@config.items_name)
15
+
16
+ cache_store.write(cache_key(owner), Marshal.dump(association.uncached_scope.pluck(:id)))
17
+ owner.update(cache_expires_at: Time.now + expires_in)
18
+ end
19
+
20
+ def read owner
21
+ @config.item_class.where(id: Marshal.load(cache_store.read(cache_key owner)))
22
+ end
23
+
24
+ def read_multi owners
25
+ cache_keys = owners.map{|owner| cache_key owner}
26
+ loaded = cache_store.read_multi(*cache_keys)
27
+ owners.map.with_index do |owner, index|
28
+ [owner, Marshal.load(loaded[cache_keys[index]])]
29
+ end.to_h
30
+ end
31
+
32
+ def cache_exists? owner
33
+ !(owner.cache_expires_at.nil? || owner.cache_expires_at < Time.now) && cache_store.exist?(cache_key owner)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
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
+ options = @config.raw_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 options[:items].to_s.singularize.to_sym, class_name: options[: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_#{options[:items]}".to_sym, class_name: options[:class_name], through: :cached_items, source: options[:items].to_s.singularize.to_sym
23
+ end
24
+ @cache_model = cached_item_model
25
+ end
26
+
27
+ def update owner
28
+ association = owner.association(@config.items_name)
29
+
30
+ @cache_model.where(collection_id: owner.id).delete_all
31
+ @cache_model.connection.execute "INSERT INTO #{@cache_model.table_name} (collection_id, item_id) #{association.uncached_scope.select(owner.id, :id).to_sql}"
32
+ owner.update(cache_expires_at: Time.now + expires_in)
33
+ end
34
+
35
+ def read owner
36
+ cache_association = owner.association("cached_#{@config.items_name}")
37
+ cache_association.scope
38
+ end
39
+
40
+ def cache_exists? owner
41
+ !(owner.cache_expires_at.nil? || owner.cache_expires_at < Time.now)
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ module SmartCollection
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
+ def initialize model:, config:
14
+ @model = model
15
+ @config = config
16
+ end
17
+
18
+ def update owner
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def read owner
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def expires_in
27
+ @config.cache_config[:expires_in] || 1.hour
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ module SmartCollection
2
+ class Config
3
+ attr_accessor :cache_manager
4
+ attr_reader :raw_config
5
+
6
+ def initialize raw_config
7
+ @raw_config = raw_config
8
+ end
9
+
10
+ def items_name
11
+ @raw_config[:items]
12
+ end
13
+
14
+ def item_class_name
15
+ @raw_config[:item_class]
16
+ end
17
+
18
+ def item_class
19
+ item_class_name.constantize
20
+ end
21
+
22
+ def cache_config
23
+ @raw_config[:cached_by]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ module SmartCollection
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
+ @__smart_collection_mixin ||= self.class.ancestors.find do |x|
18
+ x.instance_of? Mixin
19
+ end
20
+ end
21
+ end
22
+
23
+ attr_reader :config
24
+
25
+ def initialize items:, item_class: nil, cached_by: nil
26
+ @raw_config = {
27
+ items: items,
28
+ item_class: item_class,
29
+ cached_by: cached_by
30
+ }
31
+ end
32
+
33
+ def included base
34
+ COLLECTIONS[base] = true
35
+ @config = config = SmartCollection::Config.new(@raw_config)
36
+ name = config.items_name
37
+
38
+ options = {smart_collection: config}
39
+ options[:class_name] = config.item_class_name if config.item_class_name
40
+ reflection = Builder::SmartCollectionAssociation.build(base, name, nil, options)
41
+ ::ActiveRecord::Reflection.add_reflection base, name, reflection
42
+ base.include(InstanceMethods)
43
+
44
+ if cache_class = CacheManager.determine_class(@raw_config)
45
+ config.cache_manager = cache_class.new(model: base, config: config)
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ module SmartCollection
2
+ module Reflection
3
+ class SmartCollectionReflection < ::ActiveRecord::Reflection::HasManyReflection
4
+ def association_class
5
+ Associations::SmartCollectionAssociation
6
+ end
7
+
8
+ def check_eager_loadable!
9
+ raise RuntimeError, 'eager_load is not supported by now.'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ require 'active_record'
2
+
3
+ module SmartCollection
4
+ COLLECTIONS = {}
5
+ end
6
+
7
+ Dir.glob("#{File.dirname(__FILE__)}/**/*.rb").each do |file|
8
+ require file
9
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smart_collection
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - CicholGricenchos
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
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: database_cleaner
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: ''
70
+ email:
71
+ - cichol@live.cn
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/smart_collection.rb
77
+ - lib/smart_collection/associations/preloader/smart_collection.rb
78
+ - lib/smart_collection/associations/smart_collection_association.rb
79
+ - lib/smart_collection/builder/smart_collection_association.rb
80
+ - lib/smart_collection/cache_manager.rb
81
+ - lib/smart_collection/cache_manager/cache_store.rb
82
+ - lib/smart_collection/cache_manager/table.rb
83
+ - lib/smart_collection/config.rb
84
+ - lib/smart_collection/mixin.rb
85
+ - lib/smart_collection/reflection/smart_collection_reflection.rb
86
+ homepage: https://github.com/CicholGricenchos/smart_collection
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.5.2.1
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: ''
110
+ test_files: []