poro 0.1.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,24 @@
1
+ Copyright (c) 2010, Jeffrey C. Reinecke
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the copyright holders nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL JEFFREY REINECKE BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,215 @@
1
+ = Overview
2
+
3
+ The name "Poro" is derived from "plain ol' Ruby object". Poro is a simple and
4
+ lightweight persistence engine. Unlike most persistence engines, which require
5
+ your persistent objects to be subclasses of a base model class, Poro aims to
6
+ extend plain ol' Ruby objects to be stored in any persist way you choose
7
+ (e.g. SQL, MongoDB, Memcache), and even mix and match different stores between
8
+ objects.
9
+
10
+ Additionally, Poro takes a hands-off philosophy by default, only minimally
11
+ disturbing an object it persists. Of course, there is a mixin available to add
12
+ model functionality to your object if you like, but how and when you do this
13
+ is up to you.
14
+
15
+ While the packages available for managing individual kinds of repositories focus
16
+ on a large breadth of functionality, Poro aims to give the simplest, lightest
17
+ weight, common interface possible for these storage methods. Given the disparity
18
+ in functionality available between different persistence stores
19
+ (e.g. SLQ, key/value, documents), additional needs of the store are accomplished
20
+ by working with the individual adapter package APIs themselves rather than
21
+ through whatever inferior homogenized API Poro may try to provide.
22
+
23
+ = Installation
24
+
25
+ Basic usage only requires the installation of the gem:
26
+ gem install poro
27
+ However to utilize any meaningful persistence data store, the underlying gems
28
+ for the desired persistence contexts are needed. The documentation of the
29
+ desired Context class' documentation should inform you of any necessary gems,
30
+ though a useful error is thrown if you are missing a needed gem, so it is
31
+ probably easier to just try.
32
+
33
+ If you wish to run the gem's unit tests, you should also install <tt>rspec</tt>.
34
+
35
+ It is also worthwhile checking rake for meaningful tasks, using:
36
+ rake -T
37
+
38
+ = Supported Data Stores
39
+
40
+ Currently, the following data stores are supported:
41
+
42
+ [MongoDB] Install the gems mongo and bson_ext, and you should be good to go!
43
+ Good automatic support embedded documents, including conversion to
44
+ objects when it can figure out how to do so.
45
+ [In-Memory Hash] This is really only for trial and testing purposes as it stores
46
+ everything in RAM and is lost when the application dies.
47
+
48
+ The following data stores are currently planned for version 1.0:
49
+
50
+ [SQL] Install the sequel gem and it should be good to go.
51
+ [Memcache] Install instructions forthcoming. Will be useful for those working
52
+ with web apps.
53
+
54
+ = Architecture
55
+
56
+ Poro revolves around Contexts. Each class that must persist gets its own
57
+ Context, and that Context manages the persistence of that object.
58
+
59
+ Contexts come in many flavors, depending on the data store that backs them. To
60
+ create the data stores, the application must have a ContextFactory instance.
61
+ There are different ContextFactories, depending on the needs of your application,
62
+ but the base ContextFactory can be customized via a block.
63
+
64
+ In general, Poro is hands-off with the objects it persists, however there is
65
+ one exception: In order for the ContextFactory to create a Context for an object,
66
+ the object must be tagged as persistent by including Poro::Persistify.
67
+
68
+ If you wish to have model-like functionality to your objects, you may also
69
+ include Poro::Modelify. This is not necessary for a Context to be used, but
70
+ the convenience and familiarity of this paradigm makes this desirable functionality.
71
+
72
+ = Getting Started
73
+
74
+ The following sample code sets up a basic context manager for the application,
75
+ using an in-memory only testing store (which is just a hash):
76
+
77
+ require 'poro'
78
+
79
+ Poro::Context.factory = Poro::ContextFactories::SingleStore.instantiate(:hash)
80
+
81
+ class Foo
82
+ include Poro::Persistify
83
+ include Poro::Modelify
84
+ end
85
+
86
+ f = Foo.new
87
+ puts "f doesn't have an ID yet: #{f.id.inspect}"
88
+ f.save
89
+ puts "f now has an ID: #{f.id.inspect}"
90
+ g = Foo.fetch(f.id)
91
+ puts "g is a fetch of f and has the same ID as f: #{g.id.inspect}"
92
+ f.remove
93
+ puts "f no longer has an ID: #{f.id.inspect}"
94
+
95
+ = Configuration
96
+
97
+ Each Context has its own configuration parameters, based on how its data store
98
+ works. There are two ways in which to manage this configuration, depending on
99
+ the needs of your application.
100
+
101
+ == Inline
102
+
103
+ Many users, thanks to some popular Ruby ORMs, are most comfortable with model
104
+ class inline configuration. Poro's philosophy is to be hands off with objects
105
+ in your code, however there is a convenience method included into your object
106
+ when you mark it for persistence that makes inline configuration of the context
107
+ easy:
108
+
109
+ class Person
110
+ include Poro::Persistify
111
+ configure_context do |context|
112
+ context.primary_key = :some_attribute
113
+ end
114
+ include Poro::Modelify # if you want model methods.
115
+ end
116
+
117
+ The above configure method is really just a shortcut to the
118
+ <tt>configure_for_class</tt> method on Context, which can be called instead.
119
+
120
+ == External
121
+
122
+ The problem with inline configuration is that it does not abstract the
123
+ persistence engine from the plain ol' ruby objects. Poro provides a solution
124
+ to this layering violation via a configuration block that is supplied during
125
+ ContextManager initialization. This block may return the fully configured
126
+ Context instance for each persistified class.
127
+
128
+ For example, the following generic code has the same result as
129
+ <code>Poro::Context.factory = Poro::ContextFactories::SingleStore.instantiate(:hash)</code>,
130
+ which uses a specialized factory:
131
+
132
+ Poro::Context.factory = Poro::ContextManager.new do |klass|
133
+ Poro::Contexts::HashContext.new(klass)
134
+ end
135
+
136
+ Of course, one normally would have a more complex block and/or utilize one of
137
+ the specialized factories, but this example shows just how simple a factory
138
+ nees to be.
139
+
140
+ Note that all Contexts are cached after creation, so the context configuration
141
+ can be mutated by other methods (such as <tt>configure_for_class</tt> on Context),
142
+ but developers are encouraged to choose one paradigm for their application and
143
+ stick with it.
144
+
145
+ = Contact
146
+
147
+ If you have any questions, comments, concerns, patches, or bugs, you can contact
148
+ me via the github repository at:
149
+
150
+ http://github.com/paploo/poro
151
+
152
+ or directly via e-mail at:
153
+
154
+ mailto:jeff@paploo.net
155
+
156
+ = Version History
157
+
158
+ [0.1.0 - 2010-Sep-18] Initial public release.
159
+ * Major base functionality is complete, though is subject
160
+ to big changes as it is used in the real world.
161
+ * Only supports MongoDB and Hash Contexts.
162
+ * No performance testing and optimization yet done.
163
+ * The documentation is rough around the edges and may contain errors.
164
+ * Spec tests are incomplete.
165
+
166
+ = TODO List
167
+
168
+ The following are the primary TODO items, roughly in priority order:
169
+
170
+ * Modelify: Break into modules for each piece of functionality.
171
+ * Specs: Add specs for Context Find methods.
172
+ * Check that private methods are private. (Should do on subclasses too.)
173
+ * Check that the two main find methods pass through to the correct underlying
174
+ methods or throw an argument when necessary.
175
+ * Specs: Add spec tests for Mongo Context.
176
+ * Mongo Context: Split into modules in separate files.
177
+ * Context: Split out modules into files.
178
+ * Contexts: Add SQL Context.
179
+
180
+ = License
181
+
182
+ The files contained in this repository are released under the commercially and
183
+ GPL compatible "New BSD License", given below:
184
+
185
+ == License Text
186
+
187
+ Copyright (c) 2010, Jeffrey C. Reinecke
188
+ All rights reserved.
189
+
190
+ Redistribution and use in source and binary forms, with or without
191
+ modification, are permitted provided that the following conditions are met:
192
+ * Redistributions of source code must retain the above copyright
193
+ notice, this list of conditions and the following disclaimer.
194
+ * Redistributions in binary form must reproduce the above copyright
195
+ notice, this list of conditions and the following disclaimer in the
196
+ documentation and/or other materials provided with the distribution.
197
+ * Neither the name of the copyright holders nor the
198
+ names of its contributors may be used to endorse or promote products
199
+ derived from this software without specific prior written permission.
200
+
201
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
202
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
203
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
204
+ DISCLAIMED. IN NO EVENT SHALL JEFFREY REINECKE BE LIABLE FOR ANY
205
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
206
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
207
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
208
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
209
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
210
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
211
+
212
+ Poro::Util::Inflector and its submodules are adapted from ActiveSupport,
213
+ and its source is redistributed under the MIT license it was originally
214
+ distributed under. Thetext of this copyright notice is supplied
215
+ in <tt>poro/util/inflector.rb</tt>.
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ require "rake/rdoctask"
3
+
4
+ # ===== RDOC BUILDING =====
5
+ # This isn't necessary if installing from a gem.
6
+
7
+ Rake::RDocTask.new do |rdoc|
8
+ rdoc.rdoc_dir = "rdoc"
9
+ rdoc.rdoc_files.add "lib/**/*.rb", "README.rdoc"
10
+ end
11
+
12
+ # ===== SPEC TESTING =====
13
+
14
+ begin
15
+ require "spec/rake/spectask"
16
+
17
+ Spec::Rake::SpecTask.new(:spec) do |spec|
18
+ spec.spec_opts = ['-c' '-f specdoc']
19
+ spec.spec_files = ['spec']
20
+ end
21
+
22
+ Spec::Rake::SpecTask.new(:spec_with_backtrace) do |spec|
23
+ spec.spec_opts = ['-c' '-f specdoc', '-b']
24
+ spec.spec_files = ['spec']
25
+ end
26
+ rescue LoadError
27
+ task :spec do
28
+ puts "You must have rspec installed to run this task."
29
+ end
30
+ end
31
+
32
+ # ===== GEM BUILDING =====
33
+
34
+ desc "Build the gem file for this package"
35
+ task :build_gem do
36
+ STDOUT.puts `gem build poro.gemspec`
37
+ end
@@ -0,0 +1,14 @@
1
+ # Require foundation.
2
+ require 'poro/util'
3
+
4
+ # Require core classes and modules.
5
+ require 'poro/context_factory'
6
+ require 'poro/context'
7
+ require 'poro/persistify'
8
+
9
+ # Require core expansions.
10
+ require 'poro/context_factories'
11
+ require 'poro/contexts'
12
+
13
+ # Require modelfication modules.
14
+ require 'poro/modelify'
@@ -0,0 +1,459 @@
1
+ module Poro
2
+ # This is the abstract superclass of all Contexts.
3
+ #
4
+ # For find methods, see FindMethods.
5
+ #
6
+ # The Context is the responsible delegate for directly interfacing with the
7
+ # persistence layer. Each program class that needs persistence must have its
8
+ # own context instance that knows how to store/retrive only instances of that
9
+ # class.
10
+ #
11
+ # All instances respond to the methods declared here, and must conform to
12
+ # the rules described with each method.
13
+ #
14
+ # One normally uses a subclass of Context, and that subclass may have extra
15
+ # methods for setting options and configuring behavior.
16
+ class Context
17
+
18
+ # Fetches the context for the given object or class from
19
+ # <tt>ContextFactory.instance</tt>.
20
+ # Returns nil if no context is found.
21
+ def self.fetch(obj)
22
+ if( obj.kind_of?(Class) )
23
+ return self.factory.fetch(obj)
24
+ else
25
+ return self.factory.fetch(obj.class)
26
+ end
27
+ end
28
+
29
+ # Returns true if the given class is configured to be represented by a
30
+ # context. This is done by including Poro::Persistify into the module.
31
+ def self.managed_class?(klass)
32
+ return self.factory.context_managed_class?(klass)
33
+ end
34
+
35
+ # A convenience method for further configuration of a context over what the
36
+ # factory does, via the passed block.
37
+ #
38
+ # This really just fetches (and creates, if necessary) the
39
+ # Context for the class, and then yields it to the block. Returns the context.
40
+ def self.configure_for_klass(klass)
41
+ context = self.fetch(klass)
42
+ yield(context) if block_given?
43
+ return context
44
+ end
45
+
46
+ # Returns the application's ContextFactory instance.
47
+ def self.factory
48
+ return ContextFactory.instance
49
+ end
50
+
51
+ # Sets the application's ContextFactory instance.
52
+ def self.factory=(context_factory)
53
+ ContextFactory.instance = context_factory
54
+ end
55
+
56
+ # Initizialize this context for the given class. Yields self if a block
57
+ # is given, so that instances can be easily configured at instantiation.
58
+ #
59
+ # Subclasses are expected to use this method (through calls to super).
60
+ def initialize(klass)
61
+ @klass = klass
62
+ self.data_store = nil unless defined?(@data_store)
63
+ self.primary_key = :id
64
+ yield(self) if block_given?
65
+ end
66
+
67
+ # The class that this context instance services.
68
+ attr_reader :klass
69
+
70
+ # The raw data store backing this context. This is useful for advanced
71
+ # usage, such as special queries. Be aware that whenever you use this,
72
+ # there is tight coupling with the underlying persistence store!
73
+ attr_reader :data_store
74
+
75
+ # Sets the raw data store backing this context. Useful during initial
76
+ # configuration and advanced usage, but can be dangerous.
77
+ attr_writer :data_store
78
+
79
+ # Returns the a symbol for the method that returns the Context assigned
80
+ # primary key for the managed object. This defaults to <tt>:id</tt>
81
+ attr_reader :primary_key
82
+
83
+ # Set the method that returns the Context assigned primary key for the
84
+ # managed object.
85
+ #
86
+ # Note that if you want the primary key's instance variable value to be
87
+ # purged from saved data, you must name the accessor the same as the instance
88
+ # method (like if using attr_reader and attr_writer).
89
+ def primary_key=(pk)
90
+ @primary_key = pk.to_sym
91
+ end
92
+
93
+ # Returns the primary key value from the given object, using the primary
94
+ # key set for this context.
95
+ def primary_key_value(obj)
96
+ return obj.send( primary_key() )
97
+ end
98
+
99
+ # Sets the primary key value on the managed object, using the primary
100
+ # key set for this context.
101
+ def set_primary_key_value(obj, id)
102
+ method = (primary_key().to_s + '=').to_sym
103
+ obj.send(method, id)
104
+ end
105
+
106
+ # Fetches the object from the store with the given id, or returns nil
107
+ # if there are none matching.
108
+ def fetch(id)
109
+ return convert_to_plain_object( clean_id(nil) )
110
+ end
111
+
112
+ # Saves the given object to the persistent store using this context.
113
+ #
114
+ # Subclasses do not need to call super, but should follow the given rules:
115
+ #
116
+ # Returns self so that calls may be daisy chained.
117
+ #
118
+ # If the object has never been saved, it should be inserted and given
119
+ # an id. If the object has been added before, the id is used to update
120
+ # the existing record.
121
+ #
122
+ # Raises an Error if save fails.
123
+ def save(obj)
124
+ obj.id = obj.object_id if obj.respond_to?(:id) && obj.id.nil? && obj.respond_to?(:id=)
125
+ return obj
126
+ end
127
+
128
+ # Remove the given object from the persisten store using this context.
129
+ #
130
+ # Subclasses do not need to call super, but should follow the given rules:
131
+ #
132
+ # Returns self so that calls may be daisy chained.
133
+ #
134
+ # If the object is successfully removed, the id is set to nil.
135
+ #
136
+ # Raises an Error is the remove fails.
137
+ def remove(obj)
138
+ obj.id = nil if obj.respond_to?(:id=)
139
+ return obj
140
+ end
141
+
142
+ # Convert the data from the data store into the correct plain ol' ruby
143
+ # object for the class this context represents.
144
+ #
145
+ # For non-embedded persistent stores, only records of the type for this
146
+ # context must be handled. However, for embedded stores--or more
147
+ # complex embedded handling on non-embedded stores--more compex
148
+ # rules may be necessary, handling all sorts of data types.
149
+ #
150
+ # The second argument is reserved for state information that the method
151
+ # may need to pass around, say if it is recursively converting elements.
152
+ # Any root object returned from a "find" in the data store needs to be
153
+ # able to be converted
154
+ def convert_to_plain_object(data, state_info={})
155
+ return data
156
+ end
157
+
158
+ # Convert a plain ol' ruby object into the data store data format this
159
+ # context represents.
160
+ #
161
+ # For non-embedded persistent stores, only records of the type for this
162
+ # context must be handled. However, for embedded stores--or more
163
+ # complex embedded handling on non-embedded stores--more compex
164
+ # rules may be necessary, handling all sorts of data types.
165
+ #
166
+ # The second argument is reserved for state information that the method
167
+ # may need to pass around, say if it is recursively converting elements.
168
+ # Any root object returned from a "find" in the data store needs to be
169
+ # able to be converted
170
+ def convert_to_data(obj, state_info={})
171
+ return obj
172
+ end
173
+
174
+ private
175
+
176
+ # Given a value that represents an ID, scrub it to produce a clean ID as
177
+ # is needed by the data store for the context.
178
+ #
179
+ # This is used by methods like <tt>fetch</tt> and <tt>find_for_ids</tt> to
180
+ # convert the IDs from whatever types the user passed, into the correct
181
+ # values.
182
+ def clean_id(id)
183
+ return id
184
+ end
185
+
186
+ end
187
+ end
188
+
189
+
190
+
191
+
192
+ module Poro
193
+ class Context
194
+ # A mixin that contains all the context find methods.
195
+ #
196
+ # The methods are split into three groups:
197
+ # [FindMethods] Contains the methods that a developer should use but that
198
+ # a Context author should never need to override.
199
+ # [FindMethods::RootMethods] Contains the methods that a developer should
200
+ # never need to use, but that a Context author
201
+ # needs to override.
202
+ # [FindMethods::HelperMethods] Some private helper methods that rarely need
203
+ # to be used or overriden.
204
+ #
205
+ # Note that <tt>fetch</tt> is considered basic functionality and not a
206
+ # find method, even though it technically finds by id.
207
+ #
208
+ # Subclasses are expected to override the methods in RootMethods.
209
+ module FindMethods
210
+
211
+ def self.included(mod) # :nodoc:
212
+ mod.send(:include, RootMethods)
213
+ mod.send(:private, *RootMethods.instance_methods)
214
+ mod.send(:include, HelperMethods)
215
+ mod.send(:private, *HelperMethods.instance_methods)
216
+ end
217
+
218
+ # Provides the delegate methods for the find routines.
219
+ #
220
+ # These methods are made private so that developers use the main find
221
+ # methods. This makes it easier to change behavior in the future due to
222
+ # the bottlenecking.
223
+ #
224
+ # Subclasses of Context should override all of these.
225
+ # See the inline subclassing documentation sections for each method for details.
226
+ module RootMethods
227
+
228
+ # Returns an array of all the records that match the following options.
229
+ #
230
+ # One ususally calls this through <tt>find</tt> via the :all or :many argument.
231
+ #
232
+ # See <tt>find</tt> for more help.
233
+ #
234
+ # === Subclassing
235
+ #
236
+ # Subclasses MUST override this method.
237
+ #
238
+ # Subclases usually convert the options into a call to <tt>data_store_find_all</tt>.
239
+ def find_all(opts)
240
+ return data_store_find_all(opts)
241
+ end
242
+
243
+ # Returns the first record that matches the following options.
244
+ # Use of <tt>fetch</tt> is more convenient if finding by ID.
245
+ #
246
+ # One usually calls this through <tt>find</tt> via the :first or :one argument.
247
+ #
248
+ # See <tt>find</tt> for more help.
249
+ #
250
+ # === Subclassing
251
+ #
252
+ # Subclasses MUST override this method!
253
+ #
254
+ # They usually take one of several tacts:
255
+ # 1. Convert tothe options and call <tt>data_store_find_first</tt>.
256
+ # 2. Set the limit to 1 and call <tt>find_all</tt>.
257
+ def find_first(opts)
258
+ hashize_limit(opts[:limit])[:limit] = 1
259
+ return find_all(opts)
260
+ end
261
+
262
+ # Calls the relevant finder method on the underlying data store, and
263
+ # converts all the results to plain objects.
264
+ #
265
+ # One usually calls thrigh through the <tt>data_store_find</tt> method
266
+ # with the :all or :many arument.
267
+ #
268
+ # Use of this method is discouraged as being non-portable, but sometimes
269
+ # there is no alternative but to get right down to the underlying data
270
+ # store.
271
+ #
272
+ # Note that if this method still isn't enough, you'll have to use the
273
+ # data store and convert the objects yourself, like so:
274
+ # SomeContext.data_store.find_method(arguments).map {{|data| SomeContext.convert_to_plain_object(data)}
275
+ #
276
+ # === Subclassing
277
+ #
278
+ # Subclasses MUST override this method.
279
+ #
280
+ # Subclasses are expected to return the results converted to plain objects using
281
+ # self.convert_to_plain_object(data)
282
+ def data_store_find_all(*args, &block)
283
+ return [].map {|data| convert_to_plain_object(data)}
284
+ end
285
+
286
+ # Calls the relevant finder method on the underlying data store, and
287
+ # converts the result to a plain object.
288
+ #
289
+ # One usually calls thrigh through the <tt>data_store_find</tt> method
290
+ # with the :first or :one arument.
291
+ #
292
+ # Use of this method is discouraged as being non-portable, but sometimes
293
+ # there is no alternative but to get right down to the underlying data
294
+ # store.
295
+ #
296
+ # Note that if this method still isn't enough, you'll have to use the
297
+ # data store and convert the object yourself, like so:
298
+ # SomeContext.convert_to_plain_object( SomeContext.data_store.find_method(arguments) )
299
+ #
300
+ #
301
+ # === Subclassing
302
+ #
303
+ # Subclasses MUST override this method.
304
+ #
305
+ # Subclasses are expected to return the result converted to a plain object using
306
+ # self.convert_to_plain_object(data)
307
+ def data_store_find_first(*args, &block)
308
+ return convert_to_plain_object(nil)
309
+ end
310
+
311
+ # Returns the records that correspond to the passed ids (or array of ids).
312
+ #
313
+ # One usually calls this by giving an array of IDs to the <tt>find</tt> method.
314
+ #
315
+ # === Subclassing
316
+ #
317
+ # Subclasses SHOULD override this method.
318
+ #
319
+ # By default, this method aggregates separate calls to find_by_id. For
320
+ # most data stores this makes N calls to the server, decreasing performance.
321
+ #
322
+ # When possible, this method should be overriden by subclasses to be more
323
+ # efficient, probably calling a <tt>find_all</tt> with the IDs, as
324
+ # filtered by the <tt>clean_id</tt> private method.
325
+ def find_with_ids(*ids)
326
+ ids = ids.flatten
327
+ return ids.map {|id| find_by_id(id)}
328
+ end
329
+
330
+ end
331
+
332
+ # Contains some private helper methods to help process finds. These
333
+ # rarely need to be used or overriden by Context subclasses.
334
+ module HelperMethods
335
+
336
+ # Cleans the find opts. This makes it so that no matter which (legal)
337
+ # style that they give their options to find, they are made into a single
338
+ # standard format that the subclasses can depend on.
339
+ def clean_find_opts(opts)
340
+ cleaned_opts = opts.dup
341
+ cleaned_opts[:limit] = hashize_limit(opts[:limit]) if opts.has_key?(:limit)
342
+ cleaned_opts[:order] = hashize_order(opts[:order]) if opts.has_key?(:order)
343
+ return cleaned_opts
344
+ end
345
+
346
+ # Takes the limit option to find and returns a uniform hash version of it.
347
+ #
348
+ # The hash has the form:
349
+ # {:limit => (integer || nil), :offset => (integer)}
350
+ #
351
+ # Note that a limit of nil means that all records shoudl be returned.
352
+ def hashize_limit(limit_opt)
353
+ if( limit_opt.kind_of?(Hash) )
354
+ return {:limit => nil, :offset => 0}.merge(limit_opt)
355
+ elsif( limit_opt.kind_of?(Array) )
356
+ return {:limit => limit_opt[0], :offset => limit_opt[1]||0}
357
+ else
358
+ return {:limit => (limit_opt&&limit_opt.to_i), :offset => 0}
359
+ end
360
+ end
361
+
362
+ # Takes the order option to find and returns a uniform hash version of it.
363
+ #
364
+ # Returns a hash of each sort key followed by either :asc or :desc. If
365
+ # there are no sort keys, this returns an empty hash.
366
+ def hashize_order(order_opt)
367
+ if( order_opt.kind_of?(Hash) )
368
+ return order_opt
369
+ elsif( order_opt.kind_of?(Array) )
370
+ return order_opt.inject({}) {|hash,(key,direction)| hash[key] = direction || :asc; hash}
371
+ elsif( order_opt.nil? )
372
+ return {}
373
+ else
374
+ return {order_opt => :asc}
375
+ end
376
+ end
377
+
378
+ end
379
+
380
+ # Fetches records according to the parameters given in opts.
381
+ #
382
+ # Contexts attempt to implement this method as uniformily as possible,
383
+ # however some features only exist in some backings and may or may not be
384
+ # portable.
385
+ #
386
+ # WARNING: For performance, some Contexts may not check that the passed
387
+ # options are syntactically correct before passing off to their data store.
388
+ # This could result in the inadvertent support of some underlying functionality
389
+ # that may go away in a refactor. Please make sure you only use this method
390
+ # in the way it is documented for maximal future compatibility.
391
+ #
392
+ # Note that if you wish to work more directly with the data store's find
393
+ # methods, one should see <ttdata_store_find_all</tt> and
394
+ # <tt>data_store_find_first</tt>.
395
+ #
396
+ # The first argument must be one of the following:
397
+ # * An ID
398
+ # * An array of IDs
399
+ # * :all or :many
400
+ # * :first or :one
401
+ #
402
+ # The options are as follows:
403
+ # [:conditions] A hash of key-value pairs that will be matched against. They
404
+ # are joined by an "and". Note that in contexts that support embedded
405
+ # contexts, the keys may be dot separated keypaths.
406
+ # [:order] The name of the key to order by in ascending order, an array of
407
+ # keys to order by in ascending order, an array of arrays, or a hash, where
408
+ # the first value is the key, and the second value is either :asc or :desc.
409
+ # [:limit] Either the limit of the number of records to get, an array of the
410
+ # limit and offset, or a hash with keys :limit and/or :offset.
411
+ #
412
+ # === Subclassing
413
+ #
414
+ # Subclasses MUST NOT override this method.
415
+ #
416
+ # This method delegates out its calls to other methods that should be
417
+ # overridden by subclasses.
418
+ def find(arg, opts={})
419
+ if(arg == :all || arg == :many)
420
+ return find_all(opts)
421
+ elsif( arg == :first || arg == :one)
422
+ return find_first(opts)
423
+ elsif( arg.respond_to?(:map) )
424
+ return find_with_ids(arg)
425
+ else
426
+ return find_by_id(arg)
427
+ end
428
+ end
429
+
430
+ # Forwards the arguments and any block to the data store's find methods,
431
+ # and returns plain ol' objects as the result.
432
+ #
433
+ # WARNING: This normally should not be used as its behavior is dependent
434
+ # upon the underlying data store, however sometimes there is no equivalent
435
+ # to the functionality offered by the data store given by the normal find
436
+ # method.
437
+ #
438
+ # The first argument must be one of:
439
+ # * :all or :many
440
+ # * :first or :one
441
+ def data_store_find(first_or_all, *args, &block)
442
+ if(first_or_all == :all || first_or_all == :many)
443
+ return data_store_find_all(*first_or_all, &block)
444
+ elsif( first_or_all == :first || first_or_all == :one)
445
+ return data_store_find_first(*first_or_all, &block)
446
+ else
447
+ raise ArgumentError, "#{__method__} expects the first argument to be one of :all, :many, :first, or :one."
448
+ end
449
+ end
450
+
451
+ end
452
+ end
453
+ end
454
+
455
+ module Poro
456
+ class Context
457
+ include FindMethods
458
+ end
459
+ end