aqua 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +2 -1
  2. data/Aqua.gemspec +14 -11
  3. data/Rakefile +1 -1
  4. data/VERSION +1 -1
  5. data/lib/aqua.rb +5 -7
  6. data/lib/aqua/object/config.rb +2 -3
  7. data/lib/aqua/object/initializers.rb +309 -0
  8. data/lib/aqua/object/pack.rb +56 -132
  9. data/lib/aqua/object/query.rb +30 -2
  10. data/lib/aqua/object/stub.rb +60 -95
  11. data/lib/aqua/object/tank.rb +1 -0
  12. data/lib/aqua/object/translator.rb +313 -0
  13. data/lib/aqua/object/unpack.rb +26 -227
  14. data/lib/aqua/store/couch_db/couch_db.rb +1 -0
  15. data/lib/aqua/store/couch_db/database.rb +1 -1
  16. data/lib/aqua/store/couch_db/design_document.rb +126 -2
  17. data/lib/aqua/store/couch_db/result_set.rb +36 -0
  18. data/lib/aqua/store/couch_db/storage_methods.rb +182 -17
  19. data/lib/aqua/store/storage.rb +4 -48
  20. data/lib/aqua/support/mash.rb +2 -3
  21. data/lib/aqua/support/set.rb +4 -16
  22. data/spec/object/object_fixtures/array_udder.rb +1 -1
  23. data/spec/object/object_fixtures/persistent.rb +0 -2
  24. data/spec/object/pack_spec.rb +137 -517
  25. data/spec/object/query_spec.rb +36 -6
  26. data/spec/object/stub_spec.rb +10 -9
  27. data/spec/object/translator_packing_spec.rb +402 -0
  28. data/spec/object/translator_unpacking_spec.rb +262 -0
  29. data/spec/object/unpack_spec.rb +162 -320
  30. data/spec/spec_helper.rb +18 -0
  31. data/spec/store/couchdb/design_document_spec.rb +148 -7
  32. data/spec/store/couchdb/result_set_spec.rb +95 -0
  33. data/spec/store/couchdb/storage_methods_spec.rb +150 -10
  34. metadata +13 -9
  35. data/lib/aqua/support/initializers.rb +0 -216
  36. data/spec/object/object_fixtures/grounded.rb +0 -13
  37. data/spec/object/object_fixtures/sugar.rb +0 -4
@@ -1,5 +1,6 @@
1
1
  dir = File.dirname(__FILE__)
2
2
  require dir + '/pack'
3
+ require dir + '/translator'
3
4
  require dir + '/query'
4
5
  require dir + '/unpack'
5
6
  require dir + '/config'
@@ -0,0 +1,313 @@
1
+ module Aqua
2
+ # Packing of objects needs to save an object as well as query it. Therefore this packer module gives a lot
3
+ # of class methods and mirrored instance methods that pack various types of objects. The instance methods
4
+ # aggregate all of the attachments and externals that need to be mapped back to the base object after they
5
+ # are saved. The class methods return an array with the packaging in the first element and the attachment
6
+ # and externals in subsequent elements
7
+ class Translator
8
+ attr_accessor :base_object, :base_id
9
+
10
+ # PACKING ---------------------------------------------------------------------------------------
11
+ # ===============================================================================================
12
+ attr_writer :externals, :attachments
13
+
14
+ # Hash-like object that gathers all the externally saved aquatic objects. Pattern for storage is
15
+ # external_object => 'pack_path_to_object'
16
+ # where the string 'pack_path_to_object' gets instance evaled in order to update the id in the pack
17
+ # after a new external is saved
18
+ # @api private
19
+ def externals
20
+ @externals ||= {}
21
+ end
22
+
23
+ # An array of attachments that have to be stored by the base object
24
+ # @api private
25
+ def attachments
26
+ @attachments ||= []
27
+ end
28
+
29
+ # Although most of what the translator does happens via class level methods, instantiation happens
30
+ # to maintain a reference to the base object when packing/saving externals and attachments. It is
31
+ # also needed in the unpack process to locate attachments.
32
+ #
33
+ # @param [Aquatic Object]
34
+ #
35
+ # @api private
36
+ def initialize( base_object, base_id=nil )
37
+ load_base( base_object, base_id )
38
+ end
39
+
40
+ # Method used by initialized and reload_object to set necessary base object data
41
+ def load_base( base_object, base_id=nil)
42
+ self.base_object = base_object
43
+ self.base_id = base_object.id || base_id
44
+ end
45
+
46
+ # This is a wrapper method that takes the a class method and aggregates externals and attachments
47
+ # @api private
48
+ def pack
49
+ rat = yield
50
+ self.externals.merge!( rat.externals )
51
+ self.attachments += rat.attachments
52
+ rat.pack
53
+ end
54
+
55
+ # Packs the ivars for a given object.
56
+ #
57
+ # @param Object to pack
58
+ # @param [String] path to this particular object within the parent object
59
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
60
+ #
61
+ # @api private
62
+ def self.pack_ivars( obj, path='' )
63
+ path = "#{path}['ivars']"
64
+ rat = Rat.new
65
+ vars = obj.respond_to?(:_storable_attributes) ? obj._storable_attributes : obj.instance_variables
66
+ vars.each do |ivar_name|
67
+ ivar = obj.instance_variable_get( ivar_name )
68
+ ivar_path = path + "['#{ivar_name}']"
69
+ if ivar
70
+ if ivar == obj # self referential TODO: this will only work direct descendants :(
71
+ ivar_rat = pack_to_stub( ivar, ivar_path )
72
+ rat.hord( ivar_rat, ivar_name )
73
+ else
74
+ ivar_rat = pack_object( ivar, ivar_path )
75
+ rat.hord( ivar_rat, ivar_name )
76
+ end
77
+ end
78
+ end
79
+ rat
80
+ end
81
+
82
+ # The instance version is wrapped by the #pack method. It otherwise performs the class method
83
+ def pack_ivars( obj, path='' )
84
+ pack { self.class.pack_ivars( obj ) }
85
+ end
86
+
87
+ # Packs an object into data and meta data. Works recursively sending out to array, hash, etc.
88
+ # object packers, which send their values back to _pack_object
89
+ #
90
+ # @param Object to pack
91
+ # @param [String] path, so that unsaved externals can find and set their id after creation
92
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
93
+ #
94
+ # @api private
95
+ def self.pack_object( obj, path='' )
96
+ klass = obj.class
97
+ if obj.respond_to?(:to_aqua) # probably requires special initialization not just ivar assignment
98
+ obj.to_aqua( path )
99
+ elsif obj.aquatic?
100
+ if obj._embedded? || path == ''
101
+ pack_vanilla( obj, path )
102
+ else
103
+ pack_to_stub( obj, path)
104
+ end
105
+ else
106
+ pack_vanilla( obj, path )
107
+ end
108
+ end
109
+
110
+ # The instance version is wrapped by the #pack method. It otherwise performs the class method
111
+ def pack_object( obj, path='' )
112
+ pack { self.class.pack_object( obj ) }
113
+ end
114
+
115
+ # Packs the an object requiring no initialization.
116
+ #
117
+ # @param Object to pack
118
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
119
+ #
120
+ # @api private
121
+ def self.pack_vanilla( obj, path='' )
122
+ rat = Rat.new( { 'class' => obj.class.to_s } )
123
+ ivar_rat = pack_ivars( obj, path )
124
+ rat.hord( ivar_rat, 'ivars' ) unless ivar_rat.pack.empty?
125
+ rat
126
+ end
127
+
128
+ # The instance version is wrapped by the #pack method. It otherwise performs the class method
129
+ def pack_vanilla( obj, path='' )
130
+ pack { self.class.pack_vanilla( obj ) }
131
+ end
132
+
133
+
134
+ # Packs the stub for an externally saved object.
135
+ #
136
+ # @param Object to pack
137
+ # @param [String] path to this part of the object
138
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
139
+ #
140
+ # @api private
141
+ def self.pack_to_stub( obj, path='' )
142
+ rat = Rat.new( {'class' => 'Aqua::Stub'} )
143
+ stub_rat = Rat.new({'class' => obj.class.to_s, 'id' => obj.id || '' }, {obj => path} )
144
+ # deal with cached methods
145
+ unless (stub_methods = obj._stubbed_methods).empty?
146
+ stub_rat.pack['methods'] = {}
147
+ stub_methods.each do |meth|
148
+ meth = meth.to_s
149
+ method_rat = pack_object( obj.send( meth ) )
150
+ stub_rat.hord( method_rat, ['methods', "#{meth}"])
151
+ end
152
+ end
153
+ rat.hord( stub_rat, 'init' )
154
+ end
155
+
156
+ # The instance version is wrapped by the #pack method. It otherwise performs the class method
157
+ def pack_to_stub( obj, path='' )
158
+ pack { self.class.pack_to_stub( obj ) }
159
+ end
160
+
161
+ # def pack_singletons
162
+ # # TODO: figure out 1.8 and 1.9 compatibility issues.
163
+ # # Also learn the library usage, without any docs :(
164
+ # end
165
+
166
+ # Rat class is used by the translators packing side and aggregates methods for merging
167
+ # packed representation, externals and attachments
168
+ class Rat
169
+ attr_accessor :pack, :externals, :attachments
170
+ def initialize( pack=Mash.new, externals=Mash.new, attachments=[] )
171
+ self.pack = pack
172
+ self.externals = externals
173
+ self.attachments = attachments
174
+ end
175
+
176
+ # merges the two rats
177
+ def eat( other_rat )
178
+ if self.pack.respond_to?(:keys)
179
+ self.pack.merge!( other_rat.pack )
180
+ else
181
+ self.pack << other_rat.pack # this is a special case for array init rats
182
+ end
183
+ self.externals.merge!( other_rat.externals )
184
+ self.attachments += other_rat.attachments
185
+ self
186
+ end
187
+
188
+ # outputs and resets the accessor
189
+ def barf( accessor )
190
+ case accessor
191
+ when :pack, 'pack'
192
+ meal = self.pack
193
+ self.pack = {}
194
+ when :externals, 'externals'
195
+ meal = self.externals
196
+ self.externals = {}
197
+ else
198
+ meal = self.attachments
199
+ self.attachments = []
200
+ end
201
+ meal
202
+ end
203
+
204
+ def hord( other_rat, index)
205
+ if [String, Symbol].include?( index.class )
206
+ self.pack[index] = other_rat.barf(:pack)
207
+ else # for nested hording
208
+ eval_string = index.inject("self.pack") do |result, element|
209
+ element = "'#{element}'" if element.class == String
210
+ result += "[#{element}]"
211
+ end
212
+ value = other_rat.barf(:pack)
213
+ instance_eval "#{eval_string} = #{value.inspect}"
214
+ end
215
+ self.eat( other_rat )
216
+ self
217
+ end
218
+
219
+ def ==( other_rat )
220
+ self.pack == other_rat.pack && self.externals == other_rat.externals && self.attachments == other_rat.attachments
221
+ end
222
+ end
223
+
224
+ # PACKING ---------------------------------------------------------------------------------------
225
+ # ===============================================================================================
226
+ def self.classes
227
+ @classes ||= {}
228
+ end
229
+
230
+ def self.get_class( class_str )
231
+ classes[class_str] ||= class_str.constantize if class_str
232
+ end
233
+
234
+ def unpack_object( doc, opts=Opts.new )
235
+ opts.base_object = self.base_object
236
+ opts.base_id = self.base_object.id || self.base_id
237
+ self.class.unpack_object( doc, opts )
238
+ end
239
+
240
+ def self.unpack_object( doc, opts=Opts.new )
241
+ if doc.respond_to?(:from_aqua)
242
+ doc.from_aqua # these are basic types, like nil, strings, true/false, or symbols
243
+ else
244
+ obj = new_from_doc( doc, opts )
245
+
246
+ # mixin the id and rev
247
+ if obj.aquatic? && !obj._embedded?
248
+ obj.id = doc.id if doc.id
249
+ obj._rev = doc.rev if doc.rev
250
+ end
251
+
252
+ unpack_ivars( obj, doc, opts )
253
+ obj
254
+ end
255
+ end
256
+
257
+ def self.new_from_doc( doc, opts )
258
+ klass = get_class( doc['class'] )
259
+ obj = if (init = doc['init']) && klass.respond_to?( :aqua_init )
260
+ klass.aqua_init( init, opts )
261
+ else
262
+ klass.new
263
+ end
264
+ end
265
+
266
+ def reload_from_init( doc )
267
+ if init = doc['init']
268
+ raise NotImplementedError, "Class #{base_object.class} does not implement a #replace method and can't be reloaded" unless base_object.respond_to?( :replace )
269
+ init_unpacked = unpack_object( {'class' => init.class.to_s, 'init' => init } )
270
+ base_object.replace( init_unpacked )
271
+ end
272
+ base_object
273
+ end
274
+
275
+ def unpack_ivars( doc )
276
+ opts = Opts.new
277
+ opts.base_object = self.base_object
278
+ opts.base_id = self.base_object.id || self.base_id
279
+ self.class.unpack_ivars( base_object, doc, opts )
280
+ end
281
+
282
+
283
+ def self.unpack_ivars( obj, doc, opts )
284
+ # add the ivars
285
+ if ivars = doc['ivars']
286
+ ivars.each do |ivar_name, ivar_hash|
287
+ opts.path += "#{ivar_name}"
288
+ unpacked_ivar = unpack_object( ivar_hash, opts )
289
+ obj.instance_variable_set( ivar_name, unpacked_ivar )
290
+ end
291
+ end
292
+ end
293
+
294
+ def reload_object( obj, doc )
295
+ load_base( obj ) # loads this object as the base for the translator
296
+ reload_from_init( doc )
297
+ # todo: clear all non-hidden ivars too
298
+ unpack_ivars( doc )
299
+ base_object._rev = doc.rev if doc.rev
300
+ end
301
+
302
+ class Opts
303
+ attr_accessor :base_object
304
+ attr_accessor :base_id
305
+ attr_writer :path
306
+
307
+ def path
308
+ @path ||= ''
309
+ end
310
+ end
311
+
312
+ end # Translator
313
+ end # Aqua
@@ -4,7 +4,7 @@ module Aqua::Unpack
4
4
  klass.class_eval do
5
5
  extend ClassMethods
6
6
  include InstanceMethods
7
- end
7
+ end
8
8
  end
9
9
 
10
10
  module ClassMethods
@@ -14,240 +14,39 @@ module Aqua::Unpack
14
14
  #
15
15
  # @api public
16
16
  def load( id )
17
- instance = new
18
- instance.id = id
19
- instance.reload
20
- instance
17
+ doc = _get_store( id )
18
+ build( doc, id )
21
19
  end
20
+
21
+ # Retrieves objects storage from its engine.
22
+ # @return [Storage]
23
+ #
24
+ # @api private
25
+ def _get_store( id )
26
+ doc = self::Storage.get( id )
27
+ raise ArgumentError, "#{self} with id of #{doc_id} was not found" unless doc
28
+ doc
29
+ end
30
+
31
+ # Creates a new object from the doc; It is used by queries which return a set of docs.
32
+ # Also used by load to do the same thing ...
33
+ # @param [Document, Hash, Mash] converted object
34
+ def build( doc, id=nil )
35
+ translator = Aqua::Translator.new( new, id )
36
+ translator.unpack_object( doc )
37
+ end
22
38
  end
23
39
 
24
40
  module InstanceMethods
25
- # Reloads database information into the object.
26
- # @param [optional true, false] Default is true. If true the exceptions will be swallowed and
27
- # false will be returned. If false, then any exceptions raised will stop the show.
28
- # @return [Object, false] Will return false or raise error on failure and self on success.
29
- #
30
- # @api public
31
- def reload( mask_exceptions = true )
32
- if id.nil?
33
- if mask_exceptions
34
- false
35
- else
36
- raise ObjectNotFound, "#{self.class} instance must have an id to be reloaded"
37
- end
38
- else
39
- begin
40
- _reload
41
- rescue Exception => e
42
- if mask_exceptions
43
- false
44
- else
45
- raise e
46
- end
47
- end
48
- end
49
- end
50
-
51
41
  # Reloads database information into the object, and raises an error on failure.
52
42
  # @return [Object] Will return raise error on failure and return self on success.
53
43
  #
54
44
  # @api public
55
- def reload!
56
- reload( false )
57
- end
58
-
59
- private
60
- # Actual mechanism for reloading an object from stored data.
61
- # @return [Object] Will return raise error on failure and return self on success.
62
- #
63
- # @api private
64
- def _reload
65
- _get_store
66
- _unpack
67
- _clear_store
68
- self
69
- end
70
-
71
- # Retrieves objects storage from its engine.
72
- # @return [Storage]
73
- #
74
- # @api private
75
- def _get_store
76
- # this is kind of klunky, should refactor
77
- self._store = self.class::Storage.new(:id => self.id).retrieve
78
- end
79
-
80
- # Unpacks an object from hash representation of data and metadata
81
- # @return [Storage]
82
- # @todo Refactor to move more of this into individual classes
83
- #
84
- # @api private
85
- def _unpack
86
- if init = _unpack_initialization( _store )
87
- replace( init )
88
- end
89
- if ivars = _store[:ivars]
90
- _unpack_ivars( self, ivars )
91
- end
92
- end
93
-
94
- # Makes @_store nil to converve on memory
95
- #
96
- # @api private
97
- def _clear_store
98
- @_store = nil
99
- end
100
-
101
- # Unpacks an object's instance variables
102
- # @todo Refactor to move more of this into individual classes
103
- #
104
- # @api private
105
- def _unpack_ivars( obj, data )
106
- data.each do |ivar_name, data_package|
107
- unpacked = if data_package.class == String
108
- data_package
109
- else
110
- _unpack_object( data_package )
111
- end
112
- obj.instance_variable_set( ivar_name, unpacked )
113
- end
114
- end
115
-
116
- # Unpacks an the initialization object from a hash into a real object.
117
- # @return [Object] Generally a hash, array or string
118
- # @todo Refactor to move more of this into individual classes
119
- #
120
- # @api private
121
- def _unpack_initialization( obj )
122
- if init = obj[:init]
123
- init_class = init.class
124
- if init_class == String
125
- if init.match(/\A\/STUB_(\d*)\z/)
126
- _unpack_stub( $1.to_i )
127
- elsif init.match(/\A\/FILE_(.*)\z/)
128
- _unpack_file( $1, obj )
129
- else
130
- init
131
- end
132
- elsif init.class == Array
133
- _unpack_array( init )
134
- else
135
- _unpack_hash( init )
136
- end
137
- end
138
- end
139
-
140
- # Retrieves and unpacks a stubbed object from its separate storage area
141
- # @return [Aqua::Stub] Delegate object for externally saved class
142
- # @param [Fixnum] Array index for the stub details, garnered from the key name
143
- #
144
- # @api private
145
- def _unpack_stub( index )
146
- hash = _store[:stubs][index]
147
- Aqua::Stub.new( hash )
148
- end
149
-
150
- # Retrieves and unpacks a stubbed object from its separate storage area
151
- # @param [String] File name, and attachment id
152
- # @return [Aqua::FileStub] Delegate object for file attachments
153
- #
154
- # @api private
155
- def _unpack_file( name, obj )
156
- hash = {
157
- :parent => self,
158
- :id => name,
159
- :methods => obj[:methods]
160
- }
161
- Aqua::FileStub.new( hash )
162
- end
163
-
164
- # Unpacks an Array.
165
- # @return [Object] Generally a hash, array or string
166
- # @todo Refactor ?? move more of this into support/initializers Array
167
- #
168
- # @api private
169
- def _unpack_array( obj )
170
- arr = []
171
- obj.each do |value|
172
- value = _unpack_object( value ) unless value.class == String
173
- arr << value
174
- end
175
- arr
176
- end
177
-
178
- # Unpacks a Hash.
179
- # @return [Object] Generally a hash, array or string
180
- # @todo Refactor ?? move more of this into support/initializers Hash
181
- #
182
- # @api private
183
- def _unpack_hash( obj )
184
- hash = {}
185
- obj.each do |raw_key, value|
186
- value = _unpack_object( value ) unless value.class == String
187
- if raw_key.match(/\A(:)/)
188
- key = raw_key.gsub($1, '').to_sym
189
- elsif raw_key.match(/\A\/OBJECT_(\d*)\z/)
190
- key = _unpack_object( self._store[:keys][$1.to_i] )
191
- else
192
- key = raw_key
193
- end
194
- hash[key] = value
195
- end
196
- hash
197
- end
198
-
199
- # The real workhorse behind object construction: it recursively rebuilds objects based on
200
- # whether the passed in object is an Array, String or a Hash (true/false too now).
201
- # A hash that has the class key
202
- # is an object representation. If it does not have a hash key then it is an ordinary hash.
203
- # An array will either have strings or object representations values.
204
- #
205
- # @param [Hash, Mash] Representation of the data in the aqua meta format
206
- # @return [Object] The object represented by the data
207
- #
208
- # @api private
209
- def _unpack_object( store_pack )
210
- package_class = store_pack.class
211
- if package_class == String || store_pack == true || store_pack == false
212
- store_pack
213
- elsif package_class == Array
214
- _unpack_array( store_pack )
215
- else # package_class == Hash -or- Mash
216
- if store_pack['class']
217
- # Constantize the objects class
218
- obj_class = store_pack['class'].constantize rescue nil
219
-
220
- # build from initialization
221
- init = _unpack_initialization( store_pack )
222
- return_object = if init
223
- [Aqua::Stub, Aqua::FileStub].include?( obj_class ) ? init : obj_class.aqua_init( init )
224
- end
225
-
226
- # Build uninitialized object
227
- if return_object.nil?
228
- if obj_class
229
- return_object = obj_class.new
230
- else
231
- # should log an error internally
232
- return_object = OpenStruct.new
233
- end
234
- end
235
-
236
- # add the ivars
237
- if ivars = store_pack['ivars']
238
- ivars.delete('@table') if obj_class.ancestors.include?( OpenStruct )
239
- _unpack_ivars( return_object, ivars )
240
- end
241
-
242
- return_object
243
- else # not a packaged object, just a hash, so unpack
244
- _unpack_hash( hash )
245
- end
246
- end
247
-
248
- end
249
-
250
- public
45
+ def reload
46
+ doc = self.class._get_store( id )
47
+ _translator.reload_object( self, doc )
48
+ self
49
+ end
251
50
  end
252
51
 
253
52
  end