aqua 0.1.6

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