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.
- 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
|