mongoscript 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +32 -0
- data/Guardfile +19 -0
- data/Rakefile +11 -0
- data/lib/mongoscript/execution.rb +90 -0
- data/lib/mongoscript/javascripts/multiquery.js +40 -0
- data/lib/mongoscript/multiquery.rb +171 -0
- data/lib/mongoscript/orm/mongoid_adapter.rb +82 -0
- data/lib/mongoscript/orm/mongoid_document_methods.rb +11 -0
- data/lib/mongoscript/version.rb +3 -0
- data/lib/mongoscript.rb +28 -0
- data/mongoscript.gemspec +20 -0
- data/readme.md +22 -0
- data/spec/cases/execution_spec.rb +157 -0
- data/spec/cases/mongoid_adapter_spec.rb +126 -0
- data/spec/cases/mongoscript_spec.rb +25 -0
- data/spec/cases/multiquery_spec.rb +295 -0
- data/spec/fixtures/mongoid.yml +9 -0
- data/spec/fixtures/sample_script.js +3 -0
- data/spec/javascripts/helpers/SpecHelper.js +82 -0
- data/spec/javascripts/multiquery_spec.js +109 -0
- data/spec/javascripts/support/jasmine.yml +73 -0
- data/spec/javascripts/support/jasmine_config.rb +23 -0
- data/spec/javascripts/support/jasmine_runner.rb +32 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/mongoid.yml +9 -0
- data/spec/support/mongoid_classes.rb +2 -0
- metadata +98 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MongoScript::Execution do
|
4
|
+
module ObjectWithExecution
|
5
|
+
include MongoScript::ORM::MongoidAdapter
|
6
|
+
include MongoScript::Execution
|
7
|
+
end
|
8
|
+
|
9
|
+
before :all do
|
10
|
+
@original_script_dirs = ObjectWithExecution.script_dirs
|
11
|
+
end
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
ObjectWithExecution.script_dirs = @original_script_dirs
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has a constant for LOADED_SCRIPTS" do
|
18
|
+
MongoScript::Execution::LOADED_SCRIPTS.should be_a(Hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "defines ScriptNotFound error < Errno::ENOENT" do
|
22
|
+
MongoScript::Execution::ScriptNotFound.superclass.should == Errno::ENOENT
|
23
|
+
end
|
24
|
+
|
25
|
+
it "defines ExecutionFailure error < RuntimeError" do
|
26
|
+
MongoScript::Execution::ExecutionFailure.superclass.should == RuntimeError
|
27
|
+
end
|
28
|
+
|
29
|
+
it "has a script_dir accessor" do
|
30
|
+
stubby = stub("dir")
|
31
|
+
ObjectWithExecution.script_dirs = stubby
|
32
|
+
ObjectWithExecution.script_dirs.should == stubby
|
33
|
+
end
|
34
|
+
|
35
|
+
it "defaults to the built-in scripts" do
|
36
|
+
location_pieces = File.dirname(__FILE__).split("/")
|
37
|
+
# strip out /spec/cases to get back to the root directory
|
38
|
+
gem_path = location_pieces[0, location_pieces.length - 2].join("/")
|
39
|
+
ObjectWithExecution.script_dirs.should == [File.join(gem_path, "lib", "mongoscript", "javascripts")]
|
40
|
+
end
|
41
|
+
|
42
|
+
describe ".code_for" do
|
43
|
+
before :all do
|
44
|
+
@script_code = File.open(File.join(SCRIPTS_PATH, "sample_script.js")).read
|
45
|
+
end
|
46
|
+
|
47
|
+
before :each do
|
48
|
+
ObjectWithExecution.script_dirs = @original_script_dirs + [SCRIPTS_PATH]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "loads and returns the code for a given file" do
|
52
|
+
ObjectWithExecution.code_for("sample_script").should == @script_code
|
53
|
+
end
|
54
|
+
|
55
|
+
it "loads and returns the code for a given file by symbol" do
|
56
|
+
ObjectWithExecution.code_for(:sample_script).should == @script_code
|
57
|
+
end
|
58
|
+
|
59
|
+
it "stores the value in LOADED_SCRIPTS" do
|
60
|
+
ObjectWithExecution.code_for(:sample_script)
|
61
|
+
ObjectWithExecution::LOADED_SCRIPTS["sample_script"].should == @script_code
|
62
|
+
end
|
63
|
+
|
64
|
+
it "raises a ScriptNotFound error if the file doesn't exist" do
|
65
|
+
File.stubs(:exist?).returns(false)
|
66
|
+
expect { ObjectWithExecution.code_for("i don't exist") }.to raise_exception(ObjectWithExecution::ScriptNotFound)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "will look in all the directories provided" do
|
70
|
+
dir = "/foo/bar"
|
71
|
+
my_script = "a script"
|
72
|
+
ObjectWithExecution.script_dirs << dir
|
73
|
+
File.stubs(:exists?).returns(*(ObjectWithExecution.script_dirs.map {|f| f == dir}))
|
74
|
+
|
75
|
+
# make sure that we try to load the script
|
76
|
+
stubby = stub("file contents")
|
77
|
+
File.expects(:read).with(File.join(dir, "#{my_script}.js")).returns(stubby)
|
78
|
+
ObjectWithExecution.code_for(my_script).should == stubby
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe ".execute_readonly_routine" do
|
83
|
+
it "gets and passes the appropriate code and arguments to be run in readonly mode" do
|
84
|
+
args = [1, 2, {}]
|
85
|
+
name = "scriptname"
|
86
|
+
stubby = stub("code")
|
87
|
+
ObjectWithExecution.expects(:code_for).with(name).returns(stubby)
|
88
|
+
ObjectWithExecution.expects(:execute_readonly_code).with(stubby, *args)
|
89
|
+
ObjectWithExecution.execute_readonly_routine(name, *args)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe ".execute_readwrite_routine" do
|
94
|
+
it "gets and passes the appropriate code and arguments to be run in readwrite mode" do
|
95
|
+
args = [1, 2, {}]
|
96
|
+
name = "scriptname"
|
97
|
+
stubby = stub("code")
|
98
|
+
ObjectWithExecution.expects(:code_for).with(name).returns(stubby)
|
99
|
+
ObjectWithExecution.expects(:execute_readwrite_code).with(stubby, *args)
|
100
|
+
ObjectWithExecution.execute_readwrite_routine(name, *args)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe ".execute_readonly_code" do
|
105
|
+
it "executes provided code and arguments in with nolock mode" do
|
106
|
+
args = [1, 2, {}]
|
107
|
+
code = stub("code")
|
108
|
+
ObjectWithExecution.expects(:execute).with(code, args, {:nolock => true})
|
109
|
+
ObjectWithExecution.execute_readonly_code(code, *args)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe ".execute_readwrite_code" do
|
114
|
+
it "executes provided code and arguments with no Mongo options" do
|
115
|
+
args = [1, 2, {}]
|
116
|
+
code = stub("code")
|
117
|
+
ObjectWithExecution.expects(:execute).with(code, args)
|
118
|
+
ObjectWithExecution.execute_readwrite_code(code, *args)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe ".execute" do
|
123
|
+
it "executes the command via the Mongo database" do
|
124
|
+
MongoScript.database.expects(:command).returns({"ok" => 1.0})
|
125
|
+
ObjectWithExecution.execute("code")
|
126
|
+
end
|
127
|
+
|
128
|
+
it "executes the code using the eval command" do
|
129
|
+
code = stub("code")
|
130
|
+
MongoScript.database.expects(:command).with(has_entries(:$eval => code)).returns({"ok" => 1.0})
|
131
|
+
ObjectWithExecution.execute(code)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "passes in any arguments provided" do
|
135
|
+
args = [:a, :r, :g, :s]
|
136
|
+
MongoScript.database.expects(:command).with(has_entries(:args => args)).returns({"ok" => 1.0})
|
137
|
+
ObjectWithExecution.execute("code", args)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "merges in any additional options" do
|
141
|
+
options = {:a => stub("options")}
|
142
|
+
MongoScript.database.expects(:command).with(has_entries(options)).returns({"ok" => 1.0})
|
143
|
+
ObjectWithExecution.execute("code", [], options)
|
144
|
+
end
|
145
|
+
|
146
|
+
it "raises an ExecutionFailure error if the result[ok] != 1.0" do
|
147
|
+
MongoScript.database.expects(:command).returns({"result" => {}})
|
148
|
+
expect { ObjectWithExecution.execute("code") }.to raise_exception(MongoScript::Execution::ExecutionFailure)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "returns the retval" do
|
152
|
+
result = stub("result")
|
153
|
+
MongoScript.database.expects(:command).returns({"ok" => 1.0, "retval" => result})
|
154
|
+
ObjectWithExecution.execute("code").should == result
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MongoScript::ORM::MongoidAdapter do
|
4
|
+
module ObjectWithMongoidAdapter
|
5
|
+
include MongoScript::ORM::MongoidAdapter
|
6
|
+
end
|
7
|
+
|
8
|
+
class AMongoidClass
|
9
|
+
include Mongoid::Document
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
before :all do
|
14
|
+
@adapter = ObjectWithMongoidAdapter
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#database" do
|
18
|
+
it "returns Mongo::Config.database" do
|
19
|
+
db_stub = stub("database")
|
20
|
+
Mongoid::Config.stubs(:database).returns(db_stub)
|
21
|
+
@adapter.database.should == db_stub
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#rehydrate" do
|
26
|
+
it "uses Mongoid::Factory to create the Mongoid doc" do
|
27
|
+
klass = stub("class")
|
28
|
+
hash = stub("document attributes")
|
29
|
+
Mongoid::Factory.expects(:from_db).with(klass, hash)
|
30
|
+
@adapter.rehydrate(klass, hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns the rehydrated value" do
|
34
|
+
result = stub("document")
|
35
|
+
Mongoid::Factory.stubs(:from_db).returns(result)
|
36
|
+
@adapter.rehydrate("foo", "bar").should == result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#resolve_arguments" do
|
41
|
+
it "resolves any hashes in the arguments" do
|
42
|
+
args = [{}, 2, 3, [], {:c.in => 2}]
|
43
|
+
args.each {|a| @adapter.expects(:resolve_complex_criteria).with(a) if a.is_a?(Hash)}
|
44
|
+
@adapter.resolve_arguments(args)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns the mapped results" do
|
48
|
+
args = [{}, 2, 3, [], {:c.in => 2}]
|
49
|
+
stubby = stub("result")
|
50
|
+
@adapter.stubs(:resolve_complex_criteria).returns(stubby)
|
51
|
+
@adapter.resolve_arguments(args).should == args.map {|a| a.is_a?(Hash) ? stubby : a}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#resolve_complex_criteria" do
|
56
|
+
it "recursively replaces any hash values with their own resolve_complex_criteria results" do
|
57
|
+
hash = {:a.in => [1, 2], :c => {:e.exists => false, :f => {:y.ne => 2}}, :f => 3}
|
58
|
+
@adapter.resolve_complex_criteria(hash).should ==
|
59
|
+
{:a=>{"$in"=>[1, 2]}, :c=>{:e=>{"$exists"=>false}, :f=>{:y=>{"$ne"=>2}}}, :f=>3}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#processable_into_parameters?" do
|
64
|
+
it "returns true for Mongoid criteria" do
|
65
|
+
ObjectWithMongoidAdapter.processable_into_parameters?(AMongoidClass.all).should be_true
|
66
|
+
end
|
67
|
+
|
68
|
+
it "returns false for everything else" do
|
69
|
+
ObjectWithMongoidAdapter.processable_into_parameters?(Hash.new).should be_false
|
70
|
+
ObjectWithMongoidAdapter.processable_into_parameters?(Array.new).should be_false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#build_multiquery_parameters" do
|
75
|
+
# this is a mishmash of stubbing and testing against the values assigned via let :)
|
76
|
+
|
77
|
+
let(:criteria) {
|
78
|
+
AMongoidClass.where(:_ids.in => [1, 2, 3]).only(:_id).ascending(:date).limit(4)
|
79
|
+
}
|
80
|
+
|
81
|
+
it "returns nil if provided something other than a Criteria" do
|
82
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters({}).should be_nil
|
83
|
+
end
|
84
|
+
|
85
|
+
it "doesn't change the criteria's options" do
|
86
|
+
expect {
|
87
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(criteria)
|
88
|
+
}.not_to change(criteria, :options)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "returns the selector as :selector" do
|
92
|
+
selecty = stub("selector")
|
93
|
+
criteria.stubs(:selector).returns(selecty)
|
94
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(criteria)[:selector].should == selecty
|
95
|
+
end
|
96
|
+
|
97
|
+
it "returns the klass as :klass" do
|
98
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(criteria)[:klass].should == AMongoidClass
|
99
|
+
end
|
100
|
+
|
101
|
+
it "returns the name of the collection as :collection" do
|
102
|
+
name = stub("name")
|
103
|
+
criteria.collection.stubs(:name).returns(name)
|
104
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(criteria)[:collection].should == name
|
105
|
+
end
|
106
|
+
|
107
|
+
it "returns the fields to get as :fields" do
|
108
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(criteria)[:fields].should == {_id: 1, _type: 1}
|
109
|
+
end
|
110
|
+
|
111
|
+
it "returns all other options as :modifiers" do
|
112
|
+
modifiers = criteria.options.dup.delete_if {|k, v| k == :fields}
|
113
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(criteria)[:modifiers].keys.should == modifiers.keys
|
114
|
+
end
|
115
|
+
|
116
|
+
it "uses Mongo::Support to expand the sort criteria" do
|
117
|
+
sorts = stub("sorted info")
|
118
|
+
Mongo::Support.expects(:array_as_sort_parameters).with(criteria.options[:sort]).returns(sorts)
|
119
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(criteria)[:modifiers][:sort].should == sorts
|
120
|
+
end
|
121
|
+
|
122
|
+
it "works fine with no sort order" do
|
123
|
+
ObjectWithMongoidAdapter.build_multiquery_parameters(AMongoidClass.all)[:modifiers][:sort].should == {}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MongoScript do
|
4
|
+
describe "modules" do
|
5
|
+
it "includes Execution" do
|
6
|
+
MongoScript.included_modules.should include(MongoScript::Execution)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "includes whatever's determined by orm_adapter" do
|
10
|
+
MongoScript.included_modules.should include(MongoScript.orm_adapter)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe ".orm_adapter" do
|
15
|
+
it "returns the Mongoid adapter if Mongoid is defined" do
|
16
|
+
Object.stubs(:const_defined?).with("Mongoid").returns(true)
|
17
|
+
MongoScript.orm_adapter.should == MongoScript::ORM::MongoidAdapter
|
18
|
+
end
|
19
|
+
|
20
|
+
it "raises a NoORMError if no Mongo ORM is available" do
|
21
|
+
MongoScript.stubs(:const_defined?).with("Mongoid").returns(false)
|
22
|
+
expect { MongoScript.orm_adapter }.to raise_exception(MongoScript::NoORMError)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MongoScript::Multiquery do
|
4
|
+
module ObjectWithMultiquery
|
5
|
+
include MongoScript::ORM::MongoidAdapter
|
6
|
+
include MongoScript::Execution
|
7
|
+
include MongoScript::Multiquery
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:results) {
|
11
|
+
{
|
12
|
+
:cars => 3.times.collect { Car.new.attributes },
|
13
|
+
:canines => 3.times.collect { Car.new.attributes }
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
let(:queries) {
|
18
|
+
{
|
19
|
+
:cars => {:query => {:_id => {:"$in" => [1, 2, 3]}}},
|
20
|
+
:canines => {:collection => :dogs, :query => {:deleted_at => Time.now}}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
let(:normalized_queries) {
|
25
|
+
MongoScript.normalize_queries(queries)
|
26
|
+
}
|
27
|
+
|
28
|
+
let(:mongoized_queries) {
|
29
|
+
MongoScript.mongoize_queries(normalized_queries)
|
30
|
+
}
|
31
|
+
|
32
|
+
it "defines QueryFailedError error < RuntimeError" do
|
33
|
+
MongoScript::Multiquery::QueryFailedError.superclass.should == RuntimeError
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#multiquery" do
|
37
|
+
# here we just want to test the flow of the method
|
38
|
+
# further tests ensure that each individual call works as expected
|
39
|
+
# we also will have integration tests soon
|
40
|
+
let(:normalized_queries) { stub("normalized queries") }
|
41
|
+
let(:mongoized_queries) { stub("mongoized queries") }
|
42
|
+
let(:raw_results) { stub("raw results") }
|
43
|
+
let(:processed_results) { stub("processed results") }
|
44
|
+
|
45
|
+
before :each do
|
46
|
+
MongoScript.stubs(:normalize_queries).returns(normalized_queries)
|
47
|
+
MongoScript.stubs(:validate_queries!)
|
48
|
+
MongoScript.stubs(:mongoize_queries).returns(mongoized_queries)
|
49
|
+
MongoScript.stubs(:execute_readonly_routine).returns(raw_results)
|
50
|
+
MongoScript.stubs(:process_results).returns(raw_results)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns {} without hitting the database if passed {}" do
|
54
|
+
MongoScript.expects(:execute).never
|
55
|
+
MongoScript.multiquery({}).should == {}
|
56
|
+
end
|
57
|
+
|
58
|
+
it "normalizes the queries" do
|
59
|
+
MongoScript.expects(:normalize_queries).with(queries)
|
60
|
+
MongoScript.multiquery(queries)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "validates the normalized queries" do
|
64
|
+
MongoScript.expects(:validate_queries!).with(normalized_queries)
|
65
|
+
MongoScript.multiquery(queries)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "mongoizes the the normalized queries before execution" do
|
69
|
+
MongoScript.expects(:mongoize_queries).with(normalized_queries)
|
70
|
+
MongoScript.multiquery(queries)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "executes the multiquery routine with the mongoized results" do
|
74
|
+
MongoScript.expects(:execute_readonly_routine).with("multiquery", mongoized_queries).returns({})
|
75
|
+
MongoScript.multiquery(queries)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "processes the results and returns them" do
|
79
|
+
MongoScript.expects(:process_results).with(raw_results, normalized_queries)
|
80
|
+
MongoScript.multiquery(queries)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "processes the results and returns them" do
|
84
|
+
MongoScript.stubs(:process_results).returns(processed_results)
|
85
|
+
MongoScript.multiquery(queries).should == processed_results
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#normalize_queries" do
|
90
|
+
it "doesn't change the underlying hash" do
|
91
|
+
expect {
|
92
|
+
MongoScript.normalize_queries(queries)
|
93
|
+
# inspect will display all info inside the hash
|
94
|
+
# a good proxy to make sure inside values don't change
|
95
|
+
}.not_to change(queries, :inspect)
|
96
|
+
end
|
97
|
+
|
98
|
+
context "for hashes" do
|
99
|
+
let(:normalized_queries) { MongoScript.normalize_queries(queries) }
|
100
|
+
|
101
|
+
context "determining collection" do
|
102
|
+
it "derives the collection from the name if none is provided" do
|
103
|
+
queries[:cars].delete(:collection)
|
104
|
+
# normalized_query isn't executed until we call it,
|
105
|
+
# so the changes to queries are respected
|
106
|
+
normalized_queries[:cars][:collection].to_s.should == "cars"
|
107
|
+
end
|
108
|
+
|
109
|
+
it "leaves the collection alone if it's provided" do
|
110
|
+
queries[:canines][:collection] = :dogs
|
111
|
+
normalized_queries[:canines][:collection].to_s.should == queries[:canines][:collection].to_s
|
112
|
+
end
|
113
|
+
|
114
|
+
it "checks with indifferent access" do
|
115
|
+
queries[:canines].delete(:collection)
|
116
|
+
queries[:canines]["collection"] = :dogs
|
117
|
+
normalized_queries[:canines][:collection].to_s.should == queries[:canines]["collection"].to_s
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "determining the class" do
|
122
|
+
it "uses the klass entry if it's provided" do
|
123
|
+
queries[:cars][:klass] = Car
|
124
|
+
normalized_queries[:cars][:klass].should == Car
|
125
|
+
end
|
126
|
+
|
127
|
+
it "derives the klass (if not provided) from the specified collection (if provided)" do
|
128
|
+
queries[:cars].delete(:klass)
|
129
|
+
queries[:cars][:collection] = :cars
|
130
|
+
normalized_queries[:cars][:klass].should == Car
|
131
|
+
end
|
132
|
+
|
133
|
+
it "derives the klass (if not provided) from the collection (derived from name)" do
|
134
|
+
queries[:cars].delete(:klass)
|
135
|
+
queries[:cars].delete(:collection)
|
136
|
+
normalized_queries[:cars][:klass].should == Car
|
137
|
+
end
|
138
|
+
|
139
|
+
it "sets klass to false if the klass can't be determined from the collection" do
|
140
|
+
queries[:canines].delete(:collection)
|
141
|
+
queries[:canines].delete(:klass)
|
142
|
+
Object.const_defined?("Canine").should be_false
|
143
|
+
normalized_queries[:canines][:klass].should be_false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "for objects processable into queries" do
|
149
|
+
let(:sample_query) {
|
150
|
+
{
|
151
|
+
:cars => stub("Mongoid or other object"),
|
152
|
+
:canines => stub("another object"),
|
153
|
+
:hashy => {:query_type => :hash}
|
154
|
+
}.with_indifferent_access
|
155
|
+
}
|
156
|
+
|
157
|
+
before :each do
|
158
|
+
MongoScript.stubs(:processable_into_parameters?).returns(true)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "sees if it's processable" do
|
162
|
+
MongoScript.stubs(:build_multiquery_parameters)
|
163
|
+
sample_query.values.each do |val|
|
164
|
+
unless val.is_a?(Hash)
|
165
|
+
MongoScript.expects(:processable_into_parameters?).with(val).returns(true)
|
166
|
+
else
|
167
|
+
MongoScript.expects(:processable_into_parameters?).with(val).never
|
168
|
+
end
|
169
|
+
end
|
170
|
+
MongoScript.normalize_queries(sample_query)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "returns the processed values" do
|
174
|
+
# ensure that non-hash values are processed...
|
175
|
+
sample_query.inject({}) do |return_vals, (key, val)|
|
176
|
+
unless val.is_a?(Hash)
|
177
|
+
MongoScript.expects(:build_multiquery_parameters).with(val).returns("my stub value for #{key}")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
# ...and returned appropriately
|
181
|
+
MongoScript.normalize_queries(sample_query).each do |k, v|
|
182
|
+
unless sample_query[k].is_a?(Hash)
|
183
|
+
v.should == "my stub value for #{k}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context "for objects not processable into queries" do
|
190
|
+
let(:sample_query) {
|
191
|
+
{
|
192
|
+
:cars => stub("Mongoid or other object"),
|
193
|
+
:canines => stub("another object"),
|
194
|
+
:hashy => {:query_type => :hash}
|
195
|
+
}.with_indifferent_access
|
196
|
+
}
|
197
|
+
|
198
|
+
it "throws an ArgumentError" do
|
199
|
+
MongoScript.stubs(:processable_into_parameters?).returns(false)
|
200
|
+
expect { MongoScript.normalize_queries(sample_query) }.to raise_exception(ArgumentError)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "#validate_queries!" do
|
206
|
+
it "throws an error if any of the queries are missing a collection" do
|
207
|
+
normalized_queries.first.tap {|k, v| v.delete(:collection) }
|
208
|
+
expect { MongoScript.validate_queries!(normalized_queries) }.to raise_exception(ArgumentError)
|
209
|
+
end
|
210
|
+
|
211
|
+
it "throws an error if any of the queries are missing a klass" do
|
212
|
+
normalized_queries.first.tap {|k, v| v.delete(:klass) }
|
213
|
+
expect { MongoScript.validate_queries!(normalized_queries) }.to raise_exception(ArgumentError)
|
214
|
+
end
|
215
|
+
|
216
|
+
it "has detailed error descriptions"
|
217
|
+
end
|
218
|
+
|
219
|
+
describe "#mongoize_queries" do
|
220
|
+
it "returns copy of queries without the klass value" do
|
221
|
+
mongoized_queries.each_pair do |k, v|
|
222
|
+
v[:klass].should be_nil
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
it "leaves all other values the same" do
|
227
|
+
mongoized_queries.each_pair do |k, v|
|
228
|
+
# avoid symbol/string differences by using JSON
|
229
|
+
MultiJson.encode(v).should == MultiJson.encode(normalized_queries[k].tap {|h| h.delete(:klass) })
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
it "doesn't change the underlying hash" do
|
234
|
+
expect {
|
235
|
+
MongoScript.mongoize_queries(queries)
|
236
|
+
# inspect will display all info inside the hash
|
237
|
+
# a good proxy to make sure inside values don't change
|
238
|
+
}.not_to change(queries, :inspect)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "#process_results" do
|
243
|
+
def process_results(results, queries)
|
244
|
+
results.each_pair do |name, response|
|
245
|
+
if response["error"]
|
246
|
+
results[name] = QueryFailedError.new(name, queries[name], response)
|
247
|
+
else
|
248
|
+
# turn all the individual responses into real objects
|
249
|
+
response.map! {|data| MongoScript.rehydrate(queries[name][:klass], data)}
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
it "rehydrates all objects" do
|
255
|
+
normalized_queries = ObjectWithMultiquery.normalize_queries(queries)
|
256
|
+
processed_results = ObjectWithMultiquery.process_results(results, normalized_queries)
|
257
|
+
|
258
|
+
# in our test case, we could check to make sure that the ids match up
|
259
|
+
# in real life, of course, there's no guarantee the database would return
|
260
|
+
# all the objects we expect
|
261
|
+
processed_results[:canines].each {|d| d.should be_a(Dog)}
|
262
|
+
processed_results[:cars].each {|c| c.should be_a(Car)}
|
263
|
+
end
|
264
|
+
|
265
|
+
context "when a query errors" do
|
266
|
+
before :each do
|
267
|
+
results[:canines] = {"error" => "ssh mongo is sleeping!"}
|
268
|
+
end
|
269
|
+
|
270
|
+
let(:processed_results) {
|
271
|
+
ObjectWithMultiquery.process_results(results, normalized_queries)
|
272
|
+
}
|
273
|
+
|
274
|
+
let(:error) {
|
275
|
+
processed_results[:canines]
|
276
|
+
}
|
277
|
+
|
278
|
+
it "turns any errors into QueryFailedErrors" do
|
279
|
+
error.should be_a(MongoScript::Multiquery::QueryFailedError)
|
280
|
+
end
|
281
|
+
|
282
|
+
it "makes the normalized query available in the error" do
|
283
|
+
error.query_parameters.should == normalized_queries[:canines]
|
284
|
+
end
|
285
|
+
|
286
|
+
it "identifies the query name in the error" do
|
287
|
+
error.query_name.to_s.should == "canines"
|
288
|
+
end
|
289
|
+
|
290
|
+
it "makes the raw db response available in the error" do
|
291
|
+
error.db_response.should == results[:canines]
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
// mock a Mongo DB
|
2
|
+
// if you're using IE < 9, you'll need to implement map
|
3
|
+
// but I doubt that will ever come up :)
|
4
|
+
|
5
|
+
var MockMongo = function(dbName) {
|
6
|
+
var Collection = function(name, data) {
|
7
|
+
// we have to declare this as a local var
|
8
|
+
// since we need to embed it into the find scope
|
9
|
+
var collection = {
|
10
|
+
name: name,
|
11
|
+
find: function(params, fields) {
|
12
|
+
this.findResult = this.findResult || new Query(params, fields, collection);
|
13
|
+
return this.findResult;
|
14
|
+
},
|
15
|
+
data: data || []
|
16
|
+
};
|
17
|
+
return collection;
|
18
|
+
}
|
19
|
+
|
20
|
+
var Query = function(params, fields, collection) {
|
21
|
+
return {
|
22
|
+
params: params,
|
23
|
+
fields: fields,
|
24
|
+
collection: collection,
|
25
|
+
limit: function() { this.limitArgs = arguments; return this; },
|
26
|
+
sort: function() { this.sortArgs = arguments; return this; },
|
27
|
+
toArray: function() {
|
28
|
+
// always return the collection's data
|
29
|
+
return collection.data;
|
30
|
+
},
|
31
|
+
map: function() {
|
32
|
+
var data = this.toArray();
|
33
|
+
return data.map.apply(data, arguments)
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
var prototype = {
|
39
|
+
toString: function() {
|
40
|
+
return this.name;
|
41
|
+
},
|
42
|
+
|
43
|
+
addCollection: function(name, data) {
|
44
|
+
if (!this[name]) {
|
45
|
+
this[name] = new Collection(name, data);
|
46
|
+
|
47
|
+
// set up the system namespaces collection if it doesn't exist
|
48
|
+
if (!this["system.namespaces"]) {
|
49
|
+
this.addCollection("system.namespaces");
|
50
|
+
}
|
51
|
+
|
52
|
+
// and add the entry to the system.namespace array
|
53
|
+
this["system.namespaces"].data.push({
|
54
|
+
name: dbName + "." + name
|
55
|
+
})
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
var db = Object.create(prototype);
|
61
|
+
db.name = dbName;
|
62
|
+
|
63
|
+
return db;
|
64
|
+
}
|
65
|
+
|
66
|
+
var db;
|
67
|
+
|
68
|
+
beforeEach(function() {
|
69
|
+
db = MockMongo("mongoscript_test");
|
70
|
+
db.addCollection("vehicles", [
|
71
|
+
{car: 1},
|
72
|
+
{truck: 2},
|
73
|
+
{spaceship: 100000}
|
74
|
+
])
|
75
|
+
|
76
|
+
db.addCollection("paths", [
|
77
|
+
{road: 1},
|
78
|
+
{rail: 2},
|
79
|
+
{wormhole: 100000}
|
80
|
+
])
|
81
|
+
})
|
82
|
+
|