poro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
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,4 @@
1
+ require 'poro/contexts/hash_context'
2
+ require 'poro/contexts/mongo_context'
3
+ #require 'poro/contexts/sequel_context'
4
+ #require 'poro/contexts/memcache_context'
@@ -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