perry 0.4.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.
@@ -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