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
@@ -0,0 +1,183 @@
1
+ module Aqua
2
+ module Store
3
+ module CouchDB
4
+ # Attachments is a Hash-like container with keys that are attacment names and values that are file-type
5
+ # objects. Initializing and adding to the collection assures the types of both keys and values. The
6
+ # collection implements a lazy-loading scheme, such that when an attachment is requested and not found,
7
+ # it will try to load it from CouchDB.
8
+ class Attachments < Mash
9
+ attr_reader :document
10
+ attr_reader :stubs
11
+
12
+ # Creates a new attachment collection with keys that are attachment names and values that are
13
+ # file-type objects. The collection manages both the key and the value types.
14
+ #
15
+ # @param [String] Document uri; used to save and retrieve attachments directly
16
+ # @param [Hash] Initialization values
17
+ #
18
+ # @api public
19
+ def initialize( doc, hash={} )
20
+ raise ArgumentError, "must be initialized with a document" unless doc.respond_to?( :retrieve )
21
+ @document = doc
22
+ self.class.validate_hash( hash ) unless hash.empty?
23
+ super( hash )
24
+ end
25
+
26
+ # Adds an attachment to the collection, checking for type. Does not add directly to the database.
27
+ #
28
+ # @param [String, Symbol] Name of the attachment as a string or symbol
29
+ # @param [File] The attachment
30
+ #
31
+ # @api public
32
+ def add( name, file )
33
+ self.class.validate_hash( name => file )
34
+ self[name] = file
35
+ end
36
+
37
+ # Adds an attachment to the collection and to the database. Document doesn't have to be saved,
38
+ # but it does need to have an id.
39
+ #
40
+ # @param [String, Symbol] Name of the attachment as a string or symbol
41
+ # @param [File] The attachment
42
+ #
43
+ # @api public
44
+ def add!( name, file )
45
+ add( name, file )
46
+ content_type = MIME::Types.type_for(file.path).first
47
+ content_type = content_type.nil? ? "text\/plain" : content_type.simplified
48
+ data = {
49
+ 'content_type' => content_type,
50
+ 'data' => Base64.encode64( file.read ).gsub(/\s/,'')
51
+ }
52
+ file.rewind
53
+ response = CouchDB.put( uri_for( name ), data )
54
+ update_doc_rev( response )
55
+ file
56
+ end
57
+
58
+ # Deletes an attachment from the collection, and from the database. Use #delete (from Hash) to just
59
+ # delete the attachment from the collection.
60
+ #
61
+ # @param [String, Symbol] Name of the attachment as a string or symbol
62
+ # @return [File, nil] File at that location or nil if no file found
63
+ #
64
+ # @api public
65
+ def delete!( name )
66
+ if self[name]
67
+ file = delete( name )
68
+ unless document.new?
69
+ CouchDB.delete( uri_for( name ) )
70
+ end
71
+ file
72
+ end
73
+ end
74
+
75
+ # Gets an attachment from the collection first. If not found, it will be requested from the database.
76
+ #
77
+ # @param [String, Symbol] Name of the attachment
78
+ # @return [File, nil] File for that name, or nil if not found in hash or in database
79
+ #
80
+ # @api public
81
+ def get( name, stream=false )
82
+ file = self[name]
83
+ unless file
84
+ file = get!( name, stream )
85
+ end
86
+ file.rewind if file # just in case of previous streaming
87
+ file
88
+ end
89
+
90
+ # Gets an attachment from the database. Stores it in the hash.
91
+ #
92
+ # @param [String, Symbol] Name of the attachment
93
+ # @param [true, false] Stream boolean flag indicating whether the data should be converted to
94
+ # a file or kept as a stream
95
+ # @return [File, nil] File for that name, or nil if not found in the database
96
+ # @raise Any error encountered on retrieval of the attachment, json, http_client, Aqua etc
97
+ #
98
+ # @todo make this more memory favorable, maybe streaming/saving in a max number of bytes
99
+ # @api public
100
+ def get!( name, stream=false )
101
+ file = nil
102
+ response = CouchDB.get( uri_for( name, false ), true ) rescue nil
103
+ data = response && response.respond_to?(:keys) ? Base64.decode64( response['data'] ) : nil
104
+ if data || response
105
+ file = Tempfile.new( CGI.escape( name.to_s ) )
106
+ file.binmode if file.respond_to?( :binmode )
107
+ data ? file.write( data ) : file.write( response )
108
+ file.rewind
109
+ self[name] = file
110
+ end
111
+ stream ? file.read : file
112
+ end
113
+
114
+ # Constructs the standalone attachment uri for PUT and DELETE actions.
115
+ #
116
+ # @param [String] Name of the attachment as a string or symbol
117
+ #
118
+ # @api private
119
+ def uri_for( name, include_rev = true )
120
+ raise ArgumentError, 'Document must have id in order to save an attachment' if document.id.nil? || document.id.empty?
121
+ document.uri + "/#{CGI.escape( name.to_s )}" + ( document.rev && include_rev ? "?rev=#{document.rev}" : "" )
122
+ end
123
+
124
+
125
+ # Validates and throws an error on a hash, insisting that the key is a string or symbol,
126
+ # and the value is a file.
127
+ #
128
+ # @param [Hash]
129
+ #
130
+ # @api private
131
+ def self.validate_hash( hash )
132
+ hash.each do |name, file|
133
+ raise ArgumentError, "Attachment name, #{name.inspect}, must be a Symbol or a String" unless [Symbol, String ].include?( name.class )
134
+ raise ArgumentError, "Attachment file, #{file.inspect}, must be a File-like object" unless file.respond_to?( :read )
135
+ end
136
+ end
137
+
138
+ # Goes into the document and updates it's rev to match the returned rev. That way #new? will return false
139
+ # when an attachment is created before the document is saved. It also means that future attempts to save
140
+ # the doc won't fail with a conflict.
141
+ #
142
+ # @param [Hash] response from the put request
143
+ # @api private
144
+ def update_doc_rev( response )
145
+ document[:_rev] = response['rev']
146
+ end
147
+
148
+ # Creates a hash for the CouchDB _attachments key.
149
+ # @example
150
+ # "_attachments":
151
+ # {
152
+ # "foo.txt":
153
+ # {
154
+ # "content_type":"text\/plain",
155
+ # "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
156
+ # },
157
+ #
158
+ # "bar.txt":
159
+ # {
160
+ # "content_type":"text\/plain",
161
+ # "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
162
+ # }
163
+ # }
164
+ def pack
165
+ pack_hash = {}
166
+ self.keys.each do |key|
167
+ file = self[key]
168
+ content_type = MIME::Types.type_for(file.path).first
169
+ content_type = content_type.nil? ? "text\/plain" : content_type.simplified
170
+ data = {
171
+ 'content_type' => content_type,
172
+ 'data' => Base64.encode64( file.read ).gsub(/\s/,'')
173
+ }
174
+ file.rewind
175
+ pack_hash[key.to_s] = data
176
+ end
177
+ pack_hash
178
+ end
179
+
180
+ end # Attachments
181
+ end # CouchDB
182
+ end # Store
183
+ end # Aqua
@@ -0,0 +1,151 @@
1
+ require File.dirname(__FILE__) + '/http_client/rest_api'
2
+ require File.dirname(__FILE__) + '/server'
3
+ require File.dirname(__FILE__) + '/database'
4
+ require File.dirname(__FILE__) + '/attachments'
5
+ require File.dirname(__FILE__) + '/storage_methods'
6
+ require File.dirname(__FILE__) + '/design_document'
7
+
8
+ module Aqua
9
+ module Store
10
+ module CouchDB
11
+
12
+ class ResourceNotFound < IOError; end
13
+ class RequestFailed < IOError; end
14
+ class RequestTimeout < IOError; end
15
+ class ServerBrokeConnection < IOError; end
16
+ class Conflict < ArgumentError; end
17
+
18
+ # Returns a string describing the http adapter in use, or loads the default and returns a similar string
19
+ # @return [String] A string identifier for the HTTP adapter in use
20
+ def self.http_adapter
21
+ @adapter ||= set_http_adapter
22
+ end
23
+
24
+ # Sets a class variable with the library name that will be loaded.
25
+ # Then attempts to load said library from the adapter directory.
26
+ # It is extended into the HttpAbstraction module. Then the RestAPI which
27
+ # references the HttpAbstraction module is loaded/extended into Aqua
28
+ # this makes available Aqua.get 'http:://someaddress.com' and other requests
29
+
30
+ # Loads an http_adapter from the internal http_client libraries. Right now there is only the
31
+ # RestClient Adapter. Other adapters will be added when people get motivated to write and submit them.
32
+ # By default the RestClientAdapter is used, and if the CouchDB module is used without prior configuration
33
+ # it is automatically loaded.
34
+ #
35
+ # @param [optional, String] Maps to the HTTP Client Adapter module name, file name is inferred by removing the 'Adapter' suffix and underscoring the string
36
+ # @return [String] Name of HTTP Client Adapter module
37
+ # @see Aqua::Store::CouchDB::RestAPI Has detail about the required interface
38
+ # @api public
39
+ def self.set_http_adapter( mod_string='RestClientAdapter' )
40
+
41
+ # what is happening here:
42
+ # strips the Adapter portion of the module name to get at the client name
43
+ # convention over configurationing to get the file name as it relates to files in http_client/adapter
44
+ # require the hopefully found file
45
+ # modify the RestAPI class to extend the Rest methods from the adapter
46
+ # add the RestAPI to Aqua for easy access throughout the library
47
+
48
+ @adapter = mod_string
49
+ mod = @adapter.gsub(/Adapter/, '')
50
+ file = mod.underscore
51
+ require File.dirname(__FILE__) + "/http_client/adapter/#{file}"
52
+ RestAPI.adapter = "#{mod_string}".constantize
53
+ extend(::RestAPI)
54
+ @adapter # return the adapter
55
+ end
56
+
57
+ # Cache of CouchDB Servers used by Aqua. Each is identified by its namespace.
58
+ #
59
+ # @api private
60
+ def self.servers
61
+ @servers ||= {}
62
+ end
63
+
64
+ # Reader for getting or initializtion and getting a server by namespace. Used by various parts of store
65
+ # to define storage strategies. Also conserves memory so that there is only one instance of a Server per
66
+ # namespace.
67
+ #
68
+ # @param [String] Server Namespace
69
+ # @api private
70
+ def self.server( namespace=nil )
71
+ namespace ||= :aqua
72
+ namespace = namespace.to_sym unless namespace.class == Symbol
73
+ s = servers[ namespace ]
74
+ s = servers[namespace.to_sym] = Server.new( :namespace => namespace ) unless s
75
+ s
76
+ end
77
+
78
+ # Clears the cached servers. So far this is most useful for testing.
79
+ # API will depend on usefulness outside this.
80
+ #
81
+ # @api private
82
+ def self.clear_servers
83
+ @servers = {}
84
+ end
85
+
86
+
87
+ # TEXT HELPERS ================================================
88
+
89
+ # This comes from the CouchRest Library and its licence applies.
90
+ # It is included in this library as LICENCE_COUCHREST.
91
+ # The method breaks the parameters into a url query string.
92
+ #
93
+ # @param [String] The base url upon which to attach query params
94
+ # @param [optional Hash] A series of key value pairs that define the url query params
95
+ # @api semi-public
96
+ def self.paramify_url( url, params = {} )
97
+ if params && !params.empty?
98
+ query = params.collect do |k,v|
99
+ v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
100
+ "#{k}=#{CGI.escape(v.to_s)}"
101
+ end.join("&")
102
+ url = "#{url}?#{query}"
103
+ end
104
+ url
105
+ end
106
+
107
+ # A convenience method for escaping a string,
108
+ # namespaced classes with :: notation will be converted to __
109
+ # all other non-alpha numeric characters besides hyphens and underscores are removed
110
+ #
111
+ # @param [String] to be converted
112
+ # @return [String] converted
113
+ #
114
+ # @api private
115
+ def self.escape( str )
116
+ str.gsub!('::', '__')
117
+ str.gsub!(/[^a-z0-9\-_]/, '')
118
+ str
119
+ end
120
+
121
+ # DATABASE STRATEGIES ----------------------------------
122
+ # This library was built with to be flexible but have some sensible defaults. Database strategies is
123
+ # one of those areas. You can configure the CouchDB module to use one of three ways of managing data
124
+ # into databases:
125
+ # * :single - This is the default. It uses the CouchDB.server(:aqua) to build a single database where
126
+ # all the documents are stored.
127
+ # * :per_class - This strategy is the opposite of the single strategy in that each class has it's own
128
+ # database. This will make complex cross class lookups more difficult.
129
+ # * :configured - Each class configures its own database and server namespace. Any server not
130
+ # configured will default to the CouchDB.server. Any database not configured will default to the
131
+ # default database ... that set by the server namespace.
132
+ # TODO: store these strategies; give feedback to documents about the appropriate database.
133
+
134
+
135
+ # AUTOLOADING ---------
136
+ # auto loads the default http_adapter if Aqua gets used without configuring it first
137
+
138
+ class << self
139
+ def method_missing( method, *args )
140
+ if @adapter.nil?
141
+ set_http_adapter # loads up the adapter related stuff
142
+ send( method.to_sym, eval(args.map{|value| "'#{value}'"}.join(', ')) )
143
+ else
144
+ raise NoMethodError
145
+ end
146
+ end
147
+ end
148
+
149
+ end # CouchDB
150
+ end # Store
151
+ end # Aqua
@@ -0,0 +1,186 @@
1
+ require "base64"
2
+
3
+ module Aqua
4
+ module Store
5
+ module CouchDB
6
+ class Database
7
+ attr_reader :server, :name, :uri
8
+ attr_accessor :bulk_cache
9
+
10
+ # Create a CouchDB database representation from a name. Does not actually create a database on couchdb.
11
+ # It does not ensure that the database actually exists either. Just creates a ruby representation
12
+ # of a ruby database interface.
13
+ #
14
+ # @param [optional String] Name of database. If not provided server namespace will be used as database name.
15
+ # @param [optional Hash] Options for initialization. Currently the only option is :server which must be either a CouchDB server object or a symbol representing a server stored in the CouchDB module.
16
+ # @return [Database] The initialized object
17
+ #
18
+ # @api public
19
+ def initialize( name=nil, opts={})
20
+ name = nil if name && name.empty?
21
+ opts = Mash.new( opts ) unless opts.empty?
22
+ @name = name if name
23
+ initialize_server( opts[:server] )
24
+ @uri = "#{server.uri}/#{namespaced( name )}"
25
+ self.bulk_cache = []
26
+ end
27
+
28
+ # Initializes the database server with the option provided. If not option is provided the default CouchDB
29
+ # server is used instead.
30
+ #
31
+ # @param [Symbol, Server]
32
+ # If server_option argument is a Symbol then the CouchDB server stash will be queried for a matching
33
+ # server. CouchDB manages the creation of that server in the stash, if not found.
34
+ # If server_option argument is a Server object then then it is added directly to the database object.
35
+ # No management of the server will be done with CouchDB's server stash.
36
+ # @raise [ArgumentError] Raised if a server_option is passed in and if that option is neither a Hash nor a Symbol.
37
+ # @return [Server]
38
+ #
39
+ # @api private
40
+ def initialize_server( server_option )
41
+ if server_option
42
+ if server_option.class == Symbol
43
+ @server = CouchDB.server( server_option )
44
+ elsif server_option.class == Aqua::Store::CouchDB::Server
45
+ @server = server_option # WARNING: this won't get stashed in CouchDB for use with other database.
46
+ else
47
+ raise ArgumentError, ":server option must be a symbol identifying a CouchDB server, or a Server object"
48
+ end
49
+ else
50
+ @server = CouchDB.server
51
+ end
52
+ @server
53
+ end
54
+
55
+ # Namespaces the database path for the given server. If no name is provided, then the database name is
56
+ # just the Server's namespace.
57
+ #
58
+ # @param [String] Name that the database is initialized with, if any.
59
+ # @return [String] Namespaced database for use as a http path
60
+ #
61
+ # @api private
62
+ def namespaced( name )
63
+ if name
64
+ "#{server.namespace}_#{CouchDB.escape(@name)}"
65
+ else
66
+ server.namespace
67
+ end
68
+ end
69
+
70
+ # Creates a database representation and PUTs it on the CouchDB server.
71
+ # If successfull returns a database object. If not successful in creating
72
+ # the database on the CouchDB server then, false will be returned.
73
+ #
74
+ # @see Aqua::Store::CouchDB#initialize for option details
75
+ # @return [Database, false] Will return the database on success, and false if it did not succeed.
76
+ #
77
+ # @api pubilc
78
+ def self.create( name=nil, opts={} )
79
+ db = new(name, opts)
80
+ begin
81
+ CouchDB.put( db.uri )
82
+ rescue Exception => e # catch database already exists errors ...
83
+ unless e.message.match(/412/)
84
+ db = false
85
+ end
86
+ end
87
+ db
88
+ end
89
+
90
+ # Creates a database representation and PUTs it on the CouchDB server.
91
+ # This version of the #create method raises an error if the PUT request fails.
92
+ # The exception on this, is if the database already exists then the 412 HTTP code will be ignored.
93
+ #
94
+ # @see Aqua::Store::CouchDB#initialize for option details
95
+ # @return [Database] Will return the database on success.
96
+ # @raise HttpAdapter Exceptions depending on the reason for failure.
97
+ #
98
+ # @api pubilc
99
+ def self.create!( name=nil, opts={} )
100
+ db = new( name, opts )
101
+ begin
102
+ CouchDB.put( db.uri )
103
+ rescue Exception => e # catch database already exists errors ...
104
+ raise e unless e.class == RequestFailed && e.message.match(/412/)
105
+ end
106
+ db
107
+ end
108
+
109
+ # Checks to see if the database exists on the couchdb server.
110
+ #
111
+ # @return [true, false] depending on whether the database already exists in CouchDB land
112
+ #
113
+ # @api public
114
+ def exists?
115
+ begin
116
+ info
117
+ true
118
+ rescue CouchDB::ResourceNotFound
119
+ false
120
+ end
121
+ end
122
+
123
+ # GET the database info from CouchDB
124
+ def info
125
+ CouchDB.get( uri )
126
+ end
127
+
128
+ # Deletes a database; use with caution as this isn't reversible.
129
+ #
130
+ # @return A JSON response on success. nil if the resource is not found. And raises an error if another exception was raised
131
+ # @raise Exception related to request failure that is not a ResourceNotFound error.
132
+ def delete
133
+ begin
134
+ CouchDB.delete( uri )
135
+ rescue CouchDB::ResourceNotFound
136
+ nil
137
+ end
138
+ end
139
+
140
+ # Deletes a database; use with caution as this isn't reversible. Similar to #delete,
141
+ # except that it will raise an error on failure to find the database.
142
+ #
143
+ # @return A JSON response on success.
144
+ # @raise Exception related to request failure or ResourceNotFound.
145
+ def delete!
146
+ CouchDB.delete( uri )
147
+ end
148
+
149
+ # # Query the <tt>documents</tt> view. Accepts all the same arguments as view.
150
+ def documents(params = {})
151
+ keys = params.delete(:keys)
152
+ url = CouchDB.paramify_url( "#{uri}/_all_docs", params )
153
+ if keys
154
+ CouchDB.post(url, {:keys => keys})
155
+ else
156
+ CouchDB.get url
157
+ end
158
+ end
159
+
160
+ # Deletes all the documents in a given database
161
+ def delete_all
162
+ documents['rows'].each do |doc|
163
+ CouchDB.delete( "#{uri}/#{CGI.escape( doc['id'])}?rev=#{doc['value']['rev']}" ) #rescue nil
164
+ end
165
+ end
166
+
167
+ # BULK ACTIVITIES ------------------------------------------
168
+ def add_to_bulk_cache( doc )
169
+ if server.uuid_count/2.0 > bulk_cache.size
170
+ self.bulk_cache << doc
171
+ else
172
+ bulk_save
173
+ self.bulk_cache << doc
174
+ end
175
+ end
176
+
177
+ def bulk_save
178
+ docs = bulk_cache
179
+ self.bulk_cache = []
180
+ CouchDB.post( "#{uri}/_bulk_docs", {:docs => docs} )
181
+ end
182
+
183
+ end # Database
184
+ end # CouchDB
185
+ end # Store
186
+ end # Aqua