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.
- data/README.rdoc +38 -0
- data/Rakefile +70 -0
- data/lib/perry.rb +48 -0
- data/lib/perry/adapters.rb +5 -0
- data/lib/perry/adapters/abstract_adapter.rb +92 -0
- data/lib/perry/adapters/bertrpc_adapter.rb +32 -0
- data/lib/perry/adapters/restful_http_adapter.rb +99 -0
- data/lib/perry/association.rb +330 -0
- data/lib/perry/association_preload.rb +69 -0
- data/lib/perry/associations/common.rb +51 -0
- data/lib/perry/associations/contains.rb +64 -0
- data/lib/perry/associations/external.rb +67 -0
- data/lib/perry/base.rb +204 -0
- data/lib/perry/cacheable.rb +80 -0
- data/lib/perry/cacheable/entry.rb +18 -0
- data/lib/perry/cacheable/store.rb +45 -0
- data/lib/perry/core_ext/kernel/singleton_class.rb +14 -0
- data/lib/perry/errors.rb +49 -0
- data/lib/perry/logger.rb +54 -0
- data/lib/perry/persistence.rb +55 -0
- data/lib/perry/relation.rb +153 -0
- data/lib/perry/relation/finder_methods.rb +56 -0
- data/lib/perry/relation/query_methods.rb +69 -0
- data/lib/perry/scopes.rb +45 -0
- data/lib/perry/scopes/conditions.rb +101 -0
- data/lib/perry/serialization.rb +48 -0
- data/lib/perry/version.rb +13 -0
- metadata +187 -0
@@ -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
|
data/lib/perry/errors.rb
ADDED
@@ -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
|
data/lib/perry/logger.rb
ADDED
@@ -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
|