aqua 0.1.6 → 0.2.0

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 (37) hide show
  1. data/.gitignore +2 -1
  2. data/Aqua.gemspec +14 -11
  3. data/Rakefile +1 -1
  4. data/VERSION +1 -1
  5. data/lib/aqua.rb +5 -7
  6. data/lib/aqua/object/config.rb +2 -3
  7. data/lib/aqua/object/initializers.rb +309 -0
  8. data/lib/aqua/object/pack.rb +56 -132
  9. data/lib/aqua/object/query.rb +30 -2
  10. data/lib/aqua/object/stub.rb +60 -95
  11. data/lib/aqua/object/tank.rb +1 -0
  12. data/lib/aqua/object/translator.rb +313 -0
  13. data/lib/aqua/object/unpack.rb +26 -227
  14. data/lib/aqua/store/couch_db/couch_db.rb +1 -0
  15. data/lib/aqua/store/couch_db/database.rb +1 -1
  16. data/lib/aqua/store/couch_db/design_document.rb +126 -2
  17. data/lib/aqua/store/couch_db/result_set.rb +36 -0
  18. data/lib/aqua/store/couch_db/storage_methods.rb +182 -17
  19. data/lib/aqua/store/storage.rb +4 -48
  20. data/lib/aqua/support/mash.rb +2 -3
  21. data/lib/aqua/support/set.rb +4 -16
  22. data/spec/object/object_fixtures/array_udder.rb +1 -1
  23. data/spec/object/object_fixtures/persistent.rb +0 -2
  24. data/spec/object/pack_spec.rb +137 -517
  25. data/spec/object/query_spec.rb +36 -6
  26. data/spec/object/stub_spec.rb +10 -9
  27. data/spec/object/translator_packing_spec.rb +402 -0
  28. data/spec/object/translator_unpacking_spec.rb +262 -0
  29. data/spec/object/unpack_spec.rb +162 -320
  30. data/spec/spec_helper.rb +18 -0
  31. data/spec/store/couchdb/design_document_spec.rb +148 -7
  32. data/spec/store/couchdb/result_set_spec.rb +95 -0
  33. data/spec/store/couchdb/storage_methods_spec.rb +150 -10
  34. metadata +13 -9
  35. data/lib/aqua/support/initializers.rb +0 -216
  36. data/spec/object/object_fixtures/grounded.rb +0 -13
  37. data/spec/object/object_fixtures/sugar.rb +0 -4
@@ -3,6 +3,7 @@ require File.dirname(__FILE__) + '/server'
3
3
  require File.dirname(__FILE__) + '/database'
4
4
  require File.dirname(__FILE__) + '/attachments'
5
5
  require File.dirname(__FILE__) + '/storage_methods'
6
+ require File.dirname(__FILE__) + '/result_set'
6
7
  require File.dirname(__FILE__) + '/design_document'
7
8
 
8
9
  module Aqua
@@ -146,7 +146,7 @@ module Aqua
146
146
  CouchDB.delete( uri )
147
147
  end
148
148
 
149
- # # Query the <tt>documents</tt> view. Accepts all the same arguments as view.
149
+ # Query the <tt>documents</tt> view. Accepts all the same arguments as view.
150
150
  def documents(params = {})
151
151
  keys = params.delete(:keys)
152
152
  url = CouchDB.paramify_url( "#{uri}/_all_docs", params )
@@ -31,8 +31,16 @@ module Aqua
31
31
  def initialize( hash={} )
32
32
  hash = Mash.new( hash ) unless hash.empty?
33
33
  self.id = hash.delete(:name) if hash[:name]
34
- document_initialize( hash )
35
- end
34
+ document_initialize( hash ) # TODO: can't this just be a call to super?
35
+ end
36
+
37
+ def do_rev( hash )
38
+ # TODO: This is a temp hack to deal with loading the right revision number so a design doc
39
+ # can be updated from the document. Without this hack, the rev is nil, and there is a conflict.
40
+
41
+ hash.delete(:rev) # This is omited to aleviate confusion
42
+ # hash.delete(:_rev) # CouchDB determines _rev attribute
43
+ end
36
44
 
37
45
  # couchdb database url for the design document
38
46
  # @return [String] representing CouchDB uri for document
@@ -49,6 +57,122 @@ module Aqua
49
57
  def update_version( result )
50
58
  self.id = result['id'].gsub(/\A_design\//, '')
51
59
  self.rev = result['rev']
60
+ end
61
+
62
+ # Gets a design document by name.
63
+ # @param [String] Id/Name of design document
64
+ # @return [Aqua::Store::CouchDB::DesignDocument]
65
+ # @api public
66
+ def self.get( name )
67
+ design = CouchDB.get( "#{database.uri}/_design/#{CGI.escape(name)}" )
68
+ new( design )
69
+ end
70
+
71
+ # VIEWS --------------------
72
+
73
+ # An array of indexed views for the design document.
74
+ # @return [Array]
75
+ # @api public
76
+ def views
77
+ self[:views] ||= Mash.new
78
+ end
79
+
80
+ # Adds or updates a view with the given options
81
+ #
82
+ # @param [String, Hash] Name of the view, or options hash
83
+ # @option arg [String] :name The view name, required
84
+ # @option arg [String] :map Javascript map function, optional
85
+ # @option arg [String] :reduce Javascript reduce function, optional
86
+ #
87
+ # @return [Mash] Map/Reduce mash of javascript functions
88
+ #
89
+ # @example
90
+ # design_doc << 'attribute_name'
91
+ # design_doc << {:name => 'attribute_name', :map => 'function(doc){ ... }'}
92
+ #
93
+ # @api public
94
+ def <<( arg )
95
+ # handle different argument options
96
+ if [String, Symbol].include?( arg.class )
97
+ view_name = arg
98
+ opts = {}
99
+ elsif arg.class.ancestors.include?( Hash )
100
+ opts = Mash.new( arg )
101
+ view_name = opts.delete( :name )
102
+ raise ArgumentError, 'Option must include a :name that is the view\'s name' unless view_name
103
+ else
104
+ raise ArgumentError, "Must be a string or Hash like object of options"
105
+ end
106
+
107
+ # build the map/reduce query
108
+ map = opts[:map]
109
+ reduce = opts[:reduce]
110
+ views # to initialize self[:views]
111
+ self[:views][view_name] = {
112
+ :map => map || build_map( view_name, opts[:class_constraint] ),
113
+ }
114
+ self[:views][view_name][:reduce] = reduce if reduce
115
+ self[:views][view_name]
116
+ end
117
+
118
+ alias :add :<<
119
+
120
+ def add!( arg )
121
+ self << arg
122
+ save!
123
+ end
124
+
125
+ private
126
+ # Builds a generic map assuming that the view_name is the name of a document attribute.
127
+ # @param [String, Symbol] Name of document attribute
128
+ # @param [Class, String] Optional constraint on to limit view to a given class
129
+ # @return [String] Javascript map function
130
+ #
131
+ # @api private
132
+ def build_map( view_name, class_constraint=nil )
133
+ class_constraint = if class_constraint.class == Class
134
+ " && doc['type'] == '#{class_constraint}'"
135
+ elsif class_constraint.class == String
136
+ " && #{class_constraint}"
137
+ end
138
+ "function(doc) {
139
+ if( doc['#{view_name}'] #{class_constraint}){
140
+ emit( doc['#{view_name}'], 1 );
141
+ }
142
+ }"
143
+ end
144
+ public
145
+
146
+ # group=true Version 0.8.0 and forward
147
+ # group_level=int
148
+ # reduce=false Trunk only (0.9)
149
+
150
+ def query( view_name, opts={} )
151
+ opts = Mash.new( opts ) unless opts.empty?
152
+ doc_class = opts[:document_class]
153
+
154
+ params = []
155
+ params << 'include_docs=true' unless (opts[:select] && opts[:select] != 'all')
156
+ # TODO: this is according to couchdb really inefficent with large sets of data.
157
+ # A better way would involve, using start and end keys with limit. But this
158
+ # is a really hard one to figure with jumping around to different pages
159
+ params << "skip=#{opts[:offset]}" if opts[:offset]
160
+ params << "limit=#{opts[:limit]}" if opts[:limit]
161
+ params << "key=#{opts[:equals]}" if opts[:equals]
162
+ if opts[:order].to_s == 'desc' || opts[:order].to_s == 'descending'
163
+ desc = true
164
+ params << "descending=true"
165
+ end
166
+ if opts[:range] && opts[:range].size == 2
167
+ params << "startkey=#{opts[:range][desc == true ? 1 : 0 ]}"
168
+ params << "endkey=#{opts[:range][desc == true ? 0 : 1]}"
169
+ end
170
+
171
+ query_uri = "#{uri}/_view/#{CGI.escape(view_name.to_s)}?"
172
+ query_uri << params.join('&')
173
+
174
+ result = CouchDB.get( query_uri )
175
+ opts[:reduced] ? result['rows'].first['value'] : ResultSet.new( result, doc_class )
52
176
  end
53
177
 
54
178
  end
@@ -0,0 +1,36 @@
1
+ module Aqua
2
+ module Store
3
+ module CouchDB
4
+ class ResultSet < Array
5
+
6
+ def self.document_class
7
+ @document_class
8
+ end
9
+
10
+ def self.document_class=( klass )
11
+ @document_class = klass
12
+ end
13
+
14
+ attr_accessor :offset, :total, :rows, :document_class
15
+
16
+ def initialize( response, doc_class=nil )
17
+ self.document_class = doc_class || self.class.document_class
18
+ self.total = response['total_rows']
19
+ self.offset = response['offset']
20
+ self.rows = response['rows']
21
+ results = if rows && rows.first && rows.first['doc']
22
+ if document_class
23
+ rows.collect{ |h| document_class.new( h['doc'] ) }
24
+ else
25
+ rows.collect{ |h| h['doc'] }
26
+ end
27
+ else
28
+ rows.collect{ |h| h['key'] }
29
+ end
30
+ super( results )
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -64,14 +64,33 @@ module Aqua
64
64
  @database = db
65
65
  end
66
66
 
67
- # gets a document from the database based on id
67
+ # Gets a document from the database based on id
68
68
  # @param [String] id
69
69
  # @return [Hash] representing the CouchDB data
70
70
  # @api public
71
71
  def get( id )
72
- new( CouchDB.get( "#{database.uri}/#{CGI.escape(id)}" ) )
72
+ resource = begin # this is just in case the developer has already escaped the name
73
+ CouchDB.get( "#{database.uri}/#{CGI.escape(id)}" )
74
+ rescue
75
+ CouchDB.get( "#{database.uri}/#{id}" )
76
+ end
77
+ new( resource )
73
78
  end
74
79
 
80
+ # Will find a document by id, or create it if it doesn't exist. Alias is :get!
81
+ # @param [String] id
82
+ # @return [Hash] representing the CouchDB resource
83
+ # @api public
84
+ def find_or_create( id )
85
+ begin
86
+ get( id )
87
+ rescue
88
+ create!( :id => id )
89
+ end
90
+ end
91
+
92
+ alias :get! :find_or_create
93
+
75
94
  # Retrieves an attachment when provided the document id and attachment id, or the combined id
76
95
  #
77
96
  # @return [Tempfile]
@@ -81,10 +100,149 @@ module Aqua
81
100
  new( :id => document_id ).attachments.get!( attachment_id )
82
101
  end
83
102
 
84
- # Creates basic map reduce view for a given field
103
+ # Accessor for maintaining connection to aquatic class
104
+ # @api private
105
+ attr_accessor :parent_class
106
+ alias :design_name= :parent_class=
107
+ alias :design_name :parent_class
108
+
109
+ # Finds or creates design document based on aqua parent class name
110
+ # @api semi-private
111
+ def design_document( reload=false )
112
+ @design_document = nil if reload
113
+ @design_document ||= design_name ? DesignDocument.find_or_create( design_name ) : nil
114
+ end
115
+
116
+ # Stores stores a map name for a given index, allowing the same map
117
+ # to be used for various reduce functions. This means only one index is created.
118
+ # @param [String] field being indexed, or the sub field
119
+ # @param [Hash, Mash] Hash of functions used to create views
120
+ # @option opts [String] :map Javascript/CouchDB map function
121
+ # @option opts [String] :reduce Javascript/CouchDB reduce function
122
+ #
123
+ # @api public
85
124
  def index_on( field, opts={} )
86
-
87
- end
125
+ opts = Mash.new( opts )
126
+ design_document(true).add!( opts.merge!(:name => field) )
127
+ unless indexes.include?( field )
128
+ indexes << field.to_sym
129
+ indexes << field.to_s
130
+ end
131
+ self
132
+ end
133
+
134
+ # This is an aqua specific indexer whereas index_on is a more generic CouchDB indexer.
135
+ # This method seeks out the designated ivar in an Aqua structured document. Future iterations
136
+ # should go deeper into the ivar to reduce the overall size of the index, and make for more
137
+ # usable searches.
138
+ #
139
+ # @api private
140
+ def index_on_ivar( field )
141
+ index_on( field,
142
+ :map => "
143
+ function(doc) {
144
+ if( doc['class'] == '#{parent_class}' &&
145
+ doc['ivars'] && doc['ivars']['@#{field}'] ){
146
+ emit( doc['ivars']['@#{field}'], 1 );
147
+ }
148
+ }
149
+ "
150
+ )
151
+ end
152
+
153
+ # A list of index names that can be used to build other reduce functions.
154
+ # @api semi-private
155
+ def indexes
156
+ @indexes ||= []
157
+ end
158
+
159
+ # @api public
160
+ def query( index, opts={} )
161
+ raise ArgumentError, 'Index not found' unless views.include?( index.to_s )
162
+ opts = Mash.new(opts)
163
+ opts.merge!(:document_class => self) unless opts[:document_class]
164
+ opts.merge!(:reduced => design_document.views[index][:reduce] ? true : false )
165
+ design_document.query( index, opts )
166
+ end
167
+
168
+ def reduced_query( reduce_type, index, opts)
169
+ view = "#{index}_#{reduce_type}"
170
+ unless views.include?( view )
171
+ design_document(true).add!(
172
+ :name => view,
173
+ :map => design_document.views[ index.to_s ][:map],
174
+ :reduce => opts[:reduce]
175
+ )
176
+ end
177
+ query( view, opts.merge!( :select => "index only" ) )
178
+ end
179
+
180
+ # @api semi-private
181
+ def views
182
+ design_document.views.keys
183
+ end
184
+
185
+ # @api public
186
+ def count( index, opts={} )
187
+ opts = Mash.new(opts)
188
+ opts[:reduce] = "
189
+ function (key, values, rereduce) {
190
+ return sum(values);
191
+ }" unless opts[:reduce]
192
+ reduced_query(:count, index, opts)
193
+ end
194
+
195
+ # @api public
196
+ def sum( index, opts={} )
197
+ opts = Mash.new(opts)
198
+ opts[:reduce] = "
199
+ function (keys, values, rereduce) {
200
+ var key_values = []
201
+ keys.forEach( function(key) {
202
+ key_values[key_values.length] = key[0]
203
+ });
204
+ return sum( key_values );
205
+ }" unless opts[:reduce]
206
+ reduced_query(:sum, index, opts)
207
+ end
208
+
209
+ def average( index, opts={} )
210
+ sum(index, opts) / count(index, opts).to_f
211
+ end
212
+
213
+ alias :avg :average
214
+
215
+ def min( index, opts={} )
216
+ opts = Mash.new(opts)
217
+ opts[:reduce] = "
218
+ function (keys, values, rereduce) {
219
+ var key_values = []
220
+ keys.forEach( function(key) {
221
+ key_values[key_values.length] = key[0]
222
+ });
223
+ return Math.min.apply( Math, key_values ); ;
224
+ }" unless opts[:reduce]
225
+ reduced_query(:min, index, opts)
226
+ end
227
+
228
+ alias :minimum :min
229
+
230
+ def max( index, opts={} )
231
+ opts = Mash.new(opts)
232
+ opts[:reduce] = "
233
+ function (keys, values, rereduce) {
234
+ var key_values = []
235
+ keys.forEach( function(key) {
236
+ key_values[key_values.length] = key[0]
237
+ });
238
+ return Math.max.apply( Math, key_values ); ;
239
+ }" unless opts[:reduce]
240
+ reduced_query(:max, index, opts)
241
+ end
242
+
243
+ alias :maximum :max
244
+
245
+
88
246
  end
89
247
 
90
248
  module InstanceMethods
@@ -96,17 +254,23 @@ module Aqua
96
254
  # @api public
97
255
  def initialize( hash={} )
98
256
  hash = Mash.new( hash ) unless hash.empty?
99
- self.id = hash.delete(:id) if hash[:id]
100
-
101
- # ignore these keys
102
- hash.delete(:rev) # This is omited to aleviate confusion
103
- hash.delete(:_rev) # CouchDB determines _rev attribute
104
- hash.delete(:_id) # this is set via by the id=(value) method
105
- # TODO: have to deal with attachments as well
257
+
258
+ hashed_id = hash.delete(:id) || hash.delete(:_id)
259
+ self.id = hashed_id if hashed_id
106
260
 
261
+ do_rev( hash )
262
+
107
263
  # feed the rest of the hash to the super
108
264
  super( hash )
109
- end
265
+ end
266
+
267
+ # Temporary hack to allow design document refresh from within a doc.
268
+ # @todo The get method has to handle rev better!!!
269
+ def do_rev( hash, override=false )
270
+ hash.delete(:rev) # This is omited to aleviate confusion
271
+ # CouchDB determines _rev attribute on saving, but when #new is loading json passed from the
272
+ # database rev needs to be added to the class. So, the :_rev param is not being deleted anymore
273
+ end
110
274
 
111
275
  # Saves an Aqua::Storage instance to CouchDB as a document. Save can be deferred for bulk saving.
112
276
  #
@@ -191,8 +355,10 @@ module Aqua
191
355
  # @return [Hash] representing the CouchDB data
192
356
  # @api public
193
357
  def retrieve
194
- self.class.new( CouchDB.get( uri ) )
195
- end
358
+ self.class.get( id )
359
+ end
360
+
361
+ alias :reload :retrieve
196
362
 
197
363
  # reloads self from CouchDB database
198
364
  # @return [Hash] representing CouchDB data
@@ -395,8 +561,7 @@ module Aqua
395
561
  # @api public
396
562
  def attachments
397
563
  @attachments ||= Attachments.new( self )
398
- end
399
-
564
+ end
400
565
  end # InstanceMethods
401
566
 
402
567
  end # StoreMethods
@@ -1,58 +1,14 @@
1
- # This is the interface between an individual object and the Storage representation.
2
- # The storage engine should provide a module that gets included into the Mash. The
3
- # module should minimally provide this interface to the object:
4
- #
5
- # InstanceMethods
6
- # @inteface_level optional If no initialization is included. Then the Mash initialization will be used.
7
- # initialize( hash )
8
- # @param [optional Hash]
9
- # @return [Aqua::Storage] the new storage instance
10
- #
11
- # @interface_level mandatory
12
- # commit
13
- # @return [Aqua::Storage] saved storage object
14
- # @raise [Aqua::ObjectNotFound] if another error occurs in the engine, that should be raised instead
15
- # any of the Aqua Exceptions
16
- #
17
- # @interface_level mandatory
18
- # id
19
- # @param none
20
- # @return id object, whether String, Fixnum or other object as the store chooses
21
- #
22
- # @interface_level mandatory
23
- # id=( custom_id )
24
- # The library expects to save an object with a custom id. Id= method can set limits on the
25
- # types of objects that can be used as an id. Minimally it should support Strings.
26
- # @param String, Fixnum, or any other reasonable class
27
- #
28
- # @interface_level mandatory
29
- # new?
30
- # The equivalent of AR's new_record? and can be used to set create hooks or determine how to handle
31
- # object queries about whether it has changed.
32
- # @return [true, false]
33
- #
34
- # ClassMethods
35
- # @interface_level mandatory
36
- # load( id, class )
37
- # The'load'
38
- # @param [String, Fixnum] The id used by the system to
39
- # @return [Aqua::Storage]
40
- # @raise [Aqua::ResourceNotFound] if another error occurs in the engine, that should be raised instead
41
- # any of the Aqua Exceptions
42
- #
43
- # Other methods used for the storage engine can be added as needed by the engine.
44
- #
45
- # If no storage engine is configured before this class is used, CouchDB will automatically be used
46
- # as an engine.
1
+ # When the api using CouchDB is stable, an interface and tests need to be created so that aqua can be extended
2
+ # to other storage engines.
47
3
  module Aqua
48
4
  class Storage < Mash
49
5
  # auto loads the default store to CouchDB if Store is used without Aqua configuration of a store
50
6
  def method_missing( method, *args )
51
7
  if respond_to?( :commit )
52
- raise NoMethodError
8
+ raise NoMethodError, "#{method} undefined for #{self.inspect}"
53
9
  else
54
10
  Aqua.set_storage_engine # to default, currently CouchDB
55
- send( method.to_sym, eval(args.map{|value| "'#{value}'"}.join(', ')) ) # resend!
11
+ send( method.to_sym, *args ) # resend!
56
12
  end
57
13
  end
58
14
  end # Storage