perry 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ require 'perry/cacheable/store'
2
+ require 'perry/cacheable/entry'
3
+ require 'digest/md5'
4
+
5
+ module Perry::Cacheable
6
+
7
+ module ClassMethods
8
+
9
+ # TRP: Default to a 5 minute cache
10
+ DEFAULT_LONGEVITY = 5*60
11
+ @@cache_store = nil
12
+
13
+ def reset_cache_store(default_longevity=DEFAULT_LONGEVITY)
14
+ @@cache_store = Perry::Cacheable::Store.new(default_longevity)
15
+ end
16
+
17
+ def cache_store
18
+ @@cache_store
19
+ end
20
+
21
+ protected
22
+
23
+ def fetch_records_with_caching(relation)
24
+ options = relation.to_hash
25
+ key = Digest::MD5.hexdigest(self.to_s + options.to_a.sort { |a,b| a.to_s.first <=> b.to_s.first }.inspect)
26
+ cache_hit = self.cache_store.read(key)
27
+ get_fresh = options.delete(:fresh)
28
+
29
+ if cache_hit && !get_fresh
30
+ self.read_adapter.log(options, "CACHE #{self.name}")
31
+ cache_hit.each { |fv| fv.fresh = false.freeze }
32
+ cache_hit
33
+ else
34
+ fresh_value = fetch_records_without_caching(relation)
35
+
36
+ # TRP: Only store in cache if record count is below the cache_record_count_threshold (if it is set)
37
+ if !self.cache_record_count_threshold || fresh_value.size <= self.cache_record_count_threshold
38
+ self.cache_store.write(key, fresh_value, (self.cache_expires) ? fresh_value[0].send(self.cache_expires) : Time.now + DEFAULT_LONGEVITY) unless fresh_value.empty?
39
+ fresh_value.each { |fv| fv.fresh = true.freeze }
40
+ end
41
+
42
+ fresh_value
43
+ end
44
+ end
45
+
46
+ def enable_caching(options)
47
+ reset_cache_store unless cache_store
48
+ self.cacheable = true
49
+ self.cache_expires = options[:expires]
50
+ self.cache_record_count_threshold = options[:record_count_threshold]
51
+ end
52
+
53
+ end
54
+
55
+ module InstanceMethods
56
+
57
+ def fresh?
58
+ @fresh
59
+ end
60
+
61
+ end
62
+
63
+ def self.included(receiver)
64
+ receiver.class_inheritable_accessor :cache_expires, :cache_record_count_threshold
65
+ receiver.send :attr_accessor, :fresh
66
+
67
+ receiver.extend ClassMethods
68
+ receiver.send :include, InstanceMethods
69
+
70
+ # TRP: Remap calls for RPC through the caching mechanism
71
+ receiver.class_eval do
72
+ class << self
73
+ alias_method :fetch_records_without_caching, :fetch_records
74
+ alias_method :fetch_records, :fetch_records_with_caching
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,18 @@
1
+ module Perry::Cacheable
2
+
3
+ class Entry
4
+
5
+ attr_accessor :value, :expire_at
6
+
7
+ def initialize(value, expire_at)
8
+ self.value = value
9
+ self.expire_at = expire_at
10
+ end
11
+
12
+ def expired?
13
+ Time.now > self.expire_at rescue true
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,45 @@
1
+ module Perry::Cacheable
2
+
3
+ class Store
4
+
5
+ attr_reader :store
6
+ attr_accessor :default_longevity
7
+
8
+ # TRP: Specify the default longevity of a cache entry in seconds
9
+ def initialize(default_longevity)
10
+ @store = {}
11
+ @default_longevity = default_longevity
12
+ end
13
+
14
+ def clear(key=nil)
15
+ if key
16
+ @store.delete(key)
17
+ else
18
+ @store.each do |key, entry|
19
+ clear(key) if key && (!entry || entry.expired?)
20
+ end
21
+ end
22
+ end
23
+
24
+ # TRP: Returns value if a cache hit and the entry is not expired, nil otherwise
25
+ def read(key)
26
+ if entry = @store[key]
27
+ if entry.expired?
28
+ clear(key)
29
+ nil
30
+ else
31
+ entry.value
32
+ end
33
+ end
34
+ end
35
+
36
+ # TRP: Write a new cache value and optionally specify the expire time
37
+ # this also will clear all expired items out of the store to keep memory consumption as low as possible
38
+ def write(key, value, expires=Time.now + default_longevity)
39
+ clear
40
+ @store[key] = Entry.new(value, expires)
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,14 @@
1
+ # TRP: Copied from Rails 3RC1 on 8/17/2010 (not available in Rails2)
2
+ module Kernel
3
+ # Returns the object's singleton class.
4
+ def singleton_class
5
+ class << self
6
+ self
7
+ end
8
+ end unless respond_to?(:singleton_class) # exists in 1.9.2
9
+
10
+ # class_eval on an object acts like singleton_class.class_eval.
11
+ def class_eval(*args, &block)
12
+ singleton_class.class_eval(*args, &block)
13
+ end
14
+ end
@@ -0,0 +1,49 @@
1
+ module Perry
2
+
3
+ # Generic Perry error
4
+ #
5
+ class PerryError < StandardError
6
+ end
7
+
8
+ # Raised when Perry cannot find records from a given id or set of ids
9
+ #
10
+ class RecordNotFound < PerryError
11
+ end
12
+
13
+ # Raised when Perry cannot save a record through the write_adapter
14
+ # and save! or update_attributes! was used
15
+ #
16
+ class RecordNotSaved < PerryError
17
+ end
18
+
19
+ # Used for all association related errors
20
+ #
21
+ class AssociationError < PerryError
22
+ end
23
+
24
+ # Raised when trying to eager load an association that relies on instance
25
+ # level data.
26
+ # class Article < Perry::Base
27
+ # has_many :comments, :conditions => lambda { |article| ... }
28
+ # end
29
+ #
30
+ # Article.recent.includes(:comments) # Raises AssociationPreloadNotSupported
31
+ #
32
+ class AssociationPreloadNotSupported < AssociationError
33
+ end
34
+
35
+ # Raised when trying to eager load an association that does not exist or
36
+ # trying to create a :has_many_through association with a nonexistant
37
+ # association as the source
38
+ #
39
+ class AssociationNotFound < AssociationError
40
+ end
41
+
42
+ # Raised when a polymorphic association type is not present in the specified
43
+ # scope. Always be sure that any values set for the type attribute on any
44
+ # polymorphic association are real constants defined in :polymorphic_namespace
45
+ #
46
+ class PolymorphicAssociationTypeError < AssociationError
47
+ end
48
+
49
+ end
@@ -0,0 +1,54 @@
1
+ module Perry
2
+
3
+ module Logger
4
+ module ClassMethods
5
+
6
+ def log(params, name)
7
+ if block_given?
8
+ result = nil
9
+ ms = Benchmark.measure { result = yield }.real
10
+ log_info(params, name, ms*1000)
11
+ result
12
+ else
13
+ log_info(params, name, 0)
14
+ []
15
+ end
16
+ rescue Exception => err
17
+ log_info(params, name, 0)
18
+ Perry.logger.error("#{err.message} \n\n#{err.backtrace.join('\n')}")
19
+ []
20
+ end
21
+
22
+ private
23
+
24
+ def log_info(params, name, ms)
25
+ if Perry.logger && Perry.logger.debug?
26
+ name = '%s (%.1fms)' % [name || 'RPC', ms]
27
+ Perry.logger.debug(format_log_entry(name, params.inspect))
28
+ end
29
+ end
30
+
31
+ def format_log_entry(message, dump=nil)
32
+ message_color, dump_color = "4;33;1", "0;1"
33
+
34
+ log_entry = " \e[#{message_color}m#{message}\e[0m "
35
+ log_entry << "\e[#{dump_color}m%#{String === dump ? 's' : 'p'}\e[0m" % dump if dump
36
+ log_entry
37
+ end
38
+
39
+ end
40
+
41
+ module InstanceMethods
42
+
43
+ def log(*args, &block)
44
+ self.class.send(:log, *args, &block)
45
+ end
46
+ end
47
+
48
+ def self.included(receiver)
49
+ receiver.extend ClassMethods
50
+ receiver.send :include, InstanceMethods
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,55 @@
1
+ module Perry::Persistence
2
+
3
+ module ClassMethods
4
+
5
+ protected
6
+
7
+ def create_writer(attribute)
8
+ define_method("#{attribute}=") do |value|
9
+ self[attribute] = value
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+
16
+ module InstanceMethods
17
+
18
+ def []=(attribute, value)
19
+ set_attribute(attribute, value)
20
+ end
21
+
22
+ def attributes=(attributes)
23
+ set_attributes(attributes)
24
+ end
25
+
26
+ def save
27
+ write_adapter.write(self)
28
+ end
29
+
30
+ def save!
31
+ save or raise Perry::RecordNotSaved
32
+ end
33
+
34
+ def update_attributes(attributes)
35
+ self.attributes = attributes
36
+ save
37
+ end
38
+
39
+ def update_attributes!(attributes)
40
+ update_attributes(attributes) or raise Perry::RecordNotSaved
41
+ end
42
+
43
+ def destroy
44
+ write_adapter.delete(self) unless self.new_record?
45
+ end
46
+ alias :delete :destroy
47
+
48
+ end
49
+
50
+ def self.included(receiver)
51
+ receiver.extend ClassMethods
52
+ receiver.send :include, InstanceMethods
53
+ end
54
+
55
+ end
@@ -0,0 +1,153 @@
1
+ require 'perry/relation/query_methods'
2
+ require 'perry/relation/finder_methods'
3
+
4
+ # TRP: The concepts behind this class are heavily influenced by ActiveRecord::Relation v.3.0.0RC1
5
+ # => http://github.com/rails/rails
6
+ # Used to achieve the chainability of scopes -- methods are delegated back and forth from BM::Base and BM::Relation
7
+ class Perry::Relation
8
+ attr_reader :klass
9
+ attr_accessor :records
10
+
11
+ SINGLE_VALUE_METHODS = [:limit, :offset, :from, :fresh]
12
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :includes, :where, :having]
13
+
14
+ FINDER_OPTIONS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS + [:conditions, :search, :sql]
15
+
16
+ include Perry::QueryMethods
17
+ include Perry::FinderMethods
18
+
19
+ def initialize(klass)
20
+ @klass = klass
21
+
22
+ SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)}
23
+ MULTI_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_values", [])}
24
+ end
25
+
26
+ def merge(r)
27
+ merged_relation = clone
28
+ return merged_relation unless r
29
+
30
+ SINGLE_VALUE_METHODS.each do |option|
31
+ new_value = r.send("#{option}_value")
32
+ merged_relation = merged_relation.send(option, new_value) if new_value
33
+ end
34
+
35
+ MULTI_VALUE_METHODS.each do |option|
36
+ merged_relation = merged_relation.send(option, *r.send("#{option}_values"))
37
+ end
38
+
39
+ merged_relation
40
+ end
41
+
42
+ def to_hash
43
+ # TRP: If present pass :sql option alone as it trumps all other options
44
+ @hash ||= if self.raw_sql_value
45
+ { :sql => raw_sql_value }
46
+ else
47
+ hash = SINGLE_VALUE_METHODS.inject({}) do |h, option|
48
+ value = self.send("#{option}_value")
49
+ value = call_procs(value)
50
+ value ? h.merge(option => value) : h
51
+ end
52
+
53
+ hash.merge!((MULTI_VALUE_METHODS - [:select]).inject({}) do |h, option|
54
+ value = self.send("#{option}_values")
55
+ value = call_procs(value)
56
+ value && !value.empty? ? h.merge(option => value.uniq) : h
57
+ end)
58
+
59
+ # TRP: If one of the select options contains a * than select options are ignored
60
+ if select_values && !select_values.empty? && !select_values.any? { |val| val.to_s.match(/\*$/) }
61
+ value = call_procs(select_values)
62
+ hash.merge!(:select => value.uniq)
63
+ end
64
+
65
+ hash
66
+ end
67
+ end
68
+
69
+ def reset_queries
70
+ @hash = nil
71
+ @records = nil
72
+ end
73
+
74
+ def to_a
75
+ @records ||= fetch_records
76
+ end
77
+
78
+ def eager_load?
79
+ @includes_values && !@includes_values.empty?
80
+ end
81
+
82
+ def inspect
83
+ to_a.inspect
84
+ end
85
+
86
+ def scoping
87
+ @klass.scoped_methods << self
88
+ begin
89
+ yield
90
+ ensure
91
+ @klass.scoped_methods.pop
92
+ end
93
+ end
94
+
95
+ def respond_to?(method, include_private=false)
96
+ super ||
97
+ Array.method_defined?(method) ||
98
+ @klass.scopes[method] ||
99
+ dynamic_finder_method(method) ||
100
+ @klass.respond_to?(method, false)
101
+ end
102
+
103
+ def dynamic_finder_method(method)
104
+ defined_attributes = (@klass.defined_attributes || []).join('|')
105
+ if method.to_s =~ /^(find_by|find_all_by)_(#{defined_attributes})/
106
+ [$1, $2]
107
+ else
108
+ nil
109
+ end
110
+ end
111
+
112
+ protected
113
+
114
+ def method_missing(method, *args, &block)
115
+ if Array.method_defined?(method)
116
+ to_a.send(method, *args, &block)
117
+ elsif result = dynamic_finder_method(method)
118
+ method, attribute = result
119
+ options = { :conditions => { attribute => args[0] } }
120
+ options.merge!(args[1]) if args[1] && args[1].is_a?(Hash)
121
+ case method.to_sym
122
+ when :find_by
123
+ self.first(options)
124
+ when :find_all_by
125
+ self.all(options)
126
+ end
127
+ elsif @klass.scopes[method]
128
+ merge(@klass.send(method, *args, &block))
129
+ elsif @klass.respond_to?(method, false)
130
+ scoping { @klass.send(method, *args, &block) }
131
+ else
132
+ super
133
+ end
134
+ end
135
+
136
+ def fetch_records
137
+ @klass.send(:fetch_records, self)
138
+ end
139
+
140
+ private
141
+
142
+ def call_procs(values)
143
+ case values
144
+ when Array:
145
+ values.collect { |v| v.is_a?(Proc) ? v.call : v }
146
+ when Proc:
147
+ values.call
148
+ else
149
+ values
150
+ end
151
+ end
152
+
153
+ end