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
@@ -0,0 +1 @@
|
|
1
|
+
require 'poro/context_factories/single_store'
|
@@ -0,0 +1,8 @@
|
|
1
|
+
This is a place holder to make sure this directory is in git until I fill it in.
|
2
|
+
|
3
|
+
It is planned to have a factory here that takes the kind of context you want to
|
4
|
+
auto-build for each class.
|
5
|
+
|
6
|
+
It is also planned to have a factory that can use a configuration file or
|
7
|
+
directory that defines configurations for each class's context external to the
|
8
|
+
rest of the application.
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Poro
|
2
|
+
module ContextFactories
|
3
|
+
# The namespace for all the factories that only allow your application to
|
4
|
+
# back a single store.
|
5
|
+
#
|
6
|
+
# If your application needs to create multiple stores, or initiate complex
|
7
|
+
# behavior, it is better to either create your own factory, or supply a
|
8
|
+
# block to the default factory.
|
9
|
+
# It is also possible to manually instantiate these factories to delegate to
|
10
|
+
# them if you wish to use their functionality in your own. It is usually
|
11
|
+
# best to cache your single instance, instead of making new ones for each use.
|
12
|
+
module SingleStore
|
13
|
+
|
14
|
+
# A shortcut method to instantiate the
|
15
|
+
# individual single-context factories in this module.
|
16
|
+
#
|
17
|
+
# This transforms the given name into the appropriate factory, and passes the
|
18
|
+
# options hash directly to the created factory.
|
19
|
+
#
|
20
|
+
# If a block is supplied, it will be passed the class that a Context was
|
21
|
+
# generated for, and the Context itself.
|
22
|
+
def self.instantiate(name, opts={}, &block)
|
23
|
+
underscored_name = Util::Inflector.underscore(name.to_s)
|
24
|
+
klass_name = Util::Inflector.camelize( underscored_name.gsub(/_(context|factory)$/, '') + '_factory' )
|
25
|
+
klass = Util::ModuleFinder.find(klass_name, self, true)
|
26
|
+
return klass.new(opts, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'poro/context_factories/single_store/hash_factory'
|
34
|
+
require 'poro/context_factories/single_store/mongo_factory'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Poro
|
2
|
+
module ContextFactories
|
3
|
+
module SingleStore
|
4
|
+
# Creates a factory that generates a HashContext for each class.
|
5
|
+
class HashFactory < ContextFactory
|
6
|
+
|
7
|
+
# Initializes a new HashContext for each class.
|
8
|
+
def initialize(opts={})
|
9
|
+
super() do |klass|
|
10
|
+
context = Contexts::HashContext.new(klass)
|
11
|
+
yield(klass, context) if block_given?
|
12
|
+
context
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Poro
|
2
|
+
module ContextFactories
|
3
|
+
module SingleStore
|
4
|
+
# Creates a factory that generates MongoContext instances to the supplied
|
5
|
+
# database, automatically setting the data_store to point at the collection
|
6
|
+
# with the underscored, pluralized name of the class backing it.
|
7
|
+
#
|
8
|
+
# One can further configure the Context in several ways:
|
9
|
+
# 1. Supply a block to new.
|
10
|
+
# 2. Configure in the model.
|
11
|
+
# 3. Fetch the Context directly, though this is considered bad form.
|
12
|
+
# Wichever method you choose, it is wise to be consistent throughout the
|
13
|
+
# application.
|
14
|
+
#
|
15
|
+
# This factory does not allow complex behaviors such as database switching.
|
16
|
+
class MongoFactory < ContextFactory
|
17
|
+
|
18
|
+
# Instantiates a new MongoContext. The argument hash must include the
|
19
|
+
# <tt>:connection</tt> key, and it must be a Mongo::Connection instance.
|
20
|
+
def initialize(opts={})
|
21
|
+
@connection = opts[:connection] || opts['connection']
|
22
|
+
raise ArgumentError, "No mongo connection was supplied to #{self.class.name}." if @connection.nil?
|
23
|
+
|
24
|
+
super() do |klass|
|
25
|
+
collection_name = Util::Inflector.pluralize(Util::Inflector.underscore(klass.name.to_s))
|
26
|
+
context = Contexts::MongoContext.new(klass)
|
27
|
+
context.data_store = @connection[collection_name]
|
28
|
+
yield(klass, context) if block_given?
|
29
|
+
context
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Poro
|
2
|
+
# This class serves as both the base class for all context factories, and the
|
3
|
+
# root class for retriving the application's context factory.
|
4
|
+
class ContextFactory
|
5
|
+
|
6
|
+
# The base error type for a factory specific error.
|
7
|
+
class FactoryError < RuntimeError; end
|
8
|
+
|
9
|
+
# Returns the context factory instance for the application.
|
10
|
+
# Returns nil if none is set.
|
11
|
+
#
|
12
|
+
# One normally gets this via Context.factory, but it doesn't make a difference.
|
13
|
+
def self.instance
|
14
|
+
raise RuntimeError, "No context factory configured for this application." if @instance.nil?
|
15
|
+
return @instance
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sets the context factory instance for the application.
|
19
|
+
#
|
20
|
+
# One normally sets this via Context.factory, but it doesn't make a difference.
|
21
|
+
def self.instance=(instance)
|
22
|
+
raise TypeError, "Cannot set an object of class #{instance.class} as the application's context factory." unless instance.kind_of?(self) || instance.nil?
|
23
|
+
@instance = instance
|
24
|
+
end
|
25
|
+
|
26
|
+
# Takes a factory block that delivers a configured context for the class
|
27
|
+
# passed to it.
|
28
|
+
def initialize(&context_factory_block)
|
29
|
+
@context_factory_block = context_factory_block
|
30
|
+
@context_cache = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def context_managed_class?(klass)
|
34
|
+
return klass.include?(Poro::Persistify)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Fetches the context for a given class, or returns nil if the given object
|
38
|
+
# should not have a context.
|
39
|
+
#
|
40
|
+
# This is the most basic implementation possible, though, like any context
|
41
|
+
# factory must do, it guarantees that the same Context instance will be
|
42
|
+
# returned for the same class throughout the lifetime of the application so
|
43
|
+
# that configuration subsequent to generation is honored.
|
44
|
+
#
|
45
|
+
# Subclasses are expected to call this method instead of running the factory
|
46
|
+
# block directly.
|
47
|
+
def fetch(klass)
|
48
|
+
raise FactoryError, "Cannot create a context for class #{klass.inspect}, as it has not been flagged for persistence. Include Context::Persistify to fix." unless self.context_managed_class?(klass)
|
49
|
+
if( !@context_cache.has_key?(klass) )
|
50
|
+
@context_cache[klass] = build(klass)
|
51
|
+
end
|
52
|
+
return @context_cache[klass]
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Calls the context factory block to generate a new context. This should
|
58
|
+
# not be called directly, but instead left to the fetch method to call when
|
59
|
+
# needed so that it is only called once per class during the application's
|
60
|
+
# lifetime.
|
61
|
+
def build(klass)
|
62
|
+
begin
|
63
|
+
return @context_factory_block.call(klass)
|
64
|
+
rescue Exception => e
|
65
|
+
raise RuntimeError, "Error encountered during context fetch build: #{e.class}: #{e.message.inspect}", e.backtrace
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Poro
|
2
|
+
module Contexts
|
3
|
+
# Not a practical real world context manager, this is a simple in-memory
|
4
|
+
# store that uses a normal Ruby Hash. The intended use for this context is
|
5
|
+
# for building and testing code before a more realistic persistence backing
|
6
|
+
# is available for your application.
|
7
|
+
class HashContext < Context
|
8
|
+
|
9
|
+
def initialize(klass)
|
10
|
+
self.data_store = {}
|
11
|
+
super(klass)
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch(id)
|
15
|
+
return convert_to_plain_object( data_store[clean_id(id)] )
|
16
|
+
end
|
17
|
+
|
18
|
+
# Save the object in the underlying hash, using the object id as the key.
|
19
|
+
def save(obj)
|
20
|
+
pk_id = self.primary_key_value(obj)
|
21
|
+
self.set_primary_key_value(obj, obj.object_id) if(pk_id.nil?)
|
22
|
+
|
23
|
+
data_store[obj.id] = convert_to_data(obj)
|
24
|
+
return self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Remove the object from the underlying hash.
|
28
|
+
def remove(obj)
|
29
|
+
pk_id = self.primary_key_value(obj)
|
30
|
+
if( pk_id != nil )
|
31
|
+
data_store.delete(obj.id)
|
32
|
+
self.set_primary_key_value(obj, nil)
|
33
|
+
end
|
34
|
+
return self
|
35
|
+
end
|
36
|
+
|
37
|
+
def convert_to_plain_object(data)
|
38
|
+
return data
|
39
|
+
end
|
40
|
+
|
41
|
+
def convert_to_data(obj)
|
42
|
+
return obj
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def clean_id(id)
|
48
|
+
return id && id.to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
# Searching a hash is incredibly slow because the following steps must
|
52
|
+
# be taken:
|
53
|
+
# 1. If there is an order, we first have to sort ALL values by the order.
|
54
|
+
# 2. Then we must find all matching records.
|
55
|
+
# 3. Then we must apply limit and offset to fetch the correct record.
|
56
|
+
#
|
57
|
+
# There are several optimizations that can be made in the future:
|
58
|
+
# 1. When matching the last key in the list, we can stop processing when
|
59
|
+
# we reach the limit+offset number of records.
|
60
|
+
# 2. If the offset is higher than the total number of stored records, then
|
61
|
+
# we know there will be no matches.
|
62
|
+
def find_all(opts)
|
63
|
+
opts = clean_find_opts(opts)
|
64
|
+
data = limit( filter( sort( data_store.dup, opts[:order] ), opts[:conditions] ), opts[:limit])
|
65
|
+
return data.map {|data| convert_to_plain_object(data)}
|
66
|
+
end
|
67
|
+
|
68
|
+
# This is a highly inefficient implementation of the finder, as it finds
|
69
|
+
# all records and selects the first matching one.
|
70
|
+
def find_first(opts)
|
71
|
+
opts[:limit] = 1
|
72
|
+
return find_all(opts)[0]
|
73
|
+
end
|
74
|
+
|
75
|
+
# The data store has no built in finding mechanism, so this always
|
76
|
+
# returns an empty array.
|
77
|
+
def data_store_find_all(*args, &block)
|
78
|
+
return []
|
79
|
+
end
|
80
|
+
|
81
|
+
# The data store has no built in finding mechanism, so this always
|
82
|
+
# returns nil.
|
83
|
+
def data_store_find_one(*args, &block)
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sorting works by taking the found value for two records and comparing them
|
88
|
+
# with (a <=> b).to_i. If the direction is :desc, this is multiplied by
|
89
|
+
# -1.
|
90
|
+
def sort(data, sort_opt)
|
91
|
+
# If there are no sort options, don't sort.
|
92
|
+
return data if sort_opt.nil? || sort_opt.empty?
|
93
|
+
|
94
|
+
# Sort a copy of the data hash by building the comparison between elements.
|
95
|
+
return data.dup.sort do |a,b|
|
96
|
+
precedence = 0
|
97
|
+
sort_opt.each do |key, direction|
|
98
|
+
break if precedence != 0 # On the first non-zero precedence, we know who to put first!
|
99
|
+
multiplier = direction.to_s=='desc' ? -1 : 1
|
100
|
+
value_a = value_for_key(a, key)[:value]
|
101
|
+
value_b = value_for_key(b, key)[:value]
|
102
|
+
if( value_a!=nil && value_b!=nil )
|
103
|
+
precedence = multiplier * (value_a <=> value_b).to_i
|
104
|
+
elsif( value_a.nil? && value_b.nil? )
|
105
|
+
precedence = 0
|
106
|
+
elsif( value_a.nil? && value_b!=nil )
|
107
|
+
precedence = multiplier * -1 # TODO: Which way does SQL or MongoDB sort nil?
|
108
|
+
elsif( value_a!=nil && value_b.nil? )
|
109
|
+
precedence = multiplier * 1 # TODO: Which way does SQL or MongoDB sort nil?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
precedence # Sort block return
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Filters out records that, for each of the conditions in the hash,
|
117
|
+
# have a value at the keypath and the value at that keypath matches the
|
118
|
+
# desired value.
|
119
|
+
def filter(data, conditions_opt)
|
120
|
+
conditions_opt.inject(data) do |matches,(key, value)|
|
121
|
+
keypath = key.to_s.split('.')
|
122
|
+
matches.select do |record|
|
123
|
+
value_info = value_for_key(record, keypath)
|
124
|
+
value_info[:found] && value_info[:value] == value
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def limit(data, limit_opt)
|
130
|
+
if( !limit_opt.nil? && limit_opt[:limit] )
|
131
|
+
return data[limit_opt[:offset].to_i, limit_opt[:limit].to_i] || []
|
132
|
+
elsif( !limit_opt.nil? )
|
133
|
+
return data[limit_opt[:offset].to_i .. -1] || []
|
134
|
+
else
|
135
|
+
return data
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns a hash with the following keys:
|
140
|
+
# [:found] Returns true if the given keypath resolves to a value.
|
141
|
+
# [:value] The value found at the keypath. This will be nil if none was
|
142
|
+
# found, but nil could be the real stored value as well!
|
143
|
+
def value_for_key(record, keypath)
|
144
|
+
# This is a recursive method, so while record looks good for an entry point
|
145
|
+
# variable, obj is better when traversing.
|
146
|
+
obj = record
|
147
|
+
|
148
|
+
# Split the keypath if it is not an array already
|
149
|
+
keypath = keypath.to_s.split('.') unless keypath.kind_of?(Array)
|
150
|
+
|
151
|
+
# If we are at the end of hte keypath and the given object matches the
|
152
|
+
# expected value, we have a match, otherwise we don't.
|
153
|
+
#return {:matches => obj==match_value, :value => obj} if( keypath.empty? )
|
154
|
+
return {:found => true, :value => obj} if (keypath.empty?)
|
155
|
+
|
156
|
+
# If we aren't at the end of hte keypath, we get to descend one more level,
|
157
|
+
# remembering to return false if we can't descend for some reason.
|
158
|
+
key, *remaining_keys = keypath
|
159
|
+
if( obj.kind_of?(Array) )
|
160
|
+
return {:found => false, :value => nil} if key.to_i < 0 || key.to_i >= obj.length
|
161
|
+
new_obj = obj[key.to_i]
|
162
|
+
elsif( obj.kind_of?(Hash) )
|
163
|
+
return {:found => false, :value => nil} unless obj.has_key?(key.to_s) || obj.has_key?(key.to_sym) || obj.has_key?(key.to_i)
|
164
|
+
new_obj = obj[key.to_s] || obj[key.to_sym] || obj[key.to_i]
|
165
|
+
else
|
166
|
+
return {:found => false, :value => nil} unless key =~ /[_a-zA-z]([_a-zA-z0-9]*)/
|
167
|
+
ivar = ('@'+key.to_s).to_sym
|
168
|
+
return {:found => false, :value => nil} unless obj.instance_variable_defined?(ivar)
|
169
|
+
new_obj = obj.instance_variable_get(ivar)
|
170
|
+
end
|
171
|
+
return send(__method__, new_obj, remaining_keys)
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
@@ -0,0 +1,474 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Poro
|
4
|
+
module Contexts
|
5
|
+
# The MongoDB Context Adapter.
|
6
|
+
#
|
7
|
+
# Manages an object in MongoDB.
|
8
|
+
#
|
9
|
+
# WARNING: At this time, only objects that follow nice tree hierarchies can
|
10
|
+
# be encoded. Cyclical loops cannot be auto-encoded, and need embedded
|
11
|
+
# objects to be managed with the parent pointers blacklisted.
|
12
|
+
#
|
13
|
+
# WARNING: Embedded objects of the same kind--which are referenced via a
|
14
|
+
# DBRef, are re-fetched and re-saved every time the managing object is
|
15
|
+
# fetched or saved.
|
16
|
+
#
|
17
|
+
# This adapter recursively encodes the object according to the following
|
18
|
+
# rules for each instance variable's value:
|
19
|
+
# 1. If the object can be saved as a primitive, save it that way.
|
20
|
+
# 2. If the object is managed by a Mongo context, save and encode it as a DBRef.
|
21
|
+
# 3. If the object is managed by another context, save and store the class and id in a hash.
|
22
|
+
# 4. Otherwise, encode all instance variables and the class, in a Hash.
|
23
|
+
#
|
24
|
+
# For Mongo represented objects, the instance variables that are encoded
|
25
|
+
# can be controlled via any combination of the <tt>save_attributes</tt> and
|
26
|
+
# <tt>save_attributes_blacklist</tt> properties. The Context will start
|
27
|
+
# with the save attributes (which defaults to all the instance variables),
|
28
|
+
# and then subtract out the attributes in the blacklist. Thus the blacklist
|
29
|
+
# takes priority.
|
30
|
+
class MongoContext < Context
|
31
|
+
# A map of all the collection names registered for this kind of context.
|
32
|
+
# This is to facilitate DBRef dereferencing, even when your class doesn't
|
33
|
+
# match the
|
34
|
+
@@collection_map = {}
|
35
|
+
|
36
|
+
# Takes the class for the context, and optionally the collection object
|
37
|
+
# up front. This can be changed at any time by setting the data store for
|
38
|
+
# the Context.
|
39
|
+
def initialize(klass)
|
40
|
+
# Require mongo. We do it here so that it is only required when
|
41
|
+
# we use this. (How does this affect speed? It seems this takes 1/30000 of a second.)
|
42
|
+
require 'mongo'
|
43
|
+
|
44
|
+
# Set-up the lists.
|
45
|
+
@persistent_attributes_whitelist = nil
|
46
|
+
@persistent_attributes_blacklist = nil
|
47
|
+
|
48
|
+
# Initialize
|
49
|
+
super(klass)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set the data store to the given collection.
|
53
|
+
def data_store=(collection)
|
54
|
+
@@collection_map.delete(self.data_store && data.store.name) # Clean-up the old record in case we change names.
|
55
|
+
@@collection_map[collection.name] = self unless collection.nil? # Create the new record.
|
56
|
+
super(collection)
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :persistent_attributes_whitelist
|
60
|
+
attr_writer :persistent_attributes_whitelist
|
61
|
+
|
62
|
+
attr_reader :persistent_attributes_blacklist
|
63
|
+
attr_writer :persistent_attributes_blacklist
|
64
|
+
|
65
|
+
def fetch(id)
|
66
|
+
data = data_store.find_one( clean_id(id) )
|
67
|
+
return convert_to_plain_object(data)
|
68
|
+
end
|
69
|
+
|
70
|
+
def save(obj)
|
71
|
+
data = convert_to_data(obj)
|
72
|
+
data_store.save(data)
|
73
|
+
set_primary_key_value(obj, (data['_id'] || data[:_id])) # The pk generator uses a symbol, while everything else uses a string!
|
74
|
+
return obj
|
75
|
+
end
|
76
|
+
|
77
|
+
def remove(obj)
|
78
|
+
return obj
|
79
|
+
end
|
80
|
+
|
81
|
+
def convert_to_plain_object(data, state_info={})
|
82
|
+
# If it is a root record, and it has no class name, assume this context's class name.
|
83
|
+
data['_class_name'] = self.klass if( data && data.kind_of?(Hash) && !state_info[:embedded] )
|
84
|
+
obj = route_decode(data, state_info)
|
85
|
+
return obj
|
86
|
+
end
|
87
|
+
|
88
|
+
def convert_to_data(obj, state_info={})
|
89
|
+
data = route_encode(obj, state_info)
|
90
|
+
return data
|
91
|
+
end
|
92
|
+
|
93
|
+
# =============================== PRIVATE ===============================
|
94
|
+
private
|
95
|
+
|
96
|
+
def clean_id(id)
|
97
|
+
#TODO: Make the attempted transform configurable; mongo doesn't require an ObjectId as the key.
|
98
|
+
# Attempt to convert to an ObjectID if it looks like it should be.
|
99
|
+
if( !(id.kind_of?(BSON::ObjectId)) && BSON::ObjectId.legal?(id.to_s) )
|
100
|
+
id = BSON::ObjectId.from_string(id.to_s)
|
101
|
+
end
|
102
|
+
return id
|
103
|
+
end
|
104
|
+
|
105
|
+
# The computed list of instance variables to save, taking into account
|
106
|
+
# white lists, black lists, and primary keys.
|
107
|
+
def instance_variables_to_save(obj)
|
108
|
+
white_list = if( self.persistent_attributes_whitelist.nil? )
|
109
|
+
obj.instance_variables.map {|ivar| ivar.to_s[1..-1].to_sym}
|
110
|
+
else
|
111
|
+
self.persistent_attributes_whitelist
|
112
|
+
end
|
113
|
+
black_list = self.persistent_attributes_blacklist || []
|
114
|
+
# Note that this is significantly faster with arrays than sets.
|
115
|
+
# TODO: Only remove the primary key if it is not in the white list!
|
116
|
+
return white_list - black_list - [self.primary_key]
|
117
|
+
end
|
118
|
+
|
119
|
+
# If the object is a MongoDB compatible primitive, return true.
|
120
|
+
def mongo_primitive?(obj)
|
121
|
+
return(
|
122
|
+
obj.kind_of?(Integer) ||
|
123
|
+
obj.kind_of?(Float) ||
|
124
|
+
obj.kind_of?(String) ||
|
125
|
+
obj.kind_of?(Time) ||
|
126
|
+
obj==true ||
|
127
|
+
obj==false ||
|
128
|
+
obj.nil? ||
|
129
|
+
obj.kind_of?(BSON::ObjectId) ||
|
130
|
+
obj.kind_of?(BSON::DBRef)
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Turns an object into a hash, using the given list of instance variables.
|
135
|
+
def hashify_object(obj, ivars)
|
136
|
+
data = ivars.inject({}) do |hash, ivar_name|
|
137
|
+
ivar_sym = ('@' + ivar_name.to_s).to_sym
|
138
|
+
value = obj.instance_variable_get(ivar_sym)
|
139
|
+
hash[ivar_name.to_s] = self.convert_to_data(value, :embedded => true)
|
140
|
+
hash
|
141
|
+
end
|
142
|
+
data['_class_name'] = obj.class.name
|
143
|
+
return data
|
144
|
+
end
|
145
|
+
|
146
|
+
# Creates an object of a given class or class name, using the given hash of
|
147
|
+
# of attributes and encoded values.
|
148
|
+
def instantiate_object(klass_or_name, attributes)
|
149
|
+
# Translate class name.
|
150
|
+
klass = Util::ModuleFinder.find(klass_or_name)
|
151
|
+
|
152
|
+
# Allocate the instance (use allocate and not new because we have all the state variables saved).
|
153
|
+
obj = klass.allocate
|
154
|
+
|
155
|
+
# Iterate over attributes injecting.
|
156
|
+
attributes.each do |name, encoded_value|
|
157
|
+
next if name.to_s == '_class_name' || name.to_s == '_id'
|
158
|
+
ivar_sym = ('@' + name.to_s).to_sym
|
159
|
+
value = self.convert_to_plain_object(encoded_value, :embedded => true)
|
160
|
+
obj.instance_variable_set(ivar_sym, value)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Return the result.
|
164
|
+
return obj
|
165
|
+
end
|
166
|
+
|
167
|
+
# =============================== ENCODING ===============================
|
168
|
+
|
169
|
+
# Routes the encoding of an object to the appropriate method.
|
170
|
+
def route_encode(obj, state_info={})
|
171
|
+
if( obj.kind_of?(klass) && !state_info[:embedded] )
|
172
|
+
return encode_self_managed_object(obj)
|
173
|
+
elsif( obj.kind_of?(Hash) && obj.has_key?('_namespace') )
|
174
|
+
return encode_db_ref
|
175
|
+
elsif( obj.kind_of?(Hash) )
|
176
|
+
return encode_hash(obj)
|
177
|
+
elsif( obj.kind_of?(Array) )
|
178
|
+
return encode_array(obj)
|
179
|
+
elsif( obj.kind_of?(Class) )
|
180
|
+
return encode_class(obj)
|
181
|
+
elsif( Context.managed_class?(obj.class) && Context.fetch(obj.class).kind_of?(self.class) )
|
182
|
+
return encode_mongo_managed_object(obj)
|
183
|
+
elsif( Context.managed_class?(obj.class))
|
184
|
+
return encode_foreign_managed_object(obj)
|
185
|
+
elsif( mongo_primitive?(obj) )
|
186
|
+
return obj
|
187
|
+
else
|
188
|
+
return encode_unmanaged_object(obj)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Recursively encode a hash's contents.
|
193
|
+
def encode_hash(hash)
|
194
|
+
return hash.inject({}) do |hash,(k,v)|
|
195
|
+
hash[k] = self.convert_to_data(value, :embedded => true)
|
196
|
+
hash
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Recursively encode an array's contents.
|
201
|
+
def encode_array(array)
|
202
|
+
return array.map {|o| self.convert_to_data(o, :embedded => true)}
|
203
|
+
end
|
204
|
+
|
205
|
+
# Encode a class.
|
206
|
+
def encode_class(klass)
|
207
|
+
return {'_class_name' => klass.class, 'name' => klass.name}
|
208
|
+
end
|
209
|
+
|
210
|
+
# Encode a hash that came from a DBRef dereferenced and decoded by this context.
|
211
|
+
#
|
212
|
+
# This will save the hash when its owning object is saved!
|
213
|
+
def encode_db_ref(hash)
|
214
|
+
namespace = hash['_namespace'].to_s
|
215
|
+
id = hash['_id']
|
216
|
+
mongo_db = self.data_store.db
|
217
|
+
if( mongo_db.collection_names.include?(namespace) )
|
218
|
+
h = hash.dup # We want to be non-destructive here!
|
219
|
+
h.delete['_namespace']
|
220
|
+
mongo_db[namespace].save(h)
|
221
|
+
end
|
222
|
+
return BSON::DBRef.new(namespace, id)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Encode an object not managed by a context.
|
226
|
+
def encode_unmanaged_object(obj)
|
227
|
+
ivars = obj.instance_variables.map {|ivar| ivar.to_s[1..-1].to_sym}
|
228
|
+
return hashify_object(obj, ivars)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Encode an object managed by this context.
|
232
|
+
def encode_self_managed_object(obj)
|
233
|
+
data = hashify_object(obj, instance_variables_to_save(obj))
|
234
|
+
data['_id'] = primary_key_value(obj) unless primary_key_value(obj).nil?
|
235
|
+
data_store.pk_factory.create_pk(data) # Use the underlying adapter's paradigm for lazily creating the pk.
|
236
|
+
data['_class_name'] = obj.class.name
|
237
|
+
return data
|
238
|
+
end
|
239
|
+
|
240
|
+
# Encode an object managed by this kind of context. It encodes as a
|
241
|
+
# DBRef if it is in the same database, and as a foreign managed object
|
242
|
+
# if not.
|
243
|
+
def encode_mongo_managed_object(obj)
|
244
|
+
# If in the same data store, we do a DBRef. This is the usual case.
|
245
|
+
# But we do need to save it if it is stored in a different database!
|
246
|
+
obj_context = Context.fetch(obj)
|
247
|
+
if( obj_context.data_store.db == self.data_store.db )
|
248
|
+
obj_context.save(obj)
|
249
|
+
obj_id = obj_context.primary_key_value(obj)
|
250
|
+
obj_collection_name = obj_context.data_store.name
|
251
|
+
return BSON::DBRef.new(obj_collection_name, obj_id)
|
252
|
+
else
|
253
|
+
# Treat as if in a foreign database
|
254
|
+
return encode_foreign_managed_object(obj)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Encode an object managed by a completely different kind of context.
|
259
|
+
def encode_foreign_managed_object(obj)
|
260
|
+
obj_context = Context.fetch(obj)
|
261
|
+
obj_context.save(obj)
|
262
|
+
obj_id = obj_context.primary_key_value(obj)
|
263
|
+
return {'id' => obj_id, '_class_name' => obj.class.name, 'managed' => true}
|
264
|
+
end
|
265
|
+
|
266
|
+
# =============================== DECODING ===============================
|
267
|
+
|
268
|
+
# Route the decoding of data from mongo.
|
269
|
+
def route_decode(data, state_info={})
|
270
|
+
if( data && data.kind_of?(Hash) && data['_class_name'] )
|
271
|
+
return route_decode_stored_object(data, state_info)
|
272
|
+
else
|
273
|
+
return route_decode_stored_data(data, state_info)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# If the data doesn't directly encode an object, then this method knows
|
278
|
+
# how to route the decoding.
|
279
|
+
def route_decode_stored_data(data, state_info={})
|
280
|
+
if( data.kind_of?(Hash) )
|
281
|
+
return decode_hash(data)
|
282
|
+
elsif( data.kind_of?(Array) )
|
283
|
+
return decode_array(data)
|
284
|
+
elsif( data.kind_of?(BSON::DBRef) ) # This is a literal that we want to intercept.
|
285
|
+
return decode_db_ref(data)
|
286
|
+
else # mongo_primitive?(data) # Explicit check not necessary.
|
287
|
+
return data
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# If the data directly encodes an object, then this methods knows
|
292
|
+
# how to route decoding.
|
293
|
+
def route_decode_stored_object(data, state_info={})
|
294
|
+
class_name = data['_class_name'].to_s
|
295
|
+
if( class_name == 'Class' )
|
296
|
+
return decode_class(data)
|
297
|
+
elsif( class_name == self.klass.to_s )
|
298
|
+
return decode_self_managed_object(data)
|
299
|
+
elsif( class_name && data['managed'] )
|
300
|
+
return decode_foreign_managed_object(data)
|
301
|
+
else
|
302
|
+
return decode_unmanaged_object(data)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Decode a hash, recursing through its elements.
|
307
|
+
def decode_hash(hash)
|
308
|
+
return hash.inject({}) do |hash,(k,v)|
|
309
|
+
hash[k] = self.convert_to_plain_object(v, :embedded => true)
|
310
|
+
hash
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Decode an array, recursing through its elements.
|
315
|
+
def decode_array(array)
|
316
|
+
array.map {|o| self.convert_to_plain_object(o, :embedded => true)}
|
317
|
+
end
|
318
|
+
|
319
|
+
# Decode a class reference.
|
320
|
+
def decode_class(class_data)
|
321
|
+
return Util::ModuleFinder.find(class_data['name'])
|
322
|
+
end
|
323
|
+
|
324
|
+
# Decode a BSON::DBRef. If there is a context for the reference, it is
|
325
|
+
# wrapped in that object type. If there is no context, it is left as
|
326
|
+
# a DBRef so that it will re-save as a DBRef (otherwise it'll save as a
|
327
|
+
# hash of that document!)
|
328
|
+
#
|
329
|
+
# Note that one would think we'd be able to recognize any hash with '_id'
|
330
|
+
# as an embedded document that needs a DBRef, and indeed we can. But we
|
331
|
+
# won't know where to re-save it because we won't know the collection anymore,
|
332
|
+
# so we add '_namespace' to the record and strip it out on a save.
|
333
|
+
def decode_db_ref(dbref)
|
334
|
+
context = @@collection_map[dbref.namespace.to_s]
|
335
|
+
if( context )
|
336
|
+
value = context.data_store.db.dereference(dbref)
|
337
|
+
return context.convert_to_plain_object(value, :embedded => false) # We want it to work like a standalone object, so don't treat as embedded.
|
338
|
+
elsif self.data_store.db.collection_names.include?(dbref.namespace.to_s)
|
339
|
+
value = context.data_store.db.dereference(dbref)
|
340
|
+
value['_namespace'] = dbref.namespace.to_s
|
341
|
+
return value
|
342
|
+
else
|
343
|
+
return dbref
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Decode a self managed object
|
348
|
+
def decode_self_managed_object(data)
|
349
|
+
# Get the class and id. Note these are auto-stripped by instantiate_object.
|
350
|
+
class_name = data['_class_name']
|
351
|
+
id = data['_id']
|
352
|
+
# Instantiate.
|
353
|
+
obj = instantiate_object(class_name, data)
|
354
|
+
# Set the pk
|
355
|
+
self.set_primary_key_value(obj, id)
|
356
|
+
# Return
|
357
|
+
return obj
|
358
|
+
end
|
359
|
+
|
360
|
+
# Decode a foreign managed object. This is a matter of finding its
|
361
|
+
# Context and asking it to fetch it.
|
362
|
+
def decode_foreign_managed_object(data)
|
363
|
+
klass = Util::ModuleFinder.find(data['_class_name'])
|
364
|
+
context = Context.fetch(klass)
|
365
|
+
if( context )
|
366
|
+
context.find(data['id'])
|
367
|
+
else
|
368
|
+
return data
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Decode the given unmanaged object. If the class cannot be found, then just give
|
373
|
+
# back the underlying hash.
|
374
|
+
def decode_unmanaged_object(data)
|
375
|
+
begin
|
376
|
+
klass = Util::ModuleFinder.find(data['_class_name']) # The class name is autostripped in instantiate_object
|
377
|
+
return instantiate_object(klass, data)
|
378
|
+
rescue NameError
|
379
|
+
return data
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
|
388
|
+
|
389
|
+
module Poro
|
390
|
+
module Contexts
|
391
|
+
class MongoContext
|
392
|
+
# A mixin of MongoDB finder method implementations.
|
393
|
+
module FinderMethods
|
394
|
+
|
395
|
+
# Runs the given find parameters on MongoDB and returns a Mongo::Cursor
|
396
|
+
# object. Note that you must manually convert the results using
|
397
|
+
# this Context's <tt>convert_to_plain_object(obj)</tt> method or you will
|
398
|
+
# get raw Mongo objects.
|
399
|
+
#
|
400
|
+
# If a block is given, the cursor is automatically iterated over via
|
401
|
+
# the each method, but with the results pre-converterd. Note that
|
402
|
+
# the result set can change out from under you on an active system if
|
403
|
+
# you iterate in this way. Additionally, the returned cursor has been
|
404
|
+
# rewound, which means it may find different results!
|
405
|
+
#
|
406
|
+
# This method is useful if you need to do something special, like only
|
407
|
+
# get one result at a time to save on memory.
|
408
|
+
#
|
409
|
+
# WARNING: Even though the method currently does no filtering of the
|
410
|
+
# conditions, allowing advanced queries will work, in the future this
|
411
|
+
# may not be the case. If your query needs to do more than a simple
|
412
|
+
# query, it is better to use <tt>data_store_find_all</tt>.
|
413
|
+
def data_store_cursor(opts) # :yields: plain_object
|
414
|
+
find_opts = mongoize_find_opts(opts)
|
415
|
+
cursor = data_store.find(opts[:conditions], find_opts)
|
416
|
+
|
417
|
+
if( block_given? )
|
418
|
+
cursor.each do |doc|
|
419
|
+
plain_object = self.convert_to_plain_object(doc)
|
420
|
+
yield(plain_object)
|
421
|
+
end
|
422
|
+
cursor.rewind!
|
423
|
+
end
|
424
|
+
|
425
|
+
return cursor
|
426
|
+
end
|
427
|
+
|
428
|
+
private
|
429
|
+
|
430
|
+
def find_all(opts)
|
431
|
+
find_opts = mongoize_find_opts(opts)
|
432
|
+
return data_store_find_all(opts[:conditions], find_opts)
|
433
|
+
end
|
434
|
+
|
435
|
+
def find_first(opts)
|
436
|
+
find_opts = mongoize_find_opts(opts)
|
437
|
+
return data_store_find_first(opts[:conditions], find_opts)
|
438
|
+
end
|
439
|
+
|
440
|
+
def data_store_find_all(*args, &block)
|
441
|
+
return data_store.find(*args, &block).to_a.map {|data| self.convert_to_plain_object(doc)}
|
442
|
+
end
|
443
|
+
|
444
|
+
def data_store_find_first(*args, &block)
|
445
|
+
return self.convert_to_plain_object( data_store.find_one(*args, &block) )
|
446
|
+
end
|
447
|
+
|
448
|
+
# Takes find opts, runs them through <tt>clean_find_opts</tt>, and then
|
449
|
+
# converts them to Mongo's find opts.
|
450
|
+
def mongoize_find_opts(opts)
|
451
|
+
opts = clean_find_opts(opts)
|
452
|
+
|
453
|
+
find_opts = {}
|
454
|
+
|
455
|
+
find_opts[:limit] = opts[:limit][:limit] if opts[:limit] && opts[:limit][:limit]
|
456
|
+
find_opts[:offset] = opts[:limit][:skip] if opts[:limit] && opts[:limit][:skip]
|
457
|
+
|
458
|
+
find_opts[:sort] = opts[:order].inject([]) {|a,(k,d)| a << [k, (d == :desc ? Mongo::DESCENDING : Mongo::ASCENDING)]} if opts[:order]
|
459
|
+
|
460
|
+
return find_opts
|
461
|
+
end
|
462
|
+
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
module Poro
|
469
|
+
module Contexts
|
470
|
+
class MongoContext
|
471
|
+
include FinderMethods
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|