perry 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
- module Perry::Adapters
4
- class AbstractAdapter
5
- include Perry::Logger
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
- attr_accessor :config
8
- attr_reader :type
9
- @@registered_adapters ||= {}
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
- def initialize(type, config)
12
- @type = type.to_sym
13
- @configuration_contexts = config.is_a?(Array) ? config : [config]
14
- end
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
- def self.create(type, config)
17
- klass = @@registered_adapters[type.to_sym]
18
- klass.new(type, config)
19
- end
130
+ # return the merged configuration object
131
+ def config
132
+ @config ||= build_configuration
133
+ end
20
134
 
21
- def extend_adapter(config)
22
- config = config.is_a?(Array) ? config : [config]
23
- self.class.create(self.type, @configuration_contexts + config)
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
- def config
27
- @config ||= build_configuration
28
- end
141
+ options[:mode] = mode.to_sym
142
+ @stack.call(options)
143
+ end
29
144
 
30
- def call(mode, options)
31
- @stack ||= self.middlewares.reverse.inject(self.method(mode)) do |below, (above_klass, above_config)|
32
- above_klass.new(below, above_config)
33
- end
145
+ # Return an array of added middlewares
146
+ def middlewares
147
+ self.config[:middlewares] || []
148
+ end
34
149
 
35
- @stack.call(options)
36
- end
150
+ # Return an array of added processors
151
+ def processors
152
+ self.config[:processors] || []
153
+ end
37
154
 
38
- def middlewares
39
- self.config[:middlewares] || []
40
- end
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
- def read(options)
43
- raise(NotImplementedError,
44
- "You must not use the abstract adapter. Implement an adapter that extends the " +
45
- "Perry::Adapters::AbstractAdapter class and overrides this method.")
46
- end
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
- def write(object)
49
- raise(NotImplementedError,
50
- "You must not use the abstract adapter. Implement an adapter that extends the " +
51
- "Perry::Adapters::AbstractAdapter class and overrides this method.")
52
- end
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
- def delete(object)
55
- raise(NotImplementedError,
56
- "You must not use the abstract adapter. Implement an adapter that extends the " +
57
- "Perry::Adapters::AbstractAdapter class and overrides this method.")
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
- def self.register_as(name)
61
- @@registered_adapters[name.to_sym] = self
62
- end
181
+ protected
63
182
 
64
- private
183
+ def stack_items
184
+ (processors + [Perry::Middlewares::ModelBridge] + middlewares).reverse
185
+ end
65
186
 
66
- # TRP: Run each configure block in order of class hierarchy / definition and merge the results.
67
- def build_configuration
68
- @configuration_contexts.inject({}) do |sum, config|
69
- if config.is_a?(Hash)
70
- sum.merge(config)
71
- else
72
- AdapterConfig.new(sum).tap { |ac| config.call(ac) }.marshal_dump
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
- class AdapterConfig < OpenStruct
198
+ class AdapterConfig < OpenStruct
78
199
 
79
- def add_middleware(klass, config={})
80
- self.middlewares ||= []
81
- self.middlewares << [klass, config]
82
- end
200
+ def add_middleware(klass, config={})
201
+ self.middlewares ||= []
202
+ self.middlewares << [klass, config]
203
+ end
83
204
 
84
- def to_hash
85
- marshal_dump
86
- end
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
- log(options, "RPC #{config[:service]}") {
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
- options.merge(config[:default_options] || {}))
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
- attr_reader :last_response
9
-
10
- def initialize(*args)
11
- super
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
- def write(object)
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(object)
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
- parse_response_code(@last_response)
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
- params = self.config[:default_options] || {}
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
- url << object.send(self.config[:primary_key]) unless object.new_record?
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
@@ -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] || :id
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(true) do |condition, key|
96
- condition && !options[key].respond_to?(:call)
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.gsub(/[^a-zA-Z]\w*/, '')
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[self.foreign_key]
158
- base_scope(object).where(self.primary_key => object[self.foreign_key])
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
- return nil unless object[self.primary_key]
196
- s = base_scope(object).where(self.foreign_key => object[self.primary_key])
197
- if polymorphic?
198
- s = s.where(
199
- polymorphic_type => Perry::Base.base_class_name(object.class) )
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
- attr_accessor :attributes, :new_record, :read_options, :write_options
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, :cacheable,
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, :fresh, :to => :scoped
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
- options = relation.to_hash
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
- module Perry::Cacheable
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
@@ -1,4 +1,4 @@
1
- module Perry::Cacheable
1
+ class Perry::Middlewares::CacheRecords
2
2
 
3
3
  class Store
4
4