aqua 0.1.6 → 0.2.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.
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