perry 0.4.0 → 0.5.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/Rakefile +2 -1
- data/lib/perry/adapters/abstract_adapter.rb +189 -64
- data/lib/perry/adapters/bertrpc_adapter.rb +3 -2
- data/lib/perry/adapters/restful_http_adapter.rb +36 -11
- data/lib/perry/association.rb +43 -14
- data/lib/perry/base.rb +33 -17
- data/lib/perry/{cacheable → middlewares/cache_records}/entry.rb +7 -7
- data/lib/perry/middlewares/cache_records/scopes.rb +18 -0
- data/lib/perry/{cacheable → middlewares/cache_records}/store.rb +1 -1
- data/lib/perry/middlewares/cache_records.rb +67 -0
- data/lib/perry/middlewares/model_bridge.rb +66 -0
- data/lib/perry/middlewares.rb +8 -0
- data/lib/perry/persistence/response.rb +75 -0
- data/lib/perry/persistence.rb +40 -2
- data/lib/perry/processors/preload_associations.rb +61 -0
- data/lib/perry/processors.rb +5 -0
- data/lib/perry/relation/finder_methods.rb +8 -4
- data/lib/perry/relation/modifiers.rb +37 -0
- data/lib/perry/relation/query_methods.rb +19 -11
- data/lib/perry/relation.rb +9 -4
- data/lib/perry/version.rb +1 -1
- data/lib/perry.rb +5 -0
- metadata +31 -9
- data/lib/perry/association_preload.rb +0 -69
- data/lib/perry/cacheable.rb +0 -80
data/Rakefile
CHANGED
@@ -24,6 +24,7 @@ spec = Gem::Specification.new do |s|
|
|
24
24
|
|
25
25
|
s.add_dependency("activesupport", [">= 2.3.0"])
|
26
26
|
s.add_dependency("bertrpc", [">= 1.3.0"])
|
27
|
+
s.add_dependency("json", [">=1.4.6"])
|
27
28
|
end
|
28
29
|
|
29
30
|
Rake::GemPackageTask.new(spec) do |pkg|
|
@@ -52,7 +53,7 @@ begin
|
|
52
53
|
t.libs = ['test']
|
53
54
|
t.test_files = FileList["test/**/*_test.rb"]
|
54
55
|
t.verbose = true
|
55
|
-
t.rcov_opts = ['--text-report', "-x #{Gem.path}", '-x /Library/Ruby', '-x /usr/lib/ruby']
|
56
|
+
t.rcov_opts = ['--text-report', "-x #{Gem.path.join(',')}", '-x /Library/Ruby', '-x /usr/lib/ruby']
|
56
57
|
end
|
57
58
|
|
58
59
|
task :default => :coverage
|
@@ -1,92 +1,217 @@
|
|
1
1
|
require 'perry/logger'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
##
|
4
|
+
# = Perry::Adapters::AbstractAdapter
|
5
|
+
#
|
6
|
+
# This is the base class from which all adapters should inherit from. Subclasses should overwrite
|
7
|
+
# one or all of read, write, and/or delete. They should also register themselves with a
|
8
|
+
# unique name using the register_as class method.
|
9
|
+
#
|
10
|
+
# Adapters contain a stack of code that is executed on each request. Here is a diagram of the basic
|
11
|
+
# anatomy of the adaper stack:
|
12
|
+
#
|
13
|
+
# +----------------+
|
14
|
+
# | Perry::Base |
|
15
|
+
# +----------------+
|
16
|
+
# |
|
17
|
+
# +----------------+
|
18
|
+
# | Processors |
|
19
|
+
# +----------------+
|
20
|
+
# |
|
21
|
+
# +----------------+
|
22
|
+
# | ModelBridge |
|
23
|
+
# +----------------+
|
24
|
+
# |
|
25
|
+
# +----------------+
|
26
|
+
# | Middlewares |
|
27
|
+
# +----------------+
|
28
|
+
# |
|
29
|
+
# +----------------+
|
30
|
+
# | Adapter |
|
31
|
+
# +----------------+
|
32
|
+
#
|
33
|
+
# Each request is routed through registred processors, the ModelBridge, and registered middlewares
|
34
|
+
# before reaching the adapter. After the adapter does its operation the return value passes through
|
35
|
+
# each item in the stack allowing stack items to do both custom pre and post processing to every
|
36
|
+
# request.
|
37
|
+
#
|
38
|
+
# == Configuration
|
39
|
+
#
|
40
|
+
# You can configure your adapters using the configure method on Perry::Base
|
41
|
+
#
|
42
|
+
# configure(:read) do |config|
|
43
|
+
# config.adapter_var_1 = :custom_value
|
44
|
+
# config.adapter_var_2 = [:some, :values]
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# This block creates a new configuration context. Each context is merged onto the previous context
|
48
|
+
# allowing subclasses to override configuration set by their parent class.
|
49
|
+
#
|
50
|
+
# == Middlewares
|
51
|
+
#
|
52
|
+
# Middlewares allow you to add custom logic between the model and the adapter. A good example is
|
53
|
+
# caching. A caching middleware could be implemented that intercepted a request to the adapter and
|
54
|
+
# returned the cached value for that request. If the request is a cache miss it could pass the
|
55
|
+
# request on to the adapter, and then cache the result for subsequent calls of the same request.
|
56
|
+
#
|
57
|
+
# This is an example mof a no-op middleware:
|
58
|
+
#
|
59
|
+
# class NoOpMiddleware
|
60
|
+
#
|
61
|
+
# def initialize(adapter, config={})
|
62
|
+
# @adapter = adapter
|
63
|
+
# @config = config
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# def call(options)
|
67
|
+
# @adapter.call(options)
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# Though this doesn't do anything it serves to demonstrate the basic structure of a middleware.
|
73
|
+
# Logic could be added to perform caching, custom querying, or custom result processing.
|
74
|
+
#
|
75
|
+
# Middlewares can also be chained to perform several independent actions. Middlewares are configured
|
76
|
+
# through a custom configuration method:
|
77
|
+
#
|
78
|
+
# configure(:read) do |config|
|
79
|
+
# config.add_middleware(MyMiddleware, :config => 'var', :foo => 'bar')
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# == ModelBridge
|
83
|
+
#
|
84
|
+
# The ModelBridge is simply a middleware that is always installed. It instantiates the records from
|
85
|
+
# the data returned by the adapter. It "bridges" the raw data to the mapped object.
|
86
|
+
#
|
87
|
+
# == Processors
|
88
|
+
#
|
89
|
+
# Much like middlewares, processors allow you to insert logic into the request stack. The
|
90
|
+
# differentiation is that processors are able to manipulate the instantiated objects rather than
|
91
|
+
# just the raw data. Processors have access to the objects immediately before passing the data back
|
92
|
+
# to the model space.
|
93
|
+
#
|
94
|
+
# The interface for a processor is identical to that of a middleware. The return value of the call
|
95
|
+
# to adapter; however, is an array of Perry::Base objects rather than Hashes of attributes.
|
96
|
+
#
|
97
|
+
# Configuration is also very similar to middlewares:
|
98
|
+
#
|
99
|
+
# configure(:read) do |config|
|
100
|
+
# config.add_processor(MyProcessor, :config => 'var', :foo => 'bar')
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
#
|
104
|
+
class Perry::Adapters::AbstractAdapter
|
105
|
+
include Perry::Logger
|
106
|
+
|
107
|
+
attr_accessor :config
|
108
|
+
attr_reader :type
|
109
|
+
@@registered_adapters ||= {}
|
110
|
+
|
111
|
+
# Accepts type as :read, :write, or :delete and a base configuration context for this adapter.
|
112
|
+
def initialize(type, config)
|
113
|
+
@type = type.to_sym
|
114
|
+
@configuration_contexts = config.is_a?(Array) ? config : [config]
|
115
|
+
end
|
6
116
|
|
7
|
-
|
8
|
-
|
9
|
-
|
117
|
+
# Wrapper to the standard init method that will lookup the adapter's class based on its registered
|
118
|
+
# symbol name.
|
119
|
+
def self.create(type, config)
|
120
|
+
klass = @@registered_adapters[type.to_sym]
|
121
|
+
klass.new(type, config)
|
122
|
+
end
|
10
123
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
124
|
+
# Return a new adapter of the same type that adds the given configuration context
|
125
|
+
def extend_adapter(config)
|
126
|
+
config = config.is_a?(Array) ? config : [config]
|
127
|
+
self.class.create(self.type, @configuration_contexts + config)
|
128
|
+
end
|
15
129
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
130
|
+
# return the merged configuration object
|
131
|
+
def config
|
132
|
+
@config ||= build_configuration
|
133
|
+
end
|
20
134
|
|
21
|
-
|
22
|
-
|
23
|
-
|
135
|
+
# runs the adapter in the specified type mode -- designed to work with the middleware stack
|
136
|
+
def call(mode, options)
|
137
|
+
@stack ||= self.stack_items.inject(self.method(mode)) do |below, (above_klass, above_config)|
|
138
|
+
above_klass.new(below, above_config)
|
24
139
|
end
|
25
140
|
|
26
|
-
|
27
|
-
|
28
|
-
|
141
|
+
options[:mode] = mode.to_sym
|
142
|
+
@stack.call(options)
|
143
|
+
end
|
29
144
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
145
|
+
# Return an array of added middlewares
|
146
|
+
def middlewares
|
147
|
+
self.config[:middlewares] || []
|
148
|
+
end
|
34
149
|
|
35
|
-
|
36
|
-
|
150
|
+
# Return an array of added processors
|
151
|
+
def processors
|
152
|
+
self.config[:processors] || []
|
153
|
+
end
|
37
154
|
|
38
|
-
|
39
|
-
|
40
|
-
|
155
|
+
# Abstract read method -- overridden by subclasses
|
156
|
+
def read(options)
|
157
|
+
raise(NotImplementedError,
|
158
|
+
"You must not use the abstract adapter. Implement an adapter that extends the " +
|
159
|
+
"Perry::Adapters::AbstractAdapter class and overrides this method.")
|
160
|
+
end
|
41
161
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
162
|
+
# Abstract write method -- overridden by subclasses
|
163
|
+
def write(object)
|
164
|
+
raise(NotImplementedError,
|
165
|
+
"You must not use the abstract adapter. Implement an adapter that extends the " +
|
166
|
+
"Perry::Adapters::AbstractAdapter class and overrides this method.")
|
167
|
+
end
|
47
168
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
169
|
+
# Abstract delete method -- overridden by subclasses
|
170
|
+
def delete(object)
|
171
|
+
raise(NotImplementedError,
|
172
|
+
"You must not use the abstract adapter. Implement an adapter that extends the " +
|
173
|
+
"Perry::Adapters::AbstractAdapter class and overrides this method.")
|
174
|
+
end
|
53
175
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
176
|
+
# New adapters should register themselves using this method
|
177
|
+
def self.register_as(name)
|
178
|
+
@@registered_adapters[name.to_sym] = self
|
179
|
+
end
|
59
180
|
|
60
|
-
|
61
|
-
@@registered_adapters[name.to_sym] = self
|
62
|
-
end
|
181
|
+
protected
|
63
182
|
|
64
|
-
|
183
|
+
def stack_items
|
184
|
+
(processors + [Perry::Middlewares::ModelBridge] + middlewares).reverse
|
185
|
+
end
|
65
186
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
187
|
+
# TRP: Run each configure block in order of class hierarchy / definition and merge the results.
|
188
|
+
def build_configuration
|
189
|
+
@configuration_contexts.inject({}) do |sum, config|
|
190
|
+
if config.is_a?(Hash)
|
191
|
+
sum.merge(config)
|
192
|
+
else
|
193
|
+
AdapterConfig.new(sum).tap { |ac| config.call(ac) }.marshal_dump
|
74
194
|
end
|
75
195
|
end
|
196
|
+
end
|
76
197
|
|
77
|
-
|
198
|
+
class AdapterConfig < OpenStruct
|
78
199
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
200
|
+
def add_middleware(klass, config={})
|
201
|
+
self.middlewares ||= []
|
202
|
+
self.middlewares << [klass, config]
|
203
|
+
end
|
83
204
|
|
84
|
-
|
85
|
-
|
86
|
-
|
205
|
+
def add_processor(klass, config={})
|
206
|
+
self.processors ||= []
|
207
|
+
self.processors << [klass, config]
|
208
|
+
end
|
87
209
|
|
210
|
+
def to_hash
|
211
|
+
marshal_dump
|
88
212
|
end
|
89
213
|
|
90
214
|
end
|
215
|
+
|
91
216
|
end
|
92
217
|
|
@@ -12,9 +12,10 @@ module Perry::Adapters
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def read(options)
|
15
|
-
|
15
|
+
query = options[:relation].to_hash
|
16
|
+
log(query, "RPC #{config[:service]}") {
|
16
17
|
self.service.call.send(self.namespace).send(self.service_name,
|
17
|
-
|
18
|
+
query.merge(config[:default_options] || {}))
|
18
19
|
}
|
19
20
|
end
|
20
21
|
|
@@ -5,20 +5,22 @@ module Perry::Adapters
|
|
5
5
|
class RestfulHTTPAdapter < Perry::Adapters::AbstractAdapter
|
6
6
|
register_as :restful_http
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
@configuration_contexts << { :primary_key => :id }
|
8
|
+
class KeyError < Perry::PerryError
|
9
|
+
def message
|
10
|
+
'(restful_http_adapter) request not sent because primary key value was nil'
|
11
|
+
end
|
13
12
|
end
|
14
13
|
|
15
|
-
|
14
|
+
attr_reader :last_response
|
15
|
+
|
16
|
+
def write(options)
|
17
|
+
object = options[:object]
|
16
18
|
params = build_params_from_attributes(object)
|
17
19
|
object.new_record? ? post_http(object, params) : put_http(object, params)
|
18
20
|
end
|
19
21
|
|
20
|
-
def delete(
|
21
|
-
delete_http(object)
|
22
|
+
def delete(options)
|
23
|
+
delete_http(options[:object])
|
22
24
|
end
|
23
25
|
|
24
26
|
protected
|
@@ -55,7 +57,19 @@ module Perry::Adapters
|
|
55
57
|
self.log(params, "#{method.to_s.upcase} #{req_uri}") do
|
56
58
|
@last_response = Net::HTTP.new(req_uri.host, req_uri.port).start { |http| http.request(request) }
|
57
59
|
end
|
58
|
-
|
60
|
+
|
61
|
+
Perry::Persistence::Response.new.tap do |response|
|
62
|
+
response.status = @last_response.code.to_i if @last_response
|
63
|
+
response.success = parse_response_code(@last_response)
|
64
|
+
response.meta = http_headers(@last_response).to_hash if @last_response
|
65
|
+
response.raw = @last_response.body if @last_response
|
66
|
+
response.raw_format = config[:format] ? config[:format].gsub(/\W/, '').to_sym : nil
|
67
|
+
end
|
68
|
+
rescue KeyError => ex
|
69
|
+
Perry::Persistence::Response.new.tap do |response|
|
70
|
+
response.success = false
|
71
|
+
response.parsed = { :base => ex.message }
|
72
|
+
end
|
59
73
|
end
|
60
74
|
|
61
75
|
def parse_response_code(response)
|
@@ -69,7 +83,8 @@ module Perry::Adapters
|
|
69
83
|
|
70
84
|
def build_params_from_attributes(object)
|
71
85
|
if self.config[:post_body_wrapper]
|
72
|
-
|
86
|
+
defaults = self.config[:default_options]
|
87
|
+
params = defaults ? defaults.dup : {}
|
73
88
|
params.merge!(object.write_options[:default_options]) if object.write_options.is_a?(Hash) && object.write_options[:default_options].is_a?(Hash)
|
74
89
|
|
75
90
|
object.attributes.each do |attribute, value|
|
@@ -84,7 +99,11 @@ module Perry::Adapters
|
|
84
99
|
|
85
100
|
def build_uri(object, method)
|
86
101
|
url = [self.config[:host].gsub(%r{/$}, ''), self.config[:service]]
|
87
|
-
|
102
|
+
unless object.new_record?
|
103
|
+
primary_key = self.config[:primary_key] || object.primary_key
|
104
|
+
pk_value = object.send(primary_key) or raise KeyError
|
105
|
+
url << pk_value
|
106
|
+
end
|
88
107
|
uri = URI.parse "#{url.join('/')}#{self.config[:format]}"
|
89
108
|
|
90
109
|
# TRP: method DELETE has no POST body so we have to append any default options onto the query string
|
@@ -95,5 +114,11 @@ module Perry::Adapters
|
|
95
114
|
uri
|
96
115
|
end
|
97
116
|
|
117
|
+
def http_headers(response)
|
118
|
+
response.to_hash.inject({}) do |clean_headers, (key, values)|
|
119
|
+
clean_headers.merge(key => values.length > 1 ? values : values.first)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
98
123
|
end
|
99
124
|
end
|
data/lib/perry/association.rb
CHANGED
@@ -43,8 +43,14 @@ module Perry::Association
|
|
43
43
|
raise NotImplementedError, "You must define collection? in subclasses."
|
44
44
|
end
|
45
45
|
|
46
|
-
def primary_key
|
47
|
-
options[:primary_key]
|
46
|
+
def primary_key(object=nil)
|
47
|
+
if options[:primary_key]
|
48
|
+
options[:primary_key]
|
49
|
+
elsif is_a?(BelongsTo)
|
50
|
+
target_klass(object).primary_key
|
51
|
+
elsif is_a?(Has)
|
52
|
+
source_klass.primary_key
|
53
|
+
end
|
48
54
|
end
|
49
55
|
|
50
56
|
def foreign_key
|
@@ -52,10 +58,18 @@ module Perry::Association
|
|
52
58
|
end
|
53
59
|
|
54
60
|
def target_klass(object=nil)
|
61
|
+
eager_loading = object.is_a?(Array)
|
55
62
|
if options[:polymorphic] && object
|
56
63
|
poly_type = object.is_a?(Perry::Base) ? object.send("#{id}_type") : object
|
57
64
|
end
|
58
65
|
|
66
|
+
# This is an eager loading attempt
|
67
|
+
if eager_loading && !eager_loadable?
|
68
|
+
raise(Perry::AssociationPreloadNotSupported,
|
69
|
+
"This association cannot be eager loaded. It has config with procs or it is a " +
|
70
|
+
"polymorphic belongs_to association.")
|
71
|
+
end
|
72
|
+
|
59
73
|
klass = if poly_type
|
60
74
|
type_string = [
|
61
75
|
options[:polymorphic_namespace],
|
@@ -92,9 +106,10 @@ module Perry::Association
|
|
92
106
|
# TRP: Only eager loadable if association query does not depend on instance
|
93
107
|
# data
|
94
108
|
def eager_loadable?
|
95
|
-
Perry::Relation::FINDER_OPTIONS.inject(
|
96
|
-
condition
|
109
|
+
dynamic_config = Perry::Relation::FINDER_OPTIONS.inject(false) do |condition, key|
|
110
|
+
condition || options[key].respond_to?(:call)
|
97
111
|
end
|
112
|
+
!dynamic_config && !(self.polymorphic? && self.is_a?(BelongsTo))
|
98
113
|
end
|
99
114
|
|
100
115
|
protected
|
@@ -113,7 +128,8 @@ module Perry::Association
|
|
113
128
|
|
114
129
|
# TRP: Make sure the value looks like a variable syntaxtually
|
115
130
|
def sanitize_type_attribute(string)
|
116
|
-
string
|
131
|
+
string =~ /^[a-zA-Z]\w*/
|
132
|
+
Regexp.last_match.to_s
|
117
133
|
end
|
118
134
|
|
119
135
|
end
|
@@ -154,8 +170,15 @@ module Perry::Association
|
|
154
170
|
# where(:id => source[:foo_id])
|
155
171
|
#
|
156
172
|
def scope(object)
|
157
|
-
if object
|
158
|
-
|
173
|
+
if object.is_a? Array
|
174
|
+
keys = object.collect(&self.foreign_key.to_sym)
|
175
|
+
keys = nil if keys.empty?
|
176
|
+
else
|
177
|
+
keys = object[self.foreign_key]
|
178
|
+
end
|
179
|
+
if keys
|
180
|
+
scope = base_scope(object)
|
181
|
+
scope.where(self.primary_key(object) => keys)
|
159
182
|
end
|
160
183
|
end
|
161
184
|
|
@@ -192,13 +215,19 @@ module Perry::Association
|
|
192
215
|
# where(:parent_id => source[:id], :parent_type => source.class)
|
193
216
|
#
|
194
217
|
def scope(object)
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
218
|
+
if object.is_a? Array
|
219
|
+
keys = object.collect(&self.primary_key.to_sym)
|
220
|
+
keys = nil if keys.empty?
|
221
|
+
klass = object.first.class
|
222
|
+
else
|
223
|
+
keys = object[self.primary_key]
|
224
|
+
klass = object.class
|
225
|
+
end
|
226
|
+
if keys
|
227
|
+
scope = base_scope(object).where(self.foreign_key => keys)
|
228
|
+
scope = scope.where(polymorphic_type => Perry::Base.base_class_name(klass)) if polymorphic?
|
229
|
+
scope
|
200
230
|
end
|
201
|
-
s
|
202
231
|
end
|
203
232
|
|
204
233
|
def polymorphic_type
|
@@ -288,7 +317,7 @@ module Perry::Association
|
|
288
317
|
else
|
289
318
|
relation = relation.where(
|
290
319
|
proc do
|
291
|
-
{ target_association.primary_key => proxy_ids.call }
|
320
|
+
{ target_association.primary_key(options[:source_type]) => proxy_ids.call }
|
292
321
|
end
|
293
322
|
)
|
294
323
|
end
|
data/lib/perry/base.rb
CHANGED
@@ -2,28 +2,29 @@
|
|
2
2
|
require 'perry/errors'
|
3
3
|
require 'perry/associations/contains'
|
4
4
|
require 'perry/associations/external'
|
5
|
-
require 'perry/association_preload'
|
6
|
-
require 'perry/cacheable'
|
7
5
|
require 'perry/serialization'
|
8
6
|
require 'perry/relation'
|
9
7
|
require 'perry/scopes'
|
10
8
|
require 'perry/adapters'
|
11
|
-
|
9
|
+
require 'perry/middlewares'
|
10
|
+
require 'perry/processors'
|
12
11
|
|
13
12
|
class Perry::Base
|
14
13
|
include Perry::Associations::Contains
|
15
14
|
include Perry::Associations::External
|
16
|
-
include Perry::AssociationPreload
|
17
15
|
include Perry::Serialization
|
18
16
|
include Perry::Scopes
|
19
17
|
|
20
|
-
|
18
|
+
DEFAULT_PRIMARY_KEY = :id
|
19
|
+
|
20
|
+
attr_accessor :attributes, :new_record, :saved, :read_options, :write_options
|
21
21
|
alias :new_record? :new_record
|
22
|
+
alias :saved? :saved
|
23
|
+
alias :persisted? :saved?
|
22
24
|
|
23
|
-
class_inheritable_accessor :read_adapter, :write_adapter,
|
25
|
+
class_inheritable_accessor :read_adapter, :write_adapter,
|
24
26
|
:defined_attributes, :scoped_methods, :defined_associations
|
25
27
|
|
26
|
-
self.cacheable = false
|
27
28
|
self.defined_associations = {}
|
28
29
|
self.defined_attributes = []
|
29
30
|
|
@@ -36,6 +37,14 @@ class Perry::Base
|
|
36
37
|
@attributes[attribute.to_s]
|
37
38
|
end
|
38
39
|
|
40
|
+
def errors
|
41
|
+
@errors ||= {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def primary_key
|
45
|
+
self.class.primary_key
|
46
|
+
end
|
47
|
+
|
39
48
|
protected
|
40
49
|
|
41
50
|
# TRP: Common interface for setting attributes to keep things consistent; if
|
@@ -61,7 +70,22 @@ class Perry::Base
|
|
61
70
|
|
62
71
|
delegate :find, :first, :all, :search, :apply_finder_options, :to => :scoped
|
63
72
|
delegate :select, :group, :order, :joins, :where, :having, :limit, :offset,
|
64
|
-
:from, :
|
73
|
+
:from, :includes, :to => :scoped
|
74
|
+
delegate :modifiers, :to => :scoped
|
75
|
+
|
76
|
+
def primary_key
|
77
|
+
@primary_key || DEFAULT_PRIMARY_KEY
|
78
|
+
end
|
79
|
+
|
80
|
+
# Allows you to specify an attribute other than :id to use as your models
|
81
|
+
# primary key.
|
82
|
+
#
|
83
|
+
def set_primary_key(attribute)
|
84
|
+
unless defined_attributes.include?(attribute.to_s)
|
85
|
+
raise Perry::PerryError.new("cannot set primary key to non-existent attribute")
|
86
|
+
end
|
87
|
+
@primary_key = attribute.to_sym
|
88
|
+
end
|
65
89
|
|
66
90
|
def new_from_data_store(hash)
|
67
91
|
if hash.nil?
|
@@ -96,8 +120,7 @@ class Perry::Base
|
|
96
120
|
protected
|
97
121
|
|
98
122
|
def fetch_records(relation)
|
99
|
-
|
100
|
-
self.read_adapter.read(options).collect { |hash| self.new_from_data_store(hash) }.compact.tap { |result| eager_load_associations(result, relation) }
|
123
|
+
self.read_adapter.call(:read, :relation => relation).compact
|
101
124
|
end
|
102
125
|
|
103
126
|
def read_with(adapter_type)
|
@@ -136,13 +159,6 @@ class Perry::Base
|
|
136
159
|
end
|
137
160
|
end
|
138
161
|
|
139
|
-
def configure_cacheable(options={})
|
140
|
-
unless cacheable
|
141
|
-
self.send(:include, Perry::Cacheable)
|
142
|
-
self.enable_caching(options)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
162
|
# TRP: Used to declare attributes -- only attributes that are declared will be available
|
147
163
|
def attributes(*attributes)
|
148
164
|
return self.defined_attributes if attributes.empty?
|
@@ -1,18 +1,18 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
class Perry::Middlewares::CacheRecords
|
2
|
+
|
3
3
|
class Entry
|
4
|
-
|
4
|
+
|
5
5
|
attr_accessor :value, :expire_at
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(value, expire_at)
|
8
8
|
self.value = value
|
9
9
|
self.expire_at = expire_at
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def expired?
|
13
13
|
Time.now > self.expire_at rescue true
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Perry::Middlewares::CacheRecords::Scopes
|
2
|
+
def self.included(model)
|
3
|
+
model.class_eval do
|
4
|
+
|
5
|
+
# Using the :fresh scope will skip the cache and execute query regardless of
|
6
|
+
# whether a cached result is available or not.
|
7
|
+
scope :fresh, (lambda do |*args|
|
8
|
+
val = args.first.nil? ? true : args.first
|
9
|
+
modifiers(:fresh => val)
|
10
|
+
end)
|
11
|
+
|
12
|
+
# Using the :reset_cache scope in a query will delete all entries from the
|
13
|
+
# cache store before running the query. Whenever possible, you should use
|
14
|
+
# the :fresh scope instead of :reset_cache.
|
15
|
+
scope :reset_cache, modifiers(:reset_cache => true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|