aqua 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -9,6 +9,24 @@ def require_fixtures
9
9
  Dir[ File.dirname(__FILE__) + "/object/object_fixtures/**/*.rb" ].each do |file|
10
10
  require file
11
11
  end
12
+ end
13
+
14
+ def build_user_ivars
15
+ @time = Time.now
16
+ @date = Date.parse('12/23/1969')
17
+ @message = "Hello World! This is a log entry"
18
+ @log = Log.new( :message => @message, :created_at => @time ) # embedded object
19
+ @other_user = User.new( :username => 'strictnine', :name => ['What', 'Ever'] ) # stubbed objects
20
+ @user = User.new(
21
+ :username => 'kane',
22
+ :name => ['Kane', 'Baccigalupi'],
23
+ :dob => @date,
24
+ :created_at => @time,
25
+ :log => @log,
26
+ :password => 'my secret!',
27
+ :other_user => @other_user
28
+ )
29
+ @pack = @user._pack
12
30
  end
13
31
 
14
32
 
@@ -7,18 +7,18 @@ CouchDB = Aqua::Store::CouchDB unless defined?( CouchDB )
7
7
  Database = CouchDB::Database unless defined?( Database )
8
8
  Server = CouchDB::Server unless defined?( Server )
9
9
  Design = CouchDB::DesignDocument unless defined?( Design )
10
+ ResultSet = CouchDB::ResultSet unless defined?( ResultSet )
11
+
12
+ require File.dirname(__FILE__) + '/fixtures_and_data/document_fixture' # Document ... a Mash with the collection of methods
10
13
 
11
14
  describe CouchDB::DesignDocument do
12
15
  before(:each) do
13
16
  Aqua::Storage.database.delete_all
17
+ @name = 'User'
18
+ @design = Design.new(:name => @name)
14
19
  end
15
20
 
16
21
  describe 'new and create' do
17
- before(:each) do
18
- @name = 'User'
19
- @design = Design.new(:name => @name)
20
- end
21
-
22
22
  it 'should require a name to build the uri' do
23
23
  design = Design.new
24
24
  lambda{ design.uri }.should raise_error
@@ -35,9 +35,150 @@ describe CouchDB::DesignDocument do
35
35
  end
36
36
  end
37
37
 
38
+ it 'should get a design document by name' do
39
+ @design.save!
40
+ lambda{ Design.get( @name ) }.should_not raise_error
41
+ end
42
+
38
43
  describe 'views' do
44
+ before(:each) do
45
+ ResultSet.document_class = Document
46
+ end
47
+
48
+ it 'should be a Hash-like object' do
49
+ @design.views.should == Mash.new
50
+ end
39
51
 
40
- end
41
-
52
+ describe '<<, add, add!' do
53
+ describe 'string as argument' do
54
+ it 'should add a view with the right name' do
55
+ @design << 'my_attribute'
56
+ @design.views.keys.should == ['my_attribute']
57
+ end
58
+
59
+ it 'should autogenerate a generic map' do
60
+ @design << 'my_attribute'
61
+ @design.views[:my_attribute][:map].should match(/function\(doc\)/)
62
+ @design.views[:my_attribute][:map].should match(/emit/)
63
+ end
64
+
65
+ it 'should not autogenerate a reduce function' do
66
+ @design << 'my_attribute'
67
+ @design.views[:my_attribute][:reduce].should be_nil
68
+ end
69
+ end
70
+
71
+ describe 'hash options as argument' do
72
+ it 'should add a view named with the options name key' do
73
+ @design << {:name => 'my_attribute'}
74
+ @design.views.keys.should == ['my_attribute']
75
+ end
76
+
77
+ it 'should autogenerate a generic map' do
78
+ @design << {:name => 'my_attribute'}
79
+ @design.views[:my_attribute][:map].should match(/function\(doc\)/)
80
+ @design.views[:my_attribute][:map].should match(/emit/)
81
+ end
82
+
83
+ it 'should autogenerate a generic map with class constraints' do
84
+ @design << {:name => 'my_docs', :class_constraint => Document}
85
+ @design.views[:my_docs][:map].should match(/doc\['type'\] == 'Document'/)
86
+ end
87
+
88
+ it 'should autogenerate a generic map and insert preformed class constraints' do
89
+ @design << {:name => 'user', :class_constraint => "doc['class'] == 'User'" }
90
+ @design.views[:user][:map].should match(/doc\['class'\] == 'User'/)
91
+ end
92
+
93
+ it 'should not autogenerate a reduce function' do
94
+ @design << {:name => 'my_attribute'}
95
+ @design.views[:my_attribute][:reduce].should be_nil
96
+ end
97
+
98
+ it 'should apply a map option when provided' do
99
+ @design << {:name => 'my_attribute', :map => 'not the generic'}
100
+ @design.views[:my_attribute][:map].should == 'not the generic'
101
+ end
102
+
103
+ it 'should apply a reduce option when provided' do
104
+ @design << {:name => 'my_attribute', :reduce => 'I exist!'}
105
+ @design.views[:my_attribute][:map].should match(/function\(doc\)/)
106
+ @design.views[:my_attribute][:map].should match(/emit/)
107
+ @design.views[:my_attribute][:reduce].should == 'I exist!'
108
+ end
109
+ end
110
+
111
+ it 'add should act like <<' do
112
+ @design.add :name => 'my_attribute', :map => 'not the generic'
113
+ @design.views[:my_attribute][:map].should == 'not the generic'
114
+ end
115
+
116
+ it 'add! should save after adding the view' do
117
+ @design.add! :name => 'my_attribute', :map => 'not the generic'
118
+ lambda{ Design.get( @design.name ) }.should_not raise_error
119
+ design = Design.get( @design.name )
120
+ design.views.keys.should include( 'my_attribute' )
121
+ end
122
+ end
42
123
 
124
+ describe 'query' do
125
+ before(:each) do
126
+ (1..10).each do |num|
127
+ Document.create!( :index => num )
128
+ end
129
+ @design << :index
130
+ @design.save!
131
+ end
132
+
133
+ it 'should query by saved view' do
134
+ lambda{ @design.query( :index ) }.should_not raise_error
135
+ end
136
+
137
+ it 'should return a number of rows corresponding to all the documents in the query' do
138
+ @docs = @design.query( :index )
139
+ @docs.size.should == 10
140
+ end
141
+
142
+ it 'should return the documents themselves by default' do
143
+ @docs = @design.query( :index )
144
+ @docs.first.keys.should include( 'index' )
145
+ @docs.first.class.should == Document
146
+ end
147
+
148
+ it 'should limit query results' do
149
+ @docs = @design.query( :index, :limit => 2 )
150
+ @docs.size.should == 2
151
+ end
152
+
153
+ it 'should offset query results' do
154
+ @docs = @design.query( :index, :limit => 2, :offset => 2)
155
+ @docs.size.should == 2
156
+ @docs.first[:index].should == 3
157
+ end
158
+
159
+ it 'should put in descending order' do
160
+ @docs = @design.query( :index, :order => :desc )
161
+ @docs.first[:index].should == 10
162
+ end
163
+
164
+ it 'should select a range' do
165
+ @docs = @design.query( :index, :range => [2,4])
166
+ @docs.size.should == 3
167
+ @docs.first[:index].should == 2
168
+ end
169
+
170
+ it 'should select a range with descending order' do
171
+ @docs = @design.query( :index, :range => [2,4], :order => :descending )
172
+ @docs.size.should == 3
173
+ @docs.first[:index].should == 4
174
+ @docs[2][:index].should == 2
175
+ end
176
+
177
+ it 'should select by exact key' do
178
+ @docs = @design.query( :index, :equals => 3 )
179
+ @docs.size.should == 1
180
+ @docs.first[:index].should == 3
181
+ end
182
+ end
183
+ end
43
184
  end
@@ -0,0 +1,95 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ Aqua.set_storage_engine('CouchDB') # to initialize the Aqua::Store namespace
4
+
5
+ # Conveniences for typing with tests ...
6
+ CouchDB = Aqua::Store::CouchDB unless defined?( CouchDB )
7
+ Database = CouchDB::Database unless defined?( Database )
8
+ Design = CouchDB::DesignDocument unless defined?( Design )
9
+ ResultSet = CouchDB::ResultSet unless defined?( ResultSet )
10
+
11
+ require File.dirname(__FILE__) + '/fixtures_and_data/document_fixture' # Document ... a Mash with the collection of methods
12
+ class Docintalk < Document
13
+ def talk
14
+ 'Hello, I am a Docintalk'
15
+ end
16
+ end
17
+
18
+ describe ResultSet do
19
+ before(:each) do
20
+ ResultSet.document_class = nil # resets for all theses tests
21
+ # These are sample returns from couchdb for a view query
22
+ # the first is when the include_document=true is omitted
23
+ # the next is when it is included
24
+ @no_docs = {
25
+ "rows"=>[
26
+ {"id"=>"b7e4623f506c437bae2b517d169b3c88", "value"=>nil, "key"=>1},
27
+ {"id"=>"43d650760fdda785a3bd20056d53b3e0", "value"=>nil, "key"=>2},
28
+ {"id"=>"b638586d7d71267e54af9420ee803a7c", "value"=>nil, "key"=>3},
29
+ {"id"=>"c0f8a9d683fb1576dc1da943d9bf2251", "value"=>nil, "key"=>4},
30
+ {"id"=>"11d12976059170f360df222bc097834c", "value"=>nil, "key"=>5},
31
+ {"id"=>"d9270a9bdc7128f02c4b9ccdc10e6b18", "value"=>nil, "key"=>6},
32
+ {"id"=>"c85c7d08e240c1798a7db11b0756581c", "value"=>nil, "key"=>7},
33
+ {"id"=>"721ec0ec55c5d16719ae438855528575", "value"=>nil, "key"=>8},
34
+ {"id"=>"13d2140c707196af4b4e53fe73a0ab0b", "value"=>nil, "key"=>9},
35
+ {"id"=>"b139683cf575446edb4245eba245d907", "value"=>nil, "key"=>10}
36
+ ],
37
+ "offset"=>0,
38
+ "total_rows"=>10
39
+ }
40
+ @with_docs = {
41
+ "rows"=>[
42
+ {"doc"=>{"_id"=>"9d749a8a532294c22ccbf16873f50ead", "_rev"=>"1-567432465", "index"=>2}, "id"=>"9d749a8a532294c22ccbf16873f50ead", "value"=>nil, "key"=>1},
43
+ {"doc"=>{"_id"=>"b1394da1469eae862cca661da238b951", "_rev"=>"1-2128259075", "index"=>3}, "id"=>"b1394da1469eae862cca661da238b951", "value"=>nil, "key"=>2},
44
+ {"doc"=>{"_id"=>"a1b7b978b20649cb5fd648cd510e0243", "_rev"=>"1-3326711422", "index"=>4}, "id"=>"a1b7b978b20649cb5fd648cd510e0243", "value"=>nil, "key"=>3},
45
+ {"doc"=>{"_id"=>"92b69da432d658da6dc69a5c611065ca", "_rev"=>"1-2590331142", "index"=>5}, "id"=>"92b69da432d658da6dc69a5c611065ca", "value"=>nil, "key"=>4},
46
+ {"doc"=>{"_id"=>"f3b257b6d9bc60062147db98175a809b", "_rev"=>"1-2121844908", "index"=>6}, "id"=>"f3b257b6d9bc60062147db98175a809b", "value"=>nil, "key"=>5},
47
+ ],
48
+ "offset"=>1,
49
+ "total_rows"=>5
50
+ }
51
+ @docless = ResultSet.new( @no_docs )
52
+ @docfull = ResultSet.new( @with_docs )
53
+ end
54
+
55
+ it 'should have a default document class accessor' do
56
+ ResultSet.should respond_to(:document_class)
57
+ ResultSet.should respond_to(:document_class=)
58
+ end
59
+
60
+ it 'should have an offset' do
61
+ @docless.offset.should == 0
62
+ @docfull.offset.should == 1
63
+ end
64
+
65
+ it 'should have a total' do
66
+ @docless.total.should == 10
67
+ @docfull.total.should == 5
68
+ end
69
+
70
+ it 'should have rows' do
71
+ @docless.rows.should == @no_docs['rows']
72
+ @docfull.rows.should == @with_docs['rows']
73
+ end
74
+
75
+ it 'keys should be the array accessible content of the set when docs are not included' do
76
+ @docless.first.should == @no_docs['rows'].first['key']
77
+ end
78
+
79
+ it 'docs should be the array accessible content of the set, when available' do
80
+ @docfull.first.should == @with_docs['rows'].first['doc']
81
+ end
82
+
83
+ it 'it should convert docs to the default document class if no instance level document class is available' do
84
+ ResultSet.document_class = Document
85
+ docs = ResultSet.new( @with_docs )
86
+ docs.first.class.should == Document
87
+ end
88
+
89
+ it 'should use instance level document class when available' do
90
+ docs = ResultSet.new( @with_docs, Docintalk )
91
+ docs.document_class.should == Docintalk
92
+ docs.first.class.should == Docintalk
93
+ end
94
+
95
+ end
@@ -4,10 +4,10 @@ Aqua.set_storage_engine('CouchDB') # to initialize the Aqua::Store namespace
4
4
  require File.dirname(__FILE__) + '/fixtures_and_data/document_fixture' # Document ... a Mash with the collection of methods
5
5
 
6
6
  # Conveniences for typing with tests ...
7
- CouchDB = Aqua::Store::CouchDB unless defined?( CouchDB )
8
- Database = CouchDB::Database unless defined?( Database )
9
- Server = CouchDB::Server unless defined?( Server)
10
- Attachments = CouchDB::Attachments unless defined?( Attachments )
7
+ CouchDB = Aqua::Store::CouchDB unless defined?( CouchDB )
8
+ Database = CouchDB::Database unless defined?( Database )
9
+ Server = CouchDB::Server unless defined?( Server)
10
+ Attachments = CouchDB::Attachments unless defined?( Attachments )
11
11
 
12
12
  describe 'CouchDB::StorageMethods' do
13
13
  before(:each) do
@@ -17,6 +17,9 @@ describe 'CouchDB::StorageMethods' do
17
17
  :more => "my big stuff"
18
18
  }
19
19
  @doc = Document.new( @params )
20
+ # the line below is neccessary in the full suite, but not when the file is run on it's own ??
21
+ CouchDB.put( 'http://127.0.0.1:5984/aqua' ) unless @doc.class.database.exists?
22
+ @doc.class.database.delete_all
20
23
  end
21
24
 
22
25
  describe 'initialization' do
@@ -148,10 +151,6 @@ describe 'CouchDB::StorageMethods' do
148
151
  end
149
152
 
150
153
  describe 'save/create' do
151
- before(:each) do
152
- @doc.delete if @doc.exists?
153
- end
154
-
155
154
  it 'saving should create a document in the database' do
156
155
  @doc.save
157
156
  lambda{ Aqua::Store::CouchDB.get( @doc.uri ) }.should_not raise_error
@@ -213,9 +212,43 @@ describe 'CouchDB::StorageMethods' do
213
212
  @doc[:noodle] = 'spaghetti'
214
213
  @doc.reload
215
214
  @doc[:noodle].should be_nil
215
+ end
216
+
217
+ it 'should create using find_or_create' do
218
+ lambda{ Document.get( @params[:id] ) }.should raise_error
219
+ doc = Document.find_or_create( @params[:id] )
220
+ lambda{ Document.get( @params[:id] ) }.should_not raise_error
216
221
  end
217
222
  end
218
223
 
224
+ describe 'getting' do
225
+ it 'should get a document from its id' do
226
+ @doc.save!
227
+ lambda{ Document.get( @doc.id ) }.should_not raise_error
228
+ end
229
+
230
+ it 'returned document should have an id' do
231
+ @doc.save!
232
+ document = Document.get( @doc.id )
233
+ document.id.should == @doc.id
234
+ end
235
+
236
+ it 'returned document should have an id even if not explicitly set' do
237
+ doc = Document.new(:this => 'that')
238
+ doc.save!
239
+ retrieved = Document.get( doc.id )
240
+ retrieved.id.should_not be_nil
241
+ retrieved.id.should_not be_empty
242
+ end
243
+
244
+ it 'returned document should have a rev' do
245
+ @doc.save!
246
+ document = Document.get( @doc.id )
247
+ document.rev.should_not be_nil
248
+ end
249
+
250
+ end
251
+
219
252
  describe 'deleting' do
220
253
  before(:each) do
221
254
  @doc.delete if @doc.exists?
@@ -401,8 +434,115 @@ describe 'CouchDB::StorageMethods' do
401
434
 
402
435
  end
403
436
 
404
- describe 'indexing/views' do
405
- it 'should '
437
+ describe 'design document' do
438
+ it 'should not have a design document if there is no design_name' do
439
+ Document.design_name.should be_nil
440
+ Document.design_document.should be_nil
441
+ end
442
+
443
+ it 'should create a design document if there is a design_name but the design document exists' do
444
+ Document.design_name = 'User'
445
+ lambda{ CouchDB::DesignDocument.get( 'User' ) }.should raise_error
446
+ Document.design_document.should_not be_nil
447
+ lambda{ CouchDB::DesignDocument.get( 'User') }.should_not raise_error
448
+ end
449
+
450
+ it 'should retrieve a design document if there is a design_name and the design document exists' do
451
+ CouchDB::DesignDocument.create!( :name => 'User' )
452
+ # ensures that the record exists, before the real test
453
+ lambda{ CouchDB::DesignDocument.get( 'User') }.should_not raise_error
454
+ Document.design_document.should_not be_nil
455
+ end
456
+ end
457
+
458
+ describe 'indexing & queries' do
459
+ Document.class_eval { attr_accessor( :my_field ) }
460
+
461
+ it 'should have a class method "indexes" that stores map names related to the class' do
462
+ Document.indexes.should == []
463
+ end
464
+
465
+ describe 'index_on' do
466
+ before(:each) do
467
+ Document.index_on(:my_field)
468
+ end
469
+
470
+ it 'should add text and symbol entries to indexes' do
471
+ Document.indexes.size.should == 2
472
+ Document.indexes.should include( :my_field )
473
+ Document.indexes.should include( 'my_field' )
474
+ end
475
+
476
+ it 'should not duplicate an index name' do
477
+ # previous tests have already added it to the array
478
+ Document.indexes.size.should == 2
479
+ end
480
+
481
+ it 'should add a view to the design document' do
482
+ design = Document.design_document(true)
483
+ design.views.should include( :my_field )
484
+ end
485
+ end
486
+
487
+ describe 'queries' do
488
+ before(:each) do
489
+ Document.parent_class = 'Document'
490
+ Document.index_on(:my_field)
491
+ Document.index_on_ivar( :my_ivar )
492
+ (1..5).each do |number|
493
+ Document.create!(
494
+ :my_field => number * 5,
495
+ :ivars => {
496
+ '@my_ivar' => number + 5
497
+ },
498
+ :class => 'Document'
499
+ )
500
+ end
501
+ end
502
+
503
+ it 'should query full documents for an index' do
504
+ docs = Document.query(:my_field)
505
+ docs.each{ |r| r.class.should == Document }
506
+ docs.size.should == 5
507
+ end
508
+
509
+ it 'should query ivar indexes correctly' do
510
+ docs = Document.query(:my_ivar)
511
+ docs.each{ |r| r.class.should == Document }
512
+ docs.size.should == 5
513
+ end
514
+
515
+ it 'should query only the index value for an index' do
516
+ docs = Document.query(:my_field, :select => 'index only')
517
+ docs.each{ |r| r.class.should == Fixnum }
518
+ end
519
+
520
+ it 'should generate a calculated/reduced view for an index the first time it is called' do
521
+ Document.count(:my_field)
522
+ Document.design_document.views.should include( :my_field_count )
523
+ end
524
+
525
+ it 'should count an index' do
526
+ Document.count(:my_field).should == 5
527
+ end
528
+
529
+ it 'should sum an index' do
530
+ Document.sum(:my_field).should == 75
531
+ end
532
+
533
+ it 'should average an index' do
534
+ Document.average(:my_field).should == 15
535
+ end
536
+
537
+ it 'should get the minimum of an index' do
538
+ Document.min(:my_field).should == 5
539
+ end
540
+
541
+ it 'should get the maximum of an index' do
542
+ Document.max(:my_field).should == 25
543
+ end
544
+
545
+ end
406
546
  end
407
547
 
408
548
  end