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