mongoscript 0.0.8
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.
- 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
|
+
|