aqua 0.1.6

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 (60) hide show
  1. data/.document +5 -0
  2. data/.gitignore +7 -0
  3. data/Aqua.gemspec +121 -0
  4. data/LICENCE_COUCHREST +176 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +105 -0
  7. data/Rakefile +83 -0
  8. data/VERSION +1 -0
  9. data/lib/aqua.rb +101 -0
  10. data/lib/aqua/object/config.rb +43 -0
  11. data/lib/aqua/object/extensions/ar_convert.rb +0 -0
  12. data/lib/aqua/object/extensions/ar_style.rb +0 -0
  13. data/lib/aqua/object/extensions/property.rb +0 -0
  14. data/lib/aqua/object/extensions/validation.rb +0 -0
  15. data/lib/aqua/object/pack.rb +306 -0
  16. data/lib/aqua/object/query.rb +18 -0
  17. data/lib/aqua/object/stub.rb +122 -0
  18. data/lib/aqua/object/tank.rb +54 -0
  19. data/lib/aqua/object/unpack.rb +253 -0
  20. data/lib/aqua/store/couch_db/attachments.rb +183 -0
  21. data/lib/aqua/store/couch_db/couch_db.rb +151 -0
  22. data/lib/aqua/store/couch_db/database.rb +186 -0
  23. data/lib/aqua/store/couch_db/design_document.rb +57 -0
  24. data/lib/aqua/store/couch_db/http_client/adapter/rest_client.rb +53 -0
  25. data/lib/aqua/store/couch_db/http_client/rest_api.rb +62 -0
  26. data/lib/aqua/store/couch_db/server.rb +103 -0
  27. data/lib/aqua/store/couch_db/storage_methods.rb +405 -0
  28. data/lib/aqua/store/storage.rb +59 -0
  29. data/lib/aqua/support/initializers.rb +216 -0
  30. data/lib/aqua/support/mash.rb +144 -0
  31. data/lib/aqua/support/set.rb +23 -0
  32. data/lib/aqua/support/string_extensions.rb +121 -0
  33. data/spec/aqua_spec.rb +19 -0
  34. data/spec/object/config_spec.rb +58 -0
  35. data/spec/object/object_fixtures/array_udder.rb +5 -0
  36. data/spec/object/object_fixtures/canned_hash.rb +5 -0
  37. data/spec/object/object_fixtures/gerbilmiester.rb +18 -0
  38. data/spec/object/object_fixtures/grounded.rb +13 -0
  39. data/spec/object/object_fixtures/log.rb +19 -0
  40. data/spec/object/object_fixtures/persistent.rb +12 -0
  41. data/spec/object/object_fixtures/sugar.rb +4 -0
  42. data/spec/object/object_fixtures/user.rb +38 -0
  43. data/spec/object/pack_spec.rb +607 -0
  44. data/spec/object/query_spec.rb +27 -0
  45. data/spec/object/stub_spec.rb +51 -0
  46. data/spec/object/tank_spec.rb +61 -0
  47. data/spec/object/unpack_spec.rb +361 -0
  48. data/spec/spec.opts +3 -0
  49. data/spec/spec_helper.rb +16 -0
  50. data/spec/store/couchdb/attachments_spec.rb +164 -0
  51. data/spec/store/couchdb/couch_db_spec.rb +104 -0
  52. data/spec/store/couchdb/database_spec.rb +161 -0
  53. data/spec/store/couchdb/design_document_spec.rb +43 -0
  54. data/spec/store/couchdb/fixtures_and_data/document_fixture.rb +3 -0
  55. data/spec/store/couchdb/fixtures_and_data/image_attach.png +0 -0
  56. data/spec/store/couchdb/server_spec.rb +96 -0
  57. data/spec/store/couchdb/storage_methods_spec.rb +408 -0
  58. data/utils/code_statistics.rb +134 -0
  59. data/utils/console +11 -0
  60. metadata +136 -0
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.6
data/lib/aqua.rb ADDED
@@ -0,0 +1,101 @@
1
+ # DEPENDENCIES -------------------------------------
2
+ # require gems
3
+ require 'rubygems'
4
+ require 'ruby2ruby'
5
+ require 'mime/types'
6
+ # Pick your json poison. Just make sure that in adds the JSON constant
7
+ unless defined?(JSON)
8
+ begin
9
+ require 'json'
10
+ rescue LoadError
11
+ raise LoadError, "JSON constant not found. Please install a JSON library"
12
+ end
13
+ end
14
+ # There is also a dependency on the http_client of your choosing. Currently only ...
15
+ # require 'rest_client'
16
+ # It is required when a libarary is configured. If the library is not configured then it will
17
+ # automatically load the default: rest_client
18
+
19
+ # standard libraries
20
+ require 'cgi'
21
+ require 'time'
22
+ require 'ostruct'
23
+ require 'tempfile'
24
+
25
+ # require local libs
26
+ $:.unshift File.join(File.dirname(__FILE__))
27
+ # monkey pathches
28
+ require 'aqua/support/mash'
29
+ require 'aqua/support/string_extensions'
30
+ # storage
31
+ require 'aqua/store/storage'
32
+ # object methods
33
+ require 'aqua/object/tank'
34
+ # a little more monkey patching for object packaging
35
+ require 'aqua/support/initializers'
36
+
37
+
38
+ # LIBRARY SETUP -----------------------------------
39
+ module Aqua
40
+ class ObjectNotFound < IOError; end
41
+
42
+ # Loads the requested backend storage engine. Used early on in configuration.
43
+ # If not declared in configuration, will be called automatically with default
44
+ # engine, at runtime, when needed.
45
+ #
46
+ # @overload set_storage_engine( engine )
47
+ # Loads an Aqua internal library.
48
+ # @param [String] CamelCase string defining the overarching engine type
49
+ # @overload set_storage_engine( engine_details )
50
+ # Loads any engine provided a path to the self-loading library and the module full name
51
+ # @param [Hash] options that describe how to find the external library
52
+ # @option engine_details [String] :require The path or gem name used in a require statement
53
+ # @option engine_details [String] :module String with the full module name
54
+ # @return [TrueClass] when successful.
55
+ # @raise [ArgumentError] when argument is neither a Hash nor a String.
56
+ #
57
+ # @example Internal library loading from a string
58
+ # Aqua.set_storage_engine( "CouchDB" )
59
+ #
60
+ # @example External library loading from a gem. :module argument is the gem's module responsible for implementing the storage methods
61
+ # Aqua.set_storage_engine( :require => 'my_own/storage_gem', :module => 'MyOwn::StorageGem::StorageMethods' )
62
+ #
63
+ # @example External library loading from a non-gem external library.
64
+ # Aqua.set_storage_engine( :require => '/absolute/path/to/library', :module => 'My::StorageLib::StorageMethods' )
65
+ #
66
+ # @api public
67
+ def self.set_storage_engine( engine="CouchDB" )
68
+ if engine.class == String
69
+ load_internal_engine( engine )
70
+ true
71
+ elsif engine.class == Hash
72
+ engine = Mash.new( engine )
73
+ require engine[:require]
74
+ include_engine( engine[:module] )
75
+ true
76
+ else
77
+ raise ArgumentError, 'engine must be a string relating to an internal Aqua library store, or a hash of values indicating where to find the external library'
78
+ end
79
+ end
80
+
81
+ # Loads an internal engine from a string
82
+ # @api private
83
+ def self.load_internal_engine( str )
84
+ underscored = str.underscore
85
+ require "aqua/store/#{underscored}/#{underscored}"
86
+ include_engine( "Aqua::Store::#{str}::StorageMethods" )
87
+ end
88
+
89
+ # Loads an external engine from a hash of options.
90
+ # @see Aqua#set_storage_engine for the public method that uses this internally
91
+ # @api private
92
+ def self.include_engine( str )
93
+ Aqua::Storage.class_eval do
94
+ include str.constantize
95
+ end
96
+ end
97
+
98
+ end # Aqua
99
+
100
+ # This is temporary until more engines are available!
101
+ Aqua.set_storage_engine('CouchDB') # to initialize CouchDB
@@ -0,0 +1,43 @@
1
+ module Aqua::Config
2
+ def self.included( klass )
3
+ # This per aquatic class storage class is used to maintain the storage specific options,
4
+ # such as a particular database for the class. Otherwise, appeals directly to Aqua::Storage
5
+ # for class methods will loose database and other class specific storage options
6
+ klass.class_eval "
7
+ class Storage < Aqua::Storage
8
+ def self.parent_class
9
+ '#{klass}'
10
+ end
11
+ end
12
+ "
13
+
14
+ klass.class_eval do
15
+ extend ClassMethods
16
+ configure_aqua
17
+
18
+ hide_attributes :_aqua_opts
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def configure_aqua(opts={})
24
+ database = opts.delete(:database)
25
+ self::Storage.database = database
26
+ @_aqua_opts = Mash.new( _aqua_opts ).merge!(opts)
27
+ end
28
+
29
+ def _aqua_opts
30
+ @_aqua_opts ||= _aqua_config_defaults
31
+ end
32
+
33
+ private
34
+ def _aqua_config_defaults
35
+ {
36
+ :database => nil, # Default is the same as the server. Everything is saved to the same db
37
+ :embed => false, # options false, true, or :stub => [:attributes, :to_save, :in_the_other_object]
38
+ }
39
+ end
40
+ public
41
+ end
42
+
43
+ end
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,306 @@
1
+ # This module is responsible for packing objects into Storage Objects
2
+ # The Storage Object is expected to be a Mash-Like thing (Hash with indifferent access).
3
+ # It is the job of the storage engine to convert the Mash into the actual storage ivars.
4
+ module Aqua
5
+ module Pack
6
+
7
+ def self.included( klass )
8
+ klass.class_eval do
9
+ extend HiddenAttributes::ClassMethods
10
+ include HiddenAttributes::InstanceMethods
11
+ extend ClassMethods
12
+ include InstanceMethods
13
+
14
+ unless instance_methods.include?( 'id=' ) || new.instance_variables.include?( '@id' )
15
+ attr_accessor :id
16
+ end
17
+
18
+ hide_attributes :_store, :__pack, :id, :_rev
19
+ end
20
+ end
21
+
22
+ module HiddenAttributes
23
+ def self.included( klass )
24
+ klass.class_eval do
25
+ extend ClassMethods
26
+ include InstanceMethods
27
+ end
28
+ end
29
+
30
+ module ClassMethods
31
+ # Reader method for accessing hidden attributes.
32
+ # @return [Array] containing strings representing instance variables
33
+ # @api private
34
+ def _hidden_attributes
35
+ @_hidden_attributes ||= []
36
+ end
37
+
38
+ # Used in class declaration to assign certain instance variables as not for persistance
39
+ # @param [Symbol] or [Array of Symbols] ivars
40
+ #
41
+ # @example
42
+ # class User
43
+ # include Aqua::Object
44
+ # attr_accessor :username, :email, :password, :password_confirmation, :cryped_password, :salt
45
+ # hide_attributes :password, :password_confirmation
46
+ # # ... lots more user code here ...
47
+ # end
48
+ # In this case it is useful for omitting sensitive information while persisting the object, but
49
+ # maintaining the password and confirmation temporarily requires the use of instance variables.
50
+ def hide_attributes( *ivars )
51
+ ivars.each do |ivar|
52
+ raise ArgumentError, '' unless ivar.class == Symbol
53
+ _hidden_attributes << "@#{ivar}" unless _hidden_attributes.include?( "@#{ivar}" )
54
+ end
55
+ end
56
+ end # ClassMethods
57
+
58
+ module InstanceMethods
59
+ # An array of instance variables that are not hidden.
60
+ # @return [Array] of names for instance variables
61
+ #
62
+ # @api private
63
+ def _storable_attributes
64
+ (instance_variables||[]) - self.class._hidden_attributes
65
+ end
66
+ end # InstanceMethods
67
+ end
68
+
69
+ module ClassMethods
70
+ end # ClassMethods
71
+
72
+ module InstanceMethods
73
+ # TODO: option for transaction on children documents, all or nothing
74
+
75
+ # Saves object; returns false on failure; returns self on success.
76
+ def commit
77
+ _commit
78
+ end
79
+
80
+ # Saves object and raises an error on failure
81
+ def commit!
82
+ _commit( false )
83
+ end
84
+
85
+ # packs an object from it's Ruby state into a Hash-like object for storage.
86
+ # @return [Storage]
87
+ #
88
+ # @api private
89
+ def _pack
90
+ class_name = self.class.to_s
91
+ self.__pack = Storage.new
92
+ self.__pack.id = @id if @id
93
+ self.__pack[:_rev] = _rev if _rev
94
+ self.__pack[:keys] = []
95
+ self.__pack[:stubs] = []
96
+ self.__pack.merge!( _pack_object( self ) )
97
+ _pack_singletons
98
+ __pack
99
+ end
100
+
101
+ # Details from configuration options for the objects class about embedability.
102
+ # @return [true, false, Hash] If true then it should be embedded in the object at hand.
103
+ # If false, then it should be saved externally. If a hash, with the key :stub and a related
104
+ # value that is an array of methods, then the object should be saved externally,
105
+ # with a few cached methods as defined in the array.
106
+ #
107
+ # @api private
108
+ def _embed_me
109
+ self.class._aqua_opts[:embed]
110
+ end
111
+
112
+ # Packs an object into data and meta data. Works recursively sending out to array, hash, etc.
113
+ # object packers, which send their values back to _pack_object
114
+ #
115
+ # @param Object to pack
116
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
117
+ #
118
+ # @api private
119
+ def _pack_object( obj )
120
+ klass = obj.class
121
+ if klass == String
122
+ obj
123
+ elsif obj.respond_to?(:to_aqua) # Types requiring initialization
124
+ obj.to_aqua( self )
125
+ elsif obj.aquatic? && obj != self
126
+ if obj._embed_me == true
127
+ obj._pack
128
+ else
129
+ _build_stub( obj )
130
+ end
131
+ else # other object without initializations
132
+ _pack_vanilla( obj )
133
+ end
134
+ end
135
+
136
+ # Packs the ivars for a given object.
137
+ #
138
+ # @param Object to pack
139
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
140
+ #
141
+ # @api private
142
+ def _pack_ivars( obj )
143
+ return_hash = {}
144
+ vars = obj.respond_to?(:_storable_attributes) ? obj._storable_attributes : obj.instance_variables
145
+ vars.each do |ivar_name|
146
+ ivar = obj.instance_variable_get( ivar_name )
147
+ return_hash[ivar_name] = _pack_object( ivar ) unless ivar.nil?
148
+ end
149
+ return_hash
150
+ end
151
+
152
+ # Handles the case of an hash-like object with keys that are objects
153
+ #
154
+ # @param Object to pack
155
+ # @return [Integer] Index of the object in the keys array, used by the hash packer to name the key
156
+ #
157
+ # @api private
158
+ def _build_object_key( obj )
159
+ index = self.__pack[:keys].length
160
+ self.__pack[:keys] << _pack_object( obj )
161
+ index # return key
162
+ end
163
+
164
+ # Adds an attachment to the __pack document. Before save the attachments are encoded into the doc
165
+ #
166
+ # @param [String] filename used as the key/index
167
+ # @param [File, Tempfile] file to be attached
168
+ #
169
+ # @api semi-public
170
+ def _pack_file( filename, file )
171
+ __pack.attachments.add( filename, file )
172
+ end
173
+
174
+
175
+ attr_accessor :_warnings
176
+
177
+ # Private/protected methods are all prefaced by an underscore to prevent
178
+ # clogging the object instance space. Some of the public ones above are too!
179
+ protected
180
+
181
+ # __pack is an Storage object into which the object respresentation is packed
182
+ #
183
+ # _store is the current state of the storage of the object on CouchDB. It is used lazily
184
+ # and will be empty unless it is needed for unpacking or checking for changed data.
185
+ #
186
+ # _rev is needed for CouchDB store, since updates require the rev information. We could
187
+ # do without this accessor, but it would mean that an extra get request would have to be
188
+ # made with each PUT request so that the latest _rev could be obtained.
189
+ #
190
+ attr_accessor :_store, :__pack, :_rev
191
+
192
+ private
193
+
194
+ def _commit( mask_exception = true )
195
+ result = true
196
+ begin
197
+ _pack
198
+ _save_to_store
199
+ rescue Exception => e
200
+ if mask_exception
201
+ result = false
202
+ else
203
+ raise e
204
+ end
205
+ end
206
+ if result
207
+ self.id = __pack.id
208
+ self._rev = __pack.rev
209
+ _clear_accessors
210
+ self
211
+ else
212
+ result
213
+ end
214
+ end
215
+
216
+ # Object packing methods ------------
217
+
218
+ # Packs the an object requiring no initialization.
219
+ #
220
+ # @param Object to pack
221
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
222
+ #
223
+ # @api private
224
+ def _pack_vanilla( obj )
225
+ {
226
+ 'class' => obj.class.to_s,
227
+ 'ivars' => _pack_ivars( obj )
228
+ }
229
+ end
230
+
231
+ # Packs the stub for an externally saved object.
232
+ #
233
+ # @param Object to pack
234
+ # @return [Mash] Indifferent hash that is the data/metadata deconstruction of an object.
235
+ #
236
+ # @api private
237
+ def _build_stub( obj )
238
+ index = self.__pack[:stubs].length
239
+ stub = { :class => obj.class.to_s, :id => obj }
240
+ # deal with cached methods
241
+ if obj._embed_me && obj._embed_me.keys && stub_methods = obj._embed_me[:stub]
242
+ stub[:methods] = {}
243
+ if stub_methods.class == Symbol || stub_methods.class == String
244
+ stub_method = stub_methods.to_sym
245
+ stub[:methods][stub_method] = obj.send( stub_method )
246
+ else # is an array of values
247
+ stub_methods.each do |meth|
248
+ stub_method = meth.to_sym
249
+ stub[:methods][stub_method] = obj.send( stub_method )
250
+ end
251
+ end
252
+ end
253
+ # add the stub
254
+ self.__pack[:stubs] << stub
255
+ # return a hash
256
+ {'class' => 'Aqua::Stub', 'init' => "/STUB_#{index}"}
257
+ end
258
+
259
+ def _pack_singletons
260
+ # TODO: figure out 1.8 and 1.9 compatibility issues.
261
+ # Also learn the library usage, without any docs :(
262
+ end
263
+
264
+ # Saves all self and nested object requiring independent saves
265
+ #
266
+ # @return [Object, false] Returns false on failure and self on success.
267
+ #
268
+ # @api private
269
+ def _save_to_store
270
+ self._warnings = []
271
+ _commit_externals
272
+ __pack.commit # TODO: need to add some error catching and roll back the external saves where needed
273
+ end
274
+
275
+ # Saves nested object requiring independent saves. Adds warning messages to _warnings, when a save fails.
276
+ #
277
+ # @api private
278
+ def _commit_externals
279
+ __pack[:stubs].each_with_index do |obj_hash, index|
280
+ obj = obj_hash[:id]
281
+ if obj.commit
282
+ obj_hash[:id] = obj.id
283
+ else
284
+ if obj.id
285
+ self._warnings << "Unable to save latest version of #{obj.inspect}, stubbed at index #{index}"
286
+ obj_hash[:id] = obj.id if obj.id
287
+ else
288
+ self._warnings << "Unable to save #{obj.inspect}, stubbed at index #{index}"
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+ # clears the __pack and _store accessors to save on memory after each pack and unpack
295
+ #
296
+ # @api private
297
+ def _clear_accessors
298
+ self.__pack = nil
299
+ self._store = nil
300
+ end
301
+
302
+ public
303
+ end # InstanceMethods
304
+ end # Pack
305
+ end # Aqua
306
+