ricordami 0.0.1

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 (62) hide show
  1. data/CHANGELOG.md +13 -0
  2. data/ISSUES.md +0 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +454 -0
  5. data/TODO.md +21 -0
  6. data/examples/calls.rb +64 -0
  7. data/examples/singers.rb +42 -0
  8. data/lib/ricordami/attribute.rb +52 -0
  9. data/lib/ricordami/can_be_queried.rb +133 -0
  10. data/lib/ricordami/can_be_validated.rb +27 -0
  11. data/lib/ricordami/can_have_relationships.rb +152 -0
  12. data/lib/ricordami/configuration.rb +39 -0
  13. data/lib/ricordami/connection.rb +22 -0
  14. data/lib/ricordami/exceptions.rb +14 -0
  15. data/lib/ricordami/has_attributes.rb +159 -0
  16. data/lib/ricordami/has_indices.rb +105 -0
  17. data/lib/ricordami/is_lockable.rb +52 -0
  18. data/lib/ricordami/is_persisted.rb +123 -0
  19. data/lib/ricordami/is_retrievable.rb +35 -0
  20. data/lib/ricordami/key_namer.rb +63 -0
  21. data/lib/ricordami/model.rb +29 -0
  22. data/lib/ricordami/query.rb +68 -0
  23. data/lib/ricordami/relationship.rb +40 -0
  24. data/lib/ricordami/unique_index.rb +59 -0
  25. data/lib/ricordami/unique_validator.rb +21 -0
  26. data/lib/ricordami/value_index.rb +26 -0
  27. data/lib/ricordami/version.rb +3 -0
  28. data/lib/ricordami.rb +26 -0
  29. data/spec/acceptance/manage_relationships_spec.rb +42 -0
  30. data/spec/acceptance/model_with_validation_spec.rb +78 -0
  31. data/spec/acceptance/query_model_spec.rb +93 -0
  32. data/spec/acceptance_helper.rb +2 -0
  33. data/spec/ricordami/attribute_spec.rb +113 -0
  34. data/spec/ricordami/can_be_queried_spec.rb +254 -0
  35. data/spec/ricordami/can_be_validated_spec.rb +115 -0
  36. data/spec/ricordami/can_have_relationships_spec.rb +255 -0
  37. data/spec/ricordami/configuration_spec.rb +45 -0
  38. data/spec/ricordami/connection_spec.rb +25 -0
  39. data/spec/ricordami/exceptions_spec.rb +43 -0
  40. data/spec/ricordami/has_attributes_spec.rb +266 -0
  41. data/spec/ricordami/has_indices_spec.rb +73 -0
  42. data/spec/ricordami/is_lockable_spec.rb +45 -0
  43. data/spec/ricordami/is_persisted_spec.rb +186 -0
  44. data/spec/ricordami/is_retrievable_spec.rb +55 -0
  45. data/spec/ricordami/key_namer_spec.rb +56 -0
  46. data/spec/ricordami/model_spec.rb +65 -0
  47. data/spec/ricordami/query_spec.rb +156 -0
  48. data/spec/ricordami/relationship_spec.rb +123 -0
  49. data/spec/ricordami/unique_index_spec.rb +87 -0
  50. data/spec/ricordami/unique_validator_spec.rb +41 -0
  51. data/spec/ricordami/value_index_spec.rb +40 -0
  52. data/spec/spec_helper.rb +29 -0
  53. data/spec/support/constants.rb +43 -0
  54. data/spec/support/db_manager.rb +18 -0
  55. data/test/bin/data_loader.rb +107 -0
  56. data/test/data/domains.txt +462 -0
  57. data/test/data/first_names.txt +1220 -0
  58. data/test/data/last_names.txt +1028 -0
  59. data/test/data/people_100_000.csv.bz2 +0 -0
  60. data/test/data/people_10_000.csv.bz2 +0 -0
  61. data/test/data/people_1_000_000.csv.bz2 +0 -0
  62. metadata +258 -0
@@ -0,0 +1,73 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::HasIndices do
4
+ describe "the class" do
5
+ uses_constants("Car")
6
+ before(:each) do
7
+ Car.attribute :model
8
+ Car.attribute :name
9
+ end
10
+
11
+ it "raises an error when declaring an index that's neither unique or a value index" do
12
+ lambda { Car.index }.should raise_error(Ricordami::InvalidIndexDefinition)
13
+ end
14
+
15
+ describe "declaring a value index" do
16
+ it "can declare a value index with #index" do
17
+ index = Car.index :value => :model
18
+ Car.indices[:model].should be_a(Ricordami::ValueIndex)
19
+ Car.indices[:model].should == index
20
+ end
21
+ end
22
+
23
+ describe "declaring a unique index" do
24
+ it "can declare a unique index with #index" do
25
+ index = Car.index :unique => :model
26
+ Car.indices[:model].should be_a(Ricordami::UniqueIndex)
27
+ Car.indices[:model].should == index
28
+ end
29
+
30
+ it "discards the subsequent declarations if the same index is created more than once" do
31
+ Car.index :unique => :model, :get_by => true
32
+ Car.indices[:model].need_get_by.should be_true
33
+ Car.index :unique => :model, :get_by => false
34
+ Car.indices[:model].need_get_by.should be_true
35
+ end
36
+
37
+ it "saves the values of the unique attributes into the indices" do
38
+ Car.index :unique => :name
39
+ car = Car.new(:name => "Prius")
40
+ car.save
41
+ Car.indices[:name].all.should == ["Prius"]
42
+ end
43
+
44
+ it "replaces old values with new ones into the indices" do
45
+ Car.index :unique => :name
46
+ car = Car.new(:name => "Prius")
47
+ car.save
48
+ Car.indices[:name].all.should == ["Prius"]
49
+ car.name = "Rav4"
50
+ car.save
51
+ Car.indices[:name].all.should == ["Rav4"]
52
+ end
53
+
54
+ it "deletes the values of the unique attributes from the indices" do
55
+ Car.index :unique => :name
56
+ car = Car.create(:name => "Prius")
57
+ car.delete.should be_true
58
+ Car.indices[:name].all.should be_empty
59
+ end
60
+
61
+ it "adds a get_by_xxx method for each unique index xxx declared if :get_by is true" do
62
+ Car.index :unique => :name, :get_by => true
63
+ %w(il etait un petit navire).each { |n| Car.create(:name => n) }
64
+ Car.get_by_name("petit").name.should == "petit"
65
+ end
66
+
67
+ it "doesn't add the get_by_xxx method if :get_by is not true" do
68
+ Car.index :unique => :name
69
+ Car.should_not respond_to(:get_by_name)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,45 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::IsLockable do
4
+ uses_constants("Client")
5
+
6
+ it "can get only one model lock at a time" do
7
+ 5.times do
8
+ ts1 = ts2 = nil
9
+ client = Client.create
10
+ t1 = Thread.new do
11
+ client.lock! { sleep(0.01); ts1 = Time.now }
12
+ end
13
+ t2 = Thread.new do
14
+ sleep(0.01)
15
+ client.lock! { ts2 = Time.now }
16
+ end
17
+ t1.join
18
+ t2.join
19
+ ts2.should >= ts1
20
+ end
21
+ end
22
+
23
+ it "can get one lock per instance" do
24
+ 5.times do
25
+ c1ok, c2ok = false, false
26
+ client = Client.create
27
+ other = Client.create
28
+ t1 = Thread.new do
29
+ client.lock! do
30
+ c1ok = true
31
+ sleep 0.05 until c2ok
32
+ end
33
+ end
34
+ t2 = Thread.new do
35
+ other.lock! do
36
+ c2ok = true
37
+ sleep 0.05 until c1ok
38
+ end
39
+ end
40
+ t1.join
41
+ t2.join
42
+ c2ok.should be_true
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,186 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::IsPersisted do
4
+ uses_constants("Tenant")
5
+
6
+ it "is not persisted and is a new record after it's just been initialized" do
7
+ tenant = Tenant.new
8
+ tenant.should_not be_persisted
9
+ tenant.should be_a_new_record
10
+ end
11
+
12
+ it "is persisted and not a new record after it's been successfully saved with #save" do
13
+ tenant = Tenant.new
14
+ tenant.save
15
+ tenant.should be_persisted
16
+ tenant.should_not be_a_new_record
17
+ end
18
+
19
+ it "returns the redis driver instance with #redis" do
20
+ Tenant.redis.should == Ricordami.driver
21
+ Tenant.new.redis.should == Ricordami.driver
22
+ end
23
+
24
+ describe "#save & #create" do
25
+ shared_examples_for "a persister" do
26
+ it "persists a new model" do
27
+ Tenant.attribute :balance
28
+ persister_action.call
29
+ attributes = Ricordami.driver.hgetall("Tenant:att:jojo")
30
+ attributes["id"].should == "jojo"
31
+ attributes["balance"].should == "-$99.98"
32
+ end
33
+ end
34
+
35
+ describe "#save" do
36
+ let(:persister_action) {
37
+ Proc.new {
38
+ tenant = Tenant.new(:id => "jojo", :balance => "-$99.98")
39
+ tenant.save.should be_true
40
+ }
41
+ }
42
+ it_should_behave_like "a persister"
43
+ it "returns false if saving failed" do
44
+ switch_db_to_error
45
+ Tenant.new.save.should be_false
46
+ switch_db_to_ok
47
+ end
48
+ end
49
+
50
+ describe "#create" do
51
+ let(:persister_action) {
52
+ Proc.new {
53
+ tenant = Tenant.create(:id => "jojo", :balance => "-$99.98")
54
+ tenant.should be_a(Tenant)
55
+ tenant.should be_persisted
56
+ }
57
+ }
58
+ it_should_behave_like "a persister"
59
+ it "returns an instance not persisted if creating failed" do
60
+ switch_db_to_error
61
+ tenant = Tenant.create(:id => "jojo", :balance => "-$99.98")
62
+ tenant.should be_a(Tenant)
63
+ tenant.should_not be_persisted
64
+ switch_db_to_ok
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "#reload and #update_attributes" do
70
+ it "reloads the attribute values from the DB with #reload" do
71
+ Tenant.attribute :name
72
+ tenant = Tenant.create(:name => "gainsbourg")
73
+ tenant.name = "updated"
74
+ tenant.name.should == "updated"
75
+ tenant.reload
76
+ tenant.name.should == "gainsbourg"
77
+ end
78
+
79
+ it "updates passed attribute values and saves the model with #update_attributes" do
80
+ [:first, :last].each { |a| Tenant.attribute a }
81
+ tenant = Tenant.create(:first => "serge", :last => "gainsbourg")
82
+ tenant.update_attributes(:first => "SERGE").should be_true
83
+ tenant.reload
84
+ tenant.first.should == "SERGE"
85
+ tenant.last.should == "gainsbourg"
86
+ end
87
+
88
+ it "raise an error if #update_attributes can't save the changes" do
89
+ tenant = Tenant.create(:id => "gainsbare") # id is read-only
90
+ lambda {
91
+ tenant.update_attributes(:id => "gainsbourg")
92
+ }.should raise_error(Ricordami::ReadOnlyAttribute)
93
+ end
94
+ end
95
+
96
+ describe "grouping DB operations with #queue_saving_operations" do
97
+ it "raises an error if no block is passed" do
98
+ lambda {
99
+ Tenant.queue_saving_operations
100
+ }.should raise_error(ArgumentError)
101
+ end
102
+
103
+ it "raises an error if the block passed doesn't take 2 arguments" do
104
+ lambda {
105
+ Tenant.queue_saving_operations { |one| [one] }
106
+ }.should raise_error(ArgumentError)
107
+ end
108
+
109
+ it "allows to queue DB operations that will run when instance is saved" do
110
+ logs = []
111
+ Tenant.attribute :name
112
+ Tenant.queue_saving_operations do |obj, session|
113
+ logs << if obj.persisted?
114
+ "ALREADY persisted"
115
+ else
116
+ "NOT persisted"
117
+ end
118
+ end
119
+ tenant = Tenant.new
120
+ tenant.save
121
+ tenant.update_attributes(:name => "blah")
122
+ logs.should == ["NOT persisted", "ALREADY persisted"]
123
+ end
124
+ end
125
+
126
+ describe "grouping DB operations with #queue_deleting_operations" do
127
+ it "raises an error if no block is passed" do
128
+ lambda {
129
+ Tenant.queue_deleting_operations
130
+ }.should raise_error(ArgumentError)
131
+ end
132
+
133
+ it "raises an error if the block passed doesn't take 2 arguments" do
134
+ lambda {
135
+ Tenant.queue_deleting_operations { |one| [one] }
136
+ }.should raise_error(ArgumentError)
137
+ end
138
+
139
+ it "allows to queue DB operations that will run when instance is deleted" do
140
+ logs = []
141
+ Tenant.attribute :name
142
+ Tenant.queue_deleting_operations { |obj, session| logs << "Tenant deleted #1" }
143
+ Tenant.queue_deleting_operations { |obj, session| logs << "Tenant deleted #2" }
144
+ tenant = Tenant.create
145
+ tenant.delete
146
+ logs.should == ["Tenant deleted #2", "Tenant deleted #1"]
147
+ end
148
+ end
149
+
150
+ describe "#delete" do
151
+ before(:each) { Tenant.attribute :name }
152
+ let(:tenant) { Tenant.create(:id => "myid") }
153
+
154
+ it "deletes the attributes from the DB" do
155
+ tenant.delete.should be_true
156
+ from_db = Ricordami.driver.hgetall("Tenant:att:myid")
157
+ from_db.should be_empty
158
+ end
159
+
160
+ it "knows when a model has been deleted" do
161
+ tenant.should_not be_deleted
162
+ tenant.delete
163
+ tenant.should be_deleted
164
+ end
165
+
166
+ it "freezes the model after deleting it" do
167
+ tenant.delete
168
+ tenant.should be_frozen
169
+ end
170
+
171
+ it "can't save a model that was deleted" do
172
+ tenant.delete
173
+ lambda { tenant.save }.should raise_error(Ricordami::ModelHasBeenDeleted)
174
+ end
175
+
176
+ it "can't update the attributes of a model that was deleted" do
177
+ tenant.delete
178
+ lambda {
179
+ tenant.update_attributes(:name => "titi")
180
+ }.should raise_error(Ricordami::ModelHasBeenDeleted)
181
+ end
182
+
183
+ it "can't delete a model that was deleted" do
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,55 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::IsRetrievable do
4
+ uses_constants("Tenant")
5
+
6
+ describe "#get & #[]" do
7
+ it "loads a model with #get when it exists" do
8
+ Tenant.attribute :language
9
+ Tenant.new(:id => "Francois", :language => "French").save
10
+ tenant = Tenant.get("Francois")
11
+ tenant.attributes.should == {
12
+ "id" => "Francois",
13
+ "language" => "French"
14
+ }
15
+ tenant.should be_persisted
16
+ end
17
+
18
+ it "#get raises an error if the model is not found" do
19
+ lambda {
20
+ Tenant.get("doesn't exist")
21
+ }.should raise_error(Ricordami::NotFound)
22
+ end
23
+
24
+ it "#[] is an alias for get" do
25
+ Tenant.new(:id => "myid").save
26
+ Tenant["myid"].attributes["id"].should == "myid"
27
+ lambda { Tenant["nope"] }.should raise_error(Ricordami::NotFound)
28
+ end
29
+ end
30
+
31
+ describe "#all & #count" do
32
+ it "saves the ids of new instances when saved" do
33
+ Tenant.attribute :name
34
+ instance = Tenant.new(:id => "hi")
35
+ instance.save
36
+ Tenant.indices[:id].all.should == ["hi"]
37
+ instance.name = "john"
38
+ instance.save
39
+ Tenant.indices[:id].all.should == ["hi"]
40
+ end
41
+
42
+ it "returns all the instances with #all" do
43
+ %w(allo la terre).each { |n| Tenant.create(:id => n) }
44
+ Tenant.all.map(&:id).should =~ ["allo", "la", "terre"]
45
+ end
46
+
47
+ it "returns the number of instances with #count" do
48
+ Tenant.count.should == 0
49
+ Tenant.create(:id => "blah")
50
+ Tenant.count.should == 1
51
+ %w(allo la terre).each { |n| Tenant.create(:id => n) }
52
+ Tenant.count.should == 4
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,56 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::KeyNamer do
4
+ subject { Ricordami::KeyNamer }
5
+ uses_constants("Call", "Leg", "Boat")
6
+
7
+ it "returns a key name for a sequence with #sequence" do
8
+ name = subject.sequence(Boat, :type => "id")
9
+ name.should == "Boat:seq:id"
10
+ end
11
+
12
+ it "returns a key name for model attributes with #attributes" do
13
+ name = subject.attributes(Boat, :id => "42")
14
+ name.should == "Boat:att:42"
15
+ end
16
+
17
+ it "returns a key name for a unique index with #unique_index" do
18
+ name = subject.unique_index(Call, :name => "ani")
19
+ name.should == "Call:udx:ani"
20
+ end
21
+
22
+ it "returns a key name for a hash reference with #hash_ref" do
23
+ name = subject.hash_ref(Call, :fields=> ["ani", "dnis"])
24
+ name.should == "Call:hsh:ani_dnis_to_id"
25
+ end
26
+
27
+ it "returns a key name for the value of a field of a model with #index" do
28
+ name = subject.index(Leg, :field => :direction, :value => "inout")
29
+ name.should == "Leg:idx:direction:aW5vdXQ="
30
+ end
31
+
32
+ it "returns a volatile key name for a set with #volatile_set" do
33
+ kn1 = subject.volatile_set(Call, :key => nil, :info => [:and, "username", "sex"])
34
+ kn1.should == "~:Call:set:and(username,sex)"
35
+ kn2 = subject.volatile_set(Call, :key => "~:Call:set:and(username,sex)",
36
+ :info => [:or, "country"])
37
+ kn2.should == "~:Call:set:and(username,sex):or(country)"
38
+ end
39
+
40
+ it "returns a temporary key name for a model with #temporary" do
41
+ 5.times do |i|
42
+ name = subject.temporary(Leg)
43
+ name.should == "Leg:val:_tmp:#{i + 1}"
44
+ end
45
+ end
46
+
47
+ it "returns a key name for the model lock with #lock" do
48
+ name = subject.lock(Leg, :id => 3)
49
+ name.should == "Leg:val:3:_lock"
50
+ end
51
+
52
+ it "returns a key name for sort an attribute with #sort" do
53
+ name = subject.sort("Student", :sort_by => :name)
54
+ name.should == "Student:att:*->name"
55
+ end
56
+ end
@@ -0,0 +1,65 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::Model do
4
+ uses_constants('User')
5
+
6
+ it "uses ActiveSupport::Concern for a simple module structure" do
7
+ Ricordami::Model.should be_a_kind_of(ActiveSupport::Concern)
8
+ end
9
+
10
+ it "includes the modules listed with #model_can" do
11
+ User.model_can :be_queried, :be_validated, :have_relationships
12
+ user = User.new
13
+ user.should be_a_kind_of(Ricordami::CanBeQueried)
14
+ user.should be_a_kind_of(Ricordami::CanBeValidated)
15
+ user.should be_a_kind_of(Ricordami::CanHaveRelationships)
16
+ end
17
+
18
+ describe "when being included" do
19
+ it "adds model naming" do
20
+ model_name = User.model_name
21
+ model_name.should == "User"
22
+ model_name.singular.should == "user"
23
+ model_name.plural.should == "users"
24
+ end
25
+
26
+ it "adds to_model for use with Rails" do
27
+ user = User.new
28
+ user.to_model.should == user
29
+ end
30
+
31
+ describe "#to_key" do
32
+ before(:each) do
33
+ @user = User.new
34
+ def @user.id; "some_id" end
35
+ end
36
+
37
+ it "returns [id] if persisted" do
38
+ def @user.persisted?; true end
39
+ @user.to_key.should == [@user.id]
40
+ end
41
+
42
+ it "returns nil if not persisted" do
43
+ def @user.persisted?; false end
44
+ @user.to_key.should be_nil
45
+ end
46
+ end
47
+
48
+ describe "#to_param" do
49
+ before(:each) do
50
+ @user = User.new
51
+ def @user.persisted?; true end
52
+ end
53
+
54
+ it "returns key joined by - if to_key present" do
55
+ def @user.to_key; ["some", "id", "here"] end
56
+ @user.to_param.should == "some-id-here"
57
+ end
58
+
59
+ it "returns nil if to_key nil" do
60
+ def @user.persisted?; false end
61
+ @user.to_param.should be_nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,156 @@
1
+ require "spec_helper"
2
+ require "ricordami/query"
3
+
4
+ describe Ricordami::Query do
5
+ uses_constants("Instrument")
6
+ let(:query) { Ricordami::Query.new(Instrument) }
7
+
8
+ it "has expressions" do
9
+ query.expressions.should == []
10
+ end
11
+
12
+ describe "#and" do
13
+ it "saves :and expressions" do
14
+ query.and(:allo => "la terre")
15
+ query.expressions.pop.should == [:and, {:allo => "la terre"}]
16
+ end
17
+
18
+ it "uses :where as an alias for :and" do
19
+ query.where(:allo => "la terre")
20
+ query.expressions.pop.should == [:and, {:allo => "la terre"}]
21
+ end
22
+
23
+ it "returns self" do
24
+ query.and.should == query
25
+ end
26
+ end
27
+
28
+ describe "#not" do
29
+ it "saves :not expressions" do
30
+ query.not(:allo => "la terre")
31
+ query.expressions.pop.should == [:not, {:allo => "la terre"}]
32
+ end
33
+
34
+ it "returns self" do
35
+ query.not.should == query
36
+ end
37
+ end
38
+
39
+ describe "#any" do
40
+ it "saves :any expressions" do
41
+ query.any(:allo => "la terre")
42
+ query.expressions.pop.should == [:any, {:allo => "la terre"}]
43
+ end
44
+
45
+ it "returns self" do
46
+ query.any.should == query
47
+ end
48
+ end
49
+
50
+ describe "running the query" do
51
+ it "delegates #all to the runner" do
52
+ Instrument.should_receive(:all).with(:expressions => [[:and, {:key => "val"}]])
53
+ query.and(:key => "val").all
54
+ end
55
+
56
+ it "delegates #paginate to the runner" do
57
+ Instrument.should_receive(:paginate).with(:expressions => [[:and, {:key => "val"}]], :page => 3, :per_page => 18)
58
+ query.and(:key => "val").paginate(:page => 3, :per_page => 18)
59
+ end
60
+
61
+ it "delegates #first to the runner" do
62
+ Instrument.should_receive(:first).with(:expressions => [[:and, {:key => "val"}]],
63
+ :sort_by => :key,
64
+ :order => "ALPHA ASC")
65
+ query.and(:key => "val").sort(:key).first
66
+ end
67
+
68
+ it "delegates #last to the runner" do
69
+ Instrument.should_receive(:last).with(:expressions => [[:and, {:key => "val"}]],
70
+ :sort_by => :key,
71
+ :order => "ALPHA DESC")
72
+ query.and(:key => "val").sort(:key, :desc_alpha).last
73
+ end
74
+
75
+ it "delegates #rand to the runner" do
76
+ Instrument.should_receive(:respond_to?).with(:rand).and_return(true)
77
+ Instrument.should_receive(:rand).with(:expressions => [[:and, {:key => "val"}]],
78
+ :sort_by => :key,
79
+ :order => "ALPHA ASC")
80
+ query.and(:key => "val").sort(:key).rand
81
+ end
82
+
83
+ it "returns the runner if it can't delegate to the runner" do
84
+ Ricordami::Query.new([]).all.should == []
85
+ Ricordami::Query.new([]).paginate.should == []
86
+ end
87
+
88
+ it "accepts any unknown method and delegate it to the result of #all" do
89
+ instruments = %w(guitar bass drums).map { |value| Struct.new(:name).new(value) }
90
+ Instrument.should_receive(:all).
91
+ with(:expressions => [[:and, {:key => "val"}]]).
92
+ and_return(instruments)
93
+ query.and(:key => "val").map(&:name).should =~ ["guitar", "bass", "drums"]
94
+ end
95
+ end
96
+
97
+ describe "sorting the query" do
98
+ it "remembers the sorting attribute with #sort" do
99
+ query.sort(:username)
100
+ query.sort_by.should == :username
101
+ end
102
+
103
+ it "remembers the sorting direction with #sort" do
104
+ query.sort(:username)
105
+ query.sort_dir.should == :asc_alpha
106
+ query.sort(:username, :desc_alpha)
107
+ query.sort_dir.should == :desc_alpha
108
+ end
109
+
110
+ it "raises an error if the sorting order is not :asc_alpha, :asc_num, desc_alpha or :desc_num" do
111
+ lambda {
112
+ [:asc_alpha, :asc_num, :desc_alpha, :desc_num].each do |dir|
113
+ query.sort(:username, dir)
114
+ end
115
+ }.should_not raise_error(ArgumentError)
116
+ lambda {
117
+ query.sort(:username, :blah)
118
+ }.should raise_error(ArgumentError)
119
+ end
120
+ end
121
+
122
+ describe "creating new objects" do
123
+ before(:each) do
124
+ Instrument.attribute :name
125
+ Instrument.attribute :instrument_type
126
+ Instrument.attribute :difficulty
127
+ end
128
+
129
+ it "builds a new object with #build" do
130
+ query.and(:instrument_type => "wind", :name => "flute")
131
+ obj = query.build
132
+ obj.should be_an(Instrument)
133
+ obj.should_not be_persisted
134
+ obj.name.should == "flute"
135
+ obj.instrument_type.should == "wind"
136
+ end
137
+
138
+ it "can pass new attributes to #build" do
139
+ query.and(:instrument_type => "wind", :name => "flute")
140
+ obj = query.build(:name => "tuba", :difficulty => "hard")
141
+ obj.instrument_type.should == "wind"
142
+ obj.name.should == "tuba"
143
+ obj.difficulty.should == "hard"
144
+ end
145
+
146
+ it "creates a new object with #create" do
147
+ query.and(:instrument_type => "wind", :name => "flute")
148
+ obj = query.create(:name => "tuba", :difficulty => "hard")
149
+ obj.should be_an(Instrument)
150
+ obj.should be_persisted
151
+ obj.instrument_type.should == "wind"
152
+ obj.name.should == "tuba"
153
+ obj.difficulty.should == "hard"
154
+ end
155
+ end
156
+ end