poro 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +24 -0
- data/README.rdoc +215 -0
- data/Rakefile +37 -0
- data/lib/poro.rb +14 -0
- data/lib/poro/context.rb +459 -0
- data/lib/poro/context_factories.rb +1 -0
- data/lib/poro/context_factories/README.txt +8 -0
- data/lib/poro/context_factories/single_store.rb +34 -0
- data/lib/poro/context_factories/single_store/hash_factory.rb +19 -0
- data/lib/poro/context_factories/single_store/mongo_factory.rb +36 -0
- data/lib/poro/context_factory.rb +70 -0
- data/lib/poro/contexts.rb +4 -0
- data/lib/poro/contexts/hash_context.rb +177 -0
- data/lib/poro/contexts/mongo_context.rb +474 -0
- data/lib/poro/modelify.rb +100 -0
- data/lib/poro/persistify.rb +42 -0
- data/lib/poro/util.rb +2 -0
- data/lib/poro/util/inflector.rb +103 -0
- data/lib/poro/util/inflector/inflections.rb +213 -0
- data/lib/poro/util/inflector/methods.rb +153 -0
- data/lib/poro/util/module_finder.rb +66 -0
- data/spec/context_factory_spec.rb +71 -0
- data/spec/context_spec.rb +110 -0
- data/spec/hash_context_spec.rb +231 -0
- data/spec/inflector_spec.rb +32 -0
- data/spec/modelfy.rb +75 -0
- data/spec/module_finder_spec.rb +57 -0
- data/spec/mongo_context_spec.rb +28 -0
- data/spec/single_store_spec.rb +55 -0
- data/spec/spec_helper.rb +4 -0
- metadata +95 -0
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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>.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/poro.rb
ADDED
@@ -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'
|
data/lib/poro/context.rb
ADDED
@@ -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
|