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.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/Aqua.gemspec +121 -0
- data/LICENCE_COUCHREST +176 -0
- data/LICENSE +20 -0
- data/README.rdoc +105 -0
- data/Rakefile +83 -0
- data/VERSION +1 -0
- data/lib/aqua.rb +101 -0
- data/lib/aqua/object/config.rb +43 -0
- data/lib/aqua/object/extensions/ar_convert.rb +0 -0
- data/lib/aqua/object/extensions/ar_style.rb +0 -0
- data/lib/aqua/object/extensions/property.rb +0 -0
- data/lib/aqua/object/extensions/validation.rb +0 -0
- data/lib/aqua/object/pack.rb +306 -0
- data/lib/aqua/object/query.rb +18 -0
- data/lib/aqua/object/stub.rb +122 -0
- data/lib/aqua/object/tank.rb +54 -0
- data/lib/aqua/object/unpack.rb +253 -0
- data/lib/aqua/store/couch_db/attachments.rb +183 -0
- data/lib/aqua/store/couch_db/couch_db.rb +151 -0
- data/lib/aqua/store/couch_db/database.rb +186 -0
- data/lib/aqua/store/couch_db/design_document.rb +57 -0
- data/lib/aqua/store/couch_db/http_client/adapter/rest_client.rb +53 -0
- data/lib/aqua/store/couch_db/http_client/rest_api.rb +62 -0
- data/lib/aqua/store/couch_db/server.rb +103 -0
- data/lib/aqua/store/couch_db/storage_methods.rb +405 -0
- data/lib/aqua/store/storage.rb +59 -0
- data/lib/aqua/support/initializers.rb +216 -0
- data/lib/aqua/support/mash.rb +144 -0
- data/lib/aqua/support/set.rb +23 -0
- data/lib/aqua/support/string_extensions.rb +121 -0
- data/spec/aqua_spec.rb +19 -0
- data/spec/object/config_spec.rb +58 -0
- data/spec/object/object_fixtures/array_udder.rb +5 -0
- data/spec/object/object_fixtures/canned_hash.rb +5 -0
- data/spec/object/object_fixtures/gerbilmiester.rb +18 -0
- data/spec/object/object_fixtures/grounded.rb +13 -0
- data/spec/object/object_fixtures/log.rb +19 -0
- data/spec/object/object_fixtures/persistent.rb +12 -0
- data/spec/object/object_fixtures/sugar.rb +4 -0
- data/spec/object/object_fixtures/user.rb +38 -0
- data/spec/object/pack_spec.rb +607 -0
- data/spec/object/query_spec.rb +27 -0
- data/spec/object/stub_spec.rb +51 -0
- data/spec/object/tank_spec.rb +61 -0
- data/spec/object/unpack_spec.rb +361 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/store/couchdb/attachments_spec.rb +164 -0
- data/spec/store/couchdb/couch_db_spec.rb +104 -0
- data/spec/store/couchdb/database_spec.rb +161 -0
- data/spec/store/couchdb/design_document_spec.rb +43 -0
- data/spec/store/couchdb/fixtures_and_data/document_fixture.rb +3 -0
- data/spec/store/couchdb/fixtures_and_data/image_attach.png +0 -0
- data/spec/store/couchdb/server_spec.rb +96 -0
- data/spec/store/couchdb/storage_methods_spec.rb +408 -0
- data/utils/code_statistics.rb +134 -0
- data/utils/console +11 -0
- 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
|
+
|