poro 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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