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
@@ -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