poro 0.1.0

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