generation_cacheable 1.0.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/ChangeLog +2 -0
  7. data/Gemfile +16 -0
  8. data/README.md +33 -0
  9. data/Rakefile +8 -0
  10. data/generation_cacheable.gemspec +23 -0
  11. data/lib/gen_cache.rb +32 -0
  12. data/lib/gen_cache/cache_io/fetching.rb +78 -0
  13. data/lib/gen_cache/cache_io/formatting.rb +75 -0
  14. data/lib/gen_cache/cache_io/parsing.rb +66 -0
  15. data/lib/gen_cache/cache_types/association_cache.rb +50 -0
  16. data/lib/gen_cache/cache_types/attribute_cache.rb +30 -0
  17. data/lib/gen_cache/cache_types/class_method_cache.rb +23 -0
  18. data/lib/gen_cache/cache_types/key_cache.rb +14 -0
  19. data/lib/gen_cache/cache_types/method_cache.rb +28 -0
  20. data/lib/gen_cache/caches.rb +15 -0
  21. data/lib/gen_cache/expiry.rb +37 -0
  22. data/lib/gen_cache/keys.rb +70 -0
  23. data/lib/gen_cache/version.rb +3 -0
  24. data/spec/gen_cache/cache_io/fetching_spec.rb +41 -0
  25. data/spec/gen_cache/cache_io/formatting_spec.rb +59 -0
  26. data/spec/gen_cache/cache_io/parsing_spec.rb +47 -0
  27. data/spec/gen_cache/cache_types/association_cache_spec.rb +313 -0
  28. data/spec/gen_cache/cache_types/attribute_cache_spec.rb +98 -0
  29. data/spec/gen_cache/cache_types/class_method_cache_spec.rb +72 -0
  30. data/spec/gen_cache/cache_types/key_cache_spec.rb +34 -0
  31. data/spec/gen_cache/cache_types/method_cache_spec.rb +108 -0
  32. data/spec/gen_cache/expiry_spec.rb +182 -0
  33. data/spec/gen_cache/keys_spec.rb +66 -0
  34. data/spec/gen_cache_spec.rb +76 -0
  35. data/spec/models/account.rb +7 -0
  36. data/spec/models/comment.rb +9 -0
  37. data/spec/models/descendant.rb +20 -0
  38. data/spec/models/group.rb +6 -0
  39. data/spec/models/image.rb +11 -0
  40. data/spec/models/location.rb +3 -0
  41. data/spec/models/post.rb +31 -0
  42. data/spec/models/tag.rb +6 -0
  43. data/spec/models/user.rb +54 -0
  44. data/spec/spec_helper.rb +96 -0
  45. metadata +149 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: add01c39075f3847dfc1e750ece37f1329de16f5
4
+ data.tar.gz: 2f5c3410a8f11824e9d77a774ea5f57ef849b4e5
5
+ SHA512:
6
+ metadata.gz: 0ba9aade08e246c5a995c00636f2ea483533b043ade43b1e6739430951c291d723d540939bfa37ee8fd8d13288d6022ed29f7ad9009debd05c14439a775bc008
7
+ data.tar.gz: 8d975869ee3d4922c3c9557d08dc453bc14b7f195ba6f13b08062d80d3ecc2b666ca5e44a8f5789d23a3027b165ef229355d0176c9d76cdf516cb268569115fe
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ pkg/
4
+ bin/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format nested
2
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ generation_cacheable
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0
data/ChangeLog ADDED
@@ -0,0 +1,2 @@
1
+ 0.0.1
2
+ * Initial implementation
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cacheable.gemspec
4
+ gemspec
5
+
6
+ platforms :ruby do
7
+ gem "sqlite3"
8
+ gem "memcached"
9
+ gem "cityhash"
10
+ end
11
+
12
+ platforms :jruby do
13
+ gem "activerecord-jdbc-adapter"
14
+ gem "activerecord-jdbcsqlite3-adapter"
15
+ gem "jruby-memcached"
16
+ end
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ Generation Cacheable
2
+ ====================
3
+
4
+ A rails cache implementation that began as a fork of [simple cacheable](https://github.com/flyerhzm/simple_cacheable) and incorporated some ideas from [identity cache](https://github.com/Shopify/identity_cache) as well.
5
+
6
+ Usage
7
+ =====
8
+
9
+ ```ruby
10
+ class User < ActiveRecord::Base
11
+ include GenCache
12
+
13
+ has_one :profile
14
+ has_many :friends
15
+
16
+ model_cache do
17
+ with_key # => User.find_cached(1)
18
+ with_attribute :name # => User.find_cached_by_name("Pathouse")
19
+ with_method :meaning_of_life # => User.cached_meaning_of_life
20
+ with_class_method :population # => User.cached_population
21
+ with_association :profile, :friends # => User.cached_profile User.cached_friends
22
+ end
23
+
24
+ def self.population
25
+ all.count
26
+ end
27
+
28
+ def meaning_of_life
29
+ 42
30
+ end
31
+ end
32
+ ```
33
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "gen_cache/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "generation_cacheable"
7
+ s.version = GenerationCacheable::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Pat McGee"]
10
+ s.email = ["patmcgee331@gmail.com"]
11
+ s.homepage = "https://github.com/pathouse/generation-cacheable"
12
+ s.summary = %q{a simple cache implementation with attribute based expiry}
13
+ s.description = %q{a simple cache implementation with attribute based expiry}
14
+
15
+ s.add_dependency("rails", ">= 3.0.0")
16
+ s.add_development_dependency("rspec", "2.8")
17
+ s.add_development_dependency("mocha", "0.10.5")
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ end
data/lib/gen_cache.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'uri'
2
+ require 'cityhash'
3
+ require "gen_cache/caches"
4
+ require "gen_cache/keys"
5
+ require "gen_cache/expiry"
6
+ require "gen_cache/cache_io/fetching"
7
+ require "gen_cache/cache_io/formatting"
8
+ require "gen_cache/cache_io/parsing"
9
+
10
+ module GenCache
11
+
12
+ def self.included(base)
13
+ base.extend(GenCache::Caches)
14
+ base.extend(GenCache::ClassMethods)
15
+
16
+ base.class_eval do
17
+ class_attribute :cached_key,
18
+ :cached_indices,
19
+ :cached_methods,
20
+ :cached_class_methods,
21
+ :cached_associations
22
+ after_commit :expire_all
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+ def model_cache(&block)
28
+ instance_exec &block
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,78 @@
1
+ module GenCache
2
+
3
+ def self.fetch(key_blob, options={}, &block)
4
+ unless key_blob.is_a?(Array)
5
+ single_fetch(key_blob, options) { yield if block_given? }
6
+ else
7
+ multiple_fetch(key_blob) { yield if block_given? }
8
+ end
9
+ end
10
+
11
+ def self.single_fetch(key_blob, options, &block)
12
+ result = read_from_cache(key_blob)
13
+ method_args = symbolize_args(options[:args])
14
+ should_write = false
15
+
16
+ if block_given?
17
+ if method_args != :no_args && (result.nil? || result[method_args].nil?)
18
+ result ||= {}
19
+ result[method_args] = yield
20
+ should_write = true
21
+ elsif method_args == :no_args && result.nil?
22
+ result = yield
23
+ should_write = true
24
+ end
25
+ end
26
+
27
+ write_to_cache(key_blob, result) if should_write
28
+
29
+ result = (method_args == :no_args) ? result : result[method_args]
30
+ end
31
+
32
+ def self.multiple_fetch(key_blobs, &block)
33
+ results = read_multi_from_cache(key_blobs)
34
+ if results.nil?
35
+ if block_given?
36
+ results = yield
37
+ write_multi_to_cache(results)
38
+ end
39
+ end
40
+ results.values
41
+ end
42
+
43
+ ##
44
+ ## READING FROM THE CACHE
45
+ ##
46
+
47
+ def self.read_from_cache(key_blob)
48
+ result = Rails.cache.read key_blob[:key]
49
+ return result if result.nil?
50
+ parse_with_key(result, key_blob[:type])
51
+ end
52
+
53
+ def self.read_multi_from_cache(key_blobs)
54
+ keys = key_blobs.map { |blob| blob[:key] }
55
+ results = Rails.cache.read_multi(*keys)
56
+ return nil if results.values.all?(&:nil?)
57
+ results.each do |key, value|
58
+ type = key_blobs.select {|kb| kb.has_value?(key) }.first[:type]
59
+ results[key] = parse_with_key(value, type)
60
+ end
61
+ results
62
+ end
63
+
64
+ ###
65
+ ### WRITING TO THE CACHE
66
+ ###
67
+
68
+ def self.write_multi_to_cache(keys_and_results)
69
+ keys_and_results.each do |key, result|
70
+ write_to_cache(key, result)
71
+ end
72
+ end
73
+
74
+ def self.write_to_cache(key_blob, result)
75
+ formatted_result = format_with_key(result, key_blob[:type])
76
+ Rails.cache.write key_blob[:key], formatted_result
77
+ end
78
+ end
@@ -0,0 +1,75 @@
1
+ module GenCache
2
+
3
+ def self.format_with_key(result, key_type)
4
+ return if result.nil?
5
+ if key_type == :association
6
+ result
7
+ elsif key_type == :object
8
+ formatted_result = format_object(result)
9
+ else
10
+ formatted_result = format_method(result)
11
+ end
12
+ end
13
+
14
+ ## OBJECT FORMATTING ##
15
+
16
+ def self.format_object(object)
17
+ if object.is_a?(Array)
18
+ object.map { |obj| coder_from_record(obj) }
19
+ else
20
+ coder_from_record(object)
21
+ end
22
+ end
23
+
24
+ def self.coder_from_record(record)
25
+ unless record.nil?
26
+ coder = { :class => record.class }
27
+ record.encode_with(coder)
28
+ coder
29
+ end
30
+ end
31
+
32
+ ## METHOD FORMATTING ##
33
+ def self.symbolize_args(args)
34
+ return :no_args if args.nil? || args.empty?
35
+ args.map do |arg|
36
+ if arg.is_a?(Hash)
37
+ arg.map {|k,v| "#{k}:#{v}"}.join(",")
38
+ elsif arg.is_a?(Array)
39
+ arg.join(",")
40
+ else
41
+ arg.to_s.split(" ").join("_")
42
+ end
43
+ end.join("+").to_sym
44
+ end
45
+
46
+ def self.escape_punctuation(string)
47
+ string.sub(/\?\Z/, '_query').sub(/!\Z/, '_bang')
48
+ end
49
+
50
+ def self.detect_object(data)
51
+ data.is_a?(ActiveRecord::Base) ||
52
+ (data.is_a?(Array) && data[0].is_a?(ActiveRecord::Base))
53
+ end
54
+
55
+ def self.format_method(result)
56
+ if detect_object(result)
57
+ format_object(result)
58
+ elsif result.is_a?(Hash)
59
+ result.each do |arg_key, value|
60
+ result[arg_key] = format_data(value)
61
+ end
62
+ else
63
+ format_data(result)
64
+ end
65
+ end
66
+
67
+ def self.format_data(result)
68
+ if detect_object(result)
69
+ format_object(result)
70
+ else
71
+ result
72
+ end
73
+ end
74
+
75
+ end
@@ -0,0 +1,66 @@
1
+ module GenCache
2
+
3
+ def self.parse_with_key(result, key_type)
4
+ return if result.nil?
5
+ if key_type == :association
6
+ result
7
+ elsif key_type == :object
8
+ object_parse(result)
9
+ else
10
+ method_parse(result)
11
+ end
12
+ end
13
+
14
+ ## OBJECT PARSING ##
15
+
16
+ def self.object_parse(result)
17
+ if result.is_a?(Array)
18
+ result.map {|obj| record_from_coder(obj)}
19
+ else
20
+ record_from_coder(result)
21
+ end
22
+ end
23
+
24
+ def self.record_from_coder(coder)
25
+ record = coder[:class].allocate
26
+ record.init_with(coder)
27
+ record
28
+ end
29
+
30
+ ## METHOD PARSING ##
31
+ #
32
+ ## METHOD STORE FORMATTING
33
+ #
34
+ # { args.to_string.to_symbol => answer }
35
+
36
+ def self.detect_coder(data)
37
+ (data.is_a?(Hash) && hash_inspect(data)) ||
38
+ (data.is_a?(Array) && data[0].is_a?(Hash) && hash_inspect(data[0]))
39
+ end
40
+
41
+ def self.hash_inspect(hash)
42
+ hash.has_key?(:class) && hash.has_key?('attributes')
43
+ end
44
+
45
+ def self.method_parse(result)
46
+ if detect_coder(result)
47
+ object_parse(result)
48
+ elsif result.is_a?(Hash)
49
+ result.each do |k,v|
50
+ result[k] = data_parse(v)
51
+ end
52
+ else
53
+ data_parse(result)
54
+ end
55
+ end
56
+
57
+ ## DATA PARSING ##
58
+
59
+ def self.data_parse(result)
60
+ if detect_coder(result)
61
+ object_parse(result)
62
+ else
63
+ result
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,50 @@
1
+ module GenCache
2
+ module AssocationCache
3
+
4
+ def with_association(*association_names)
5
+ self.cached_associations ||= []
6
+ self.cached_associations += association_names
7
+
8
+ association_names.each do |assoc_name|
9
+ cached_assoc_methods(assoc_name)
10
+ end
11
+ end
12
+
13
+ def cached_assoc_methods(name)
14
+ method_name = "cached_#{name}"
15
+ define_method(method_name) do
16
+ cache_key = GenCache.association_key(self, name)
17
+
18
+ # an object's association cache holds a collection of the instance keys
19
+ # for the objects returned by a call to that association. These are read first
20
+ if instance_variable_get("@#{method_name}").nil?
21
+ instance_keys = GenCache.fetch(cache_key) do
22
+ Array.wrap(self.send(name)).map { |obj| GenCache.instance_key(obj.class, obj.id) }
23
+ end
24
+
25
+ # all of the instance keys are read in a single multi_read
26
+ association = GenCache.fetch(instance_keys) do |key_blobs|
27
+ result = {}
28
+ instance_keys.each do |ik|
29
+ key_parts = ik[:key].scan(/(^.*)\/(.*)\/(.*$)/).flatten
30
+ result[ik] = Object.const_get(key_parts.first.singularize.capitalize).send(:find, key_parts.last.to_i)
31
+ end
32
+ result
33
+ end
34
+
35
+ # plural associations expect arrays, singular associations expect single objects
36
+ association = if association.size == 0
37
+ name.to_s.pluralize == name.to_s ? [] : nil
38
+ elsif association.size == 1
39
+ name.to_s.pluralize == name.to_s ? association : association.first
40
+ else
41
+ association
42
+ end
43
+
44
+ instance_variable_set("@#{method_name}", association)
45
+ end
46
+ instance_variable_get("@#{method_name}")
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ module GenCache
2
+ module AttributeCache
3
+ def with_attribute(*attributes)
4
+ self.cached_indices ||= {}
5
+ self.cached_indices = self.cached_indices.merge(attributes.each_with_object({}) {
6
+ |attribute, indices| indices[attribute] = {}
7
+ })
8
+
9
+ attributes.each do |attribute|
10
+ define_singleton_method("find_cached_by_#{attribute}") do |value|
11
+ self.cached_indices["#{attribute}"] ||= []
12
+ self.cached_indices["#{attribute}"] << value
13
+ cache_key = GenCache.attribute_key(self, attribute, value)
14
+ GenCache.fetch(cache_key) do
15
+ self.send("find_by_#{attribute}", value)
16
+ end
17
+ end
18
+
19
+ define_singleton_method("find_cached_all_by_#{attribute}") do |value|
20
+ self.cached_indices["#{attribute}"] ||= []
21
+ self.cached_indices["#{attribute}"] << value
22
+ cache_key = GenCache.attribute_key(self, attribute, value, all: true)
23
+ GenCache.fetch(cache_key) do
24
+ self.send("find_all_by_#{attribute}", value)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end