mongodoc 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +143 -0
- data/Rakefile +35 -3
- data/VERSION +1 -1
- data/examples/simple_document.rb +35 -0
- data/examples/simple_object.rb +32 -0
- data/features/finders.feature +72 -0
- data/features/mongodoc_base.feature +12 -2
- data/features/named_scopes.feature +66 -0
- data/features/new_record.feature +36 -0
- data/features/partial_updates.feature +105 -0
- data/features/step_definitions/criteria_steps.rb +4 -41
- data/features/step_definitions/document_steps.rb +56 -5
- data/features/step_definitions/documents.rb +14 -3
- data/features/step_definitions/finder_steps.rb +15 -0
- data/features/step_definitions/named_scope_steps.rb +18 -0
- data/features/step_definitions/partial_update_steps.rb +32 -0
- data/features/step_definitions/query_steps.rb +51 -0
- data/features/using_criteria.feature +5 -1
- data/lib/mongodoc/attributes.rb +76 -63
- data/lib/mongodoc/collection.rb +9 -9
- data/lib/mongodoc/criteria.rb +152 -161
- data/lib/mongodoc/cursor.rb +7 -5
- data/lib/mongodoc/document.rb +95 -31
- data/lib/mongodoc/finders.rb +29 -0
- data/lib/mongodoc/named_scope.rb +68 -0
- data/lib/mongodoc/parent_proxy.rb +15 -6
- data/lib/mongodoc/proxy.rb +22 -13
- data/lib/mongodoc.rb +3 -3
- data/mongodoc.gemspec +42 -14
- data/perf/mongodoc_runner.rb +90 -0
- data/perf/ruby_driver_runner.rb +64 -0
- data/spec/attributes_spec.rb +46 -12
- data/spec/collection_spec.rb +23 -23
- data/spec/criteria_spec.rb +124 -187
- data/spec/cursor_spec.rb +21 -17
- data/spec/document_ext.rb +2 -2
- data/spec/document_spec.rb +187 -218
- data/spec/embedded_save_spec.rb +104 -0
- data/spec/finders_spec.rb +81 -0
- data/spec/hash_matchers.rb +27 -0
- data/spec/named_scope_spec.rb +82 -0
- data/spec/new_record_spec.rb +216 -0
- data/spec/parent_proxy_spec.rb +8 -6
- data/spec/proxy_spec.rb +80 -0
- data/spec/spec_helper.rb +2 -0
- metadata +35 -7
- data/README.rdoc +0 -75
@@ -45,13 +45,26 @@ Given /^I set the id on the document '(.*)' to (.*)$/ do |doc_name, value|
|
|
45
45
|
doc._id = Mongo::ObjectID.new([value.to_i])
|
46
46
|
end
|
47
47
|
|
48
|
+
Given /^'(.+)' has one (.+?) as (.+?) \(identified by '(.+)'\):$/ do |doc_name, class_name, assoc_name, var_name, table|
|
49
|
+
doc = instance_variable_get("@#{doc_name}")
|
50
|
+
obj = class_name.constantize.new
|
51
|
+
table.hashes.each do |hash|
|
52
|
+
hash.each do |key, value|
|
53
|
+
obj.send("#{key.underscore.gsub(' ', '_')}=", value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
instance_variable_set("@#{var_name}", obj)
|
57
|
+
doc.send("#{assoc_name.underscore.gsub(' ', '_')}=", obj)
|
58
|
+
@last = obj
|
59
|
+
end
|
60
|
+
|
48
61
|
When /^I save the document '(.*)'$/ do |name|
|
49
62
|
object = instance_variable_get("@#{name}")
|
50
|
-
|
63
|
+
@last_return = object.save
|
51
64
|
end
|
52
65
|
|
53
66
|
When /^I save the last document$/ do
|
54
|
-
|
67
|
+
@last_return = @last.save
|
55
68
|
end
|
56
69
|
|
57
70
|
When /^I create an (.*) '(.*)' from the hash '(.*)'$/ do |doc, name, hash|
|
@@ -63,7 +76,18 @@ end
|
|
63
76
|
When /^I update the document '(.*)' with the hash named '(.*)'$/ do |doc_name, hash_name|
|
64
77
|
doc = instance_variable_get("@#{doc_name}")
|
65
78
|
attrs = instance_variable_get("@#{hash_name}")
|
66
|
-
|
79
|
+
@last_return = doc.update_attributes(attrs)
|
80
|
+
end
|
81
|
+
|
82
|
+
When /^I query (.*) with find (.*)$/ do |doc, criteria_text|
|
83
|
+
klass = doc.singularize.camelize.constantize
|
84
|
+
criteria = eval(criteria_text)
|
85
|
+
@last_return = klass.find(:all, criteria)
|
86
|
+
end
|
87
|
+
|
88
|
+
When /^'(.+)' is the first (.+?) of '(.+)'$/ do |var_name, single_assoc, doc_name|
|
89
|
+
doc = instance_variable_get("@#{doc_name}")
|
90
|
+
instance_variable_set("@#{var_name}", doc.send(single_assoc.pluralize).first)
|
67
91
|
end
|
68
92
|
|
69
93
|
Then /^'(.*)' is not a new record$/ do |name|
|
@@ -82,6 +106,33 @@ Then /^the document '(.*)' roundtrips$/ do |name|
|
|
82
106
|
instance_variable_set("@#{name}", from_db)
|
83
107
|
end
|
84
108
|
|
85
|
-
Then /^the
|
86
|
-
|
109
|
+
Then /^the document '(.+)' does not roundtrip$/ do |name|
|
110
|
+
object = instance_variable_get("@#{name}")
|
111
|
+
from_db = object.class.find_one(object._id)
|
112
|
+
from_db.should_not == object
|
113
|
+
end
|
114
|
+
|
115
|
+
Then /^the last return value is (.+)$/ do |bool_val|
|
116
|
+
@last_return.should send("be_#{bool_val}")
|
117
|
+
end
|
118
|
+
|
119
|
+
Then /^the first (.*) of '(.*)' is not a new record$/ do |assoc, name|
|
120
|
+
object = instance_variable_get("@#{name}")
|
121
|
+
plural = assoc.pluralize
|
122
|
+
object.send(plural).first.should_not be_new_record
|
123
|
+
end
|
124
|
+
|
125
|
+
Then /^the (\w*) of '(.*)' is not a new record$/ do |assoc, name|
|
126
|
+
object = instance_variable_get("@#{name}")
|
127
|
+
object.send(assoc).should_not be_new_record
|
128
|
+
end
|
129
|
+
|
130
|
+
Then /^the (\w*) of '(.*)' roundtrips$/ do |assoc, name|
|
131
|
+
object = instance_variable_get("@#{name}")
|
132
|
+
from_db = object.class.find_one(object._id)
|
133
|
+
object.send(assoc).id.should == from_db.send(assoc).id
|
134
|
+
end
|
135
|
+
|
136
|
+
Then /^the size of the last return value is (.*)$/ do |count|
|
137
|
+
@last_return.size.should == count.to_i
|
87
138
|
end
|
@@ -1,19 +1,30 @@
|
|
1
|
-
class Address
|
1
|
+
class Address
|
2
|
+
include MongoDoc::Document
|
3
|
+
|
2
4
|
key :street
|
3
5
|
key :city
|
4
6
|
key :state
|
5
7
|
key :zip_code
|
6
8
|
end
|
7
9
|
|
8
|
-
class Place
|
10
|
+
class Place
|
11
|
+
include MongoDoc::Document
|
12
|
+
|
9
13
|
key :name
|
10
14
|
key :type
|
11
15
|
has_one :address
|
12
16
|
end
|
13
17
|
|
14
|
-
class Contact
|
18
|
+
class Contact
|
19
|
+
include MongoDoc::Document
|
20
|
+
|
15
21
|
key :name
|
16
22
|
key :type
|
23
|
+
key :note
|
17
24
|
key :interests
|
18
25
|
has_many :addresses
|
26
|
+
|
27
|
+
named_scope :rubyists, :in => {:interests => ['ruby']}
|
28
|
+
named_scope :contract_work, :in => {:interests => ['contract work']}
|
29
|
+
named_scope :in_state, lambda {|state| { :where => {'addresses.state' => state}}}
|
19
30
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
def finder_query=(finder)
|
2
|
+
@query = finder
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I query (.+) with (\w+)$/ do |doc, finder|
|
6
|
+
self.finder_query = klass(doc).send(finder)
|
7
|
+
end
|
8
|
+
|
9
|
+
When /^I query (.+) to find_one with the id of the '(.+)' document$/ do |collection, doc_name|
|
10
|
+
self.finder_query = klass(collection).find_one(instance_variable_get("@#{doc_name}").id)
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^the query result was (\d+) documents$/ do |count|
|
14
|
+
query.should == count.to_i
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
def scope_query=(scope)
|
2
|
+
@query = scope
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I query (.*) with scope '(.*)'$/ do |doc, scope|
|
6
|
+
self.scope_query = klass(doc).send(scope)
|
7
|
+
end
|
8
|
+
|
9
|
+
When /^I query (.*) with scopes '(.*)'$/ do |doc, scopes|
|
10
|
+
self.scope_query = scopes.split(',').inject(klass(doc)) do |result, scope|
|
11
|
+
result.send(scope.strip)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
When /^I query (.*) with lambda scope '(.*)' with parameters '(.*)'$/ do |doc, scope, params_text|
|
16
|
+
params = params_text.split(',').map(&:strip)
|
17
|
+
self.scope_query = klass(doc).send(scope, *params)
|
18
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
When /^I(\sstrict\s|\s)update the '(.+)' for '(.+)' to '(.+)'$/ do |strict, attr, doc_name, value|
|
2
|
+
doc = instance_variable_get("@#{doc_name}")
|
3
|
+
attrs = {attr => value}
|
4
|
+
attrs.merge!(:__strict__ => true) unless strict.blank?
|
5
|
+
@last_return = doc.update_attributes(attrs)
|
6
|
+
end
|
7
|
+
|
8
|
+
When /^someone else changes the (.+?) '(.+)' of '(.+)' to$/ do |assoc_klass, assoc_name, name, table|
|
9
|
+
orig = instance_variable_get("@#{name}")
|
10
|
+
doc = orig.class.find_one(orig._id)
|
11
|
+
obj = assoc_klass.constantize.new
|
12
|
+
table.hashes.each do |hash|
|
13
|
+
hash.each do |key, value|
|
14
|
+
obj.send("#{key.underscore.gsub(' ', '_')}=", value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
doc.send("#{assoc_name.underscore.gsub(' ', '_')}=", obj)
|
18
|
+
doc.save
|
19
|
+
end
|
20
|
+
|
21
|
+
When /^someone else changes the (.+) of '(.+)':$/ do |assoc_name, name, table|
|
22
|
+
orig = instance_variable_get("@#{name}")
|
23
|
+
doc = orig.class.find_one(orig._id)
|
24
|
+
doc.send(assoc_name).clear
|
25
|
+
table.hashes.each do |hash|
|
26
|
+
doc.send(assoc_name) << hash.inject({}) do |attrs, (attr, value)|
|
27
|
+
attrs["#{attr.underscore.gsub(' ', '_')}"] = value
|
28
|
+
attrs
|
29
|
+
end
|
30
|
+
end
|
31
|
+
doc.save
|
32
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
def klass(klass_name = nil)
|
2
|
+
@klass ||= klass_name.singularize.camelize.constantize
|
3
|
+
end
|
4
|
+
|
5
|
+
def query(klass_name = nil)
|
6
|
+
@query ||= klass(klass_name).criteria
|
7
|
+
end
|
8
|
+
|
9
|
+
Then /^the (first|last) query result is equal to the document '(.*)'$/ do |position, name|
|
10
|
+
object = instance_variable_get("@#{name}")
|
11
|
+
query.send(position).should == object
|
12
|
+
end
|
13
|
+
|
14
|
+
Then /^one of the query results is the document '(.*)'$/ do |name|
|
15
|
+
object = instance_variable_get("@#{name}")
|
16
|
+
query.any? {|doc| doc == object}
|
17
|
+
end
|
18
|
+
|
19
|
+
Then /^the aggregate query result with "(.*)" == "(.*)" has a count of (.*)$/ do |key, value, count|
|
20
|
+
result = query.aggregate
|
21
|
+
result.find {|r| r.has_key?(key) and r[key] == value }['count'].should == count.to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
Then /^the query result has (.*) documents*$/ do |count|
|
25
|
+
if query.respond_to?(:size)
|
26
|
+
query.size.should == count.to_i
|
27
|
+
else
|
28
|
+
query.count.should == count.to_i
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Then /^the size of the query result is (.*)$/ do |count|
|
33
|
+
query.to_a.size.should == count.to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
Then /^the group query result with "([^\"]*)" == "([^\"]*)" has the document '(.*)'$/ do |key, value, name|
|
37
|
+
object = instance_variable_get("@#{name}")
|
38
|
+
result = query.group
|
39
|
+
result.find {|r| r.has_key?(key) and r[key] == value }['group'].should include(object)
|
40
|
+
end
|
41
|
+
|
42
|
+
Then /^the query result is the document '(.*)'$/ do |name|
|
43
|
+
object = instance_variable_get("@#{name}")
|
44
|
+
if query.kind_of?(Array)
|
45
|
+
query.size.should == 1
|
46
|
+
query.first.should == object
|
47
|
+
else
|
48
|
+
query.should == object
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -116,7 +116,7 @@ Feature: MongoDoc::Base
|
|
116
116
|
Then the query result has 3 documents
|
117
117
|
|
118
118
|
Scenario: Selecting a contact with the id operator
|
119
|
-
When I query contacts with 'hashrocket' id
|
119
|
+
When I query contacts with the 'hashrocket' id
|
120
120
|
Then the query result has 1 documents
|
121
121
|
And the first query result is equal to the document 'hashrocket'
|
122
122
|
|
@@ -143,3 +143,7 @@ Feature: MongoDoc::Base
|
|
143
143
|
When I query contacts with every "{'interests' => ['ruby', 'rails']}"
|
144
144
|
And I set the query on contacts to skip 1
|
145
145
|
Then the size of the query result is 2
|
146
|
+
|
147
|
+
Scenario: All
|
148
|
+
When I query contacts with 'all'
|
149
|
+
Then the size of the query result is 3
|
data/lib/mongodoc/attributes.rb
CHANGED
@@ -3,93 +3,106 @@ require 'mongodoc/parent_proxy'
|
|
3
3
|
|
4
4
|
module MongoDoc
|
5
5
|
module Attributes
|
6
|
-
def self.
|
7
|
-
klass.
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
def self.included(klass)
|
7
|
+
klass.class_eval do
|
8
|
+
class_inheritable_array :_keys
|
9
|
+
self._keys = []
|
10
|
+
class_inheritable_array :_associations
|
11
|
+
self._associations = []
|
11
12
|
|
12
|
-
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
13
|
attr_accessor :_parent
|
14
|
+
attr_accessor :_id
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
extend ClassMethods
|
17
|
+
end
|
18
|
+
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
association = send(a)
|
23
|
-
association._root = root if association
|
24
|
-
end
|
25
|
-
end
|
20
|
+
def _root
|
21
|
+
@_root
|
22
|
+
end
|
26
23
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
def _root=(root)
|
25
|
+
@_root = root
|
26
|
+
_associations.each do|a|
|
27
|
+
association = send(a)
|
28
|
+
association._root = root if association
|
29
|
+
end
|
32
30
|
end
|
33
31
|
|
34
|
-
def
|
35
|
-
|
32
|
+
def _path_to_root(src, attrs)
|
33
|
+
return attrs unless _parent
|
34
|
+
_parent._path_to_root(self, attrs)
|
36
35
|
end
|
37
36
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
attr_accessor name
|
42
|
-
end
|
37
|
+
def _selector_path_to_root(selector)
|
38
|
+
return selector unless _parent
|
39
|
+
_parent._selector_path_to_root(selector)
|
43
40
|
end
|
44
41
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
42
|
+
module ClassMethods
|
43
|
+
def _attributes
|
44
|
+
_keys + _associations
|
45
|
+
end
|
49
46
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
value._root = _root || self
|
55
|
-
end
|
56
|
-
instance_variable_set("@#{name}", value)
|
47
|
+
def key(*args)
|
48
|
+
args.each do |name|
|
49
|
+
_keys << name unless _keys.include?(name)
|
50
|
+
attr_accessor name
|
57
51
|
end
|
58
|
-
|
59
|
-
validates_associated name
|
60
52
|
end
|
61
|
-
end
|
62
53
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
54
|
+
def has_one(*args)
|
55
|
+
args.each do |name|
|
56
|
+
_associations << name unless _associations.include?(name)
|
57
|
+
attr_reader name
|
58
|
+
|
59
|
+
define_method("#{name}=") do |value|
|
60
|
+
if value
|
61
|
+
raise NotADocumentError unless Document === value
|
62
|
+
value._parent = ParentProxy.new(self, name)
|
63
|
+
value._root = _root || self
|
64
|
+
value._root.register_save_observer(value)
|
65
|
+
end
|
66
|
+
instance_variable_set("@#{name}", value)
|
67
|
+
end
|
68
|
+
|
69
|
+
validates_associated name
|
70
|
+
end
|
67
71
|
end
|
68
72
|
|
69
|
-
args
|
70
|
-
|
73
|
+
def has_many(*args)
|
74
|
+
options = args.extract_options!
|
75
|
+
collection_class = if class_name = options.delete(:class_name)
|
76
|
+
type_name_with_module(class_name).constantize
|
77
|
+
end
|
71
78
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
79
|
+
args.each do |name|
|
80
|
+
_associations << name unless _associations.include?(name)
|
81
|
+
|
82
|
+
define_method("#{name}") do
|
83
|
+
association = instance_variable_get("@#{name}")
|
84
|
+
unless association
|
85
|
+
association = Proxy.new(:root => _root || self, :parent => self, :assoc_name => name, :collection_class => collection_class || self.class.type_name_with_module(name.to_s.classify).constantize)
|
86
|
+
instance_variable_set("@#{name}", association)
|
87
|
+
end
|
88
|
+
association
|
77
89
|
end
|
78
|
-
association
|
79
|
-
end
|
80
90
|
|
81
|
-
|
91
|
+
validates_associated name
|
82
92
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
93
|
+
define_method("#{name}=") do |arrayish|
|
94
|
+
proxy = send("#{name}")
|
95
|
+
proxy.clear
|
96
|
+
Array.wrap(arrayish).each do|item|
|
97
|
+
proxy << item
|
98
|
+
end
|
99
|
+
end
|
87
100
|
end
|
88
101
|
end
|
89
|
-
end
|
90
102
|
|
91
|
-
|
92
|
-
|
103
|
+
def type_name_with_module(type_name)
|
104
|
+
(/^::/ =~ type_name) ? type_name : "#{parents}::#{type_name}"
|
105
|
+
end
|
93
106
|
end
|
94
107
|
end
|
95
108
|
end
|
data/lib/mongodoc/collection.rb
CHANGED
@@ -4,11 +4,11 @@ module MongoDoc
|
|
4
4
|
class Collection
|
5
5
|
attr_accessor :_collection
|
6
6
|
delegate :[], :clear, :count, :create_index, :db, :drop, :drop_index, :drop_indexes, :group, :hint, :index_information, :name, :options, :remove, :rename, :size, :to => :_collection
|
7
|
-
|
7
|
+
|
8
8
|
def initialize(name)
|
9
9
|
self._collection = self.class.mongo_collection(name)
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def find(query = {}, options = {})
|
13
13
|
cursor = MongoDoc::Cursor.new(_collection.find(query, options))
|
14
14
|
if block_given?
|
@@ -18,28 +18,28 @@ module MongoDoc
|
|
18
18
|
cursor
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def find_one(spec_or_object_id = nil, options = {})
|
23
23
|
MongoDoc::BSON.decode(_collection.find_one(spec_or_object_id, options))
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
def insert(doc_or_docs, options = {})
|
27
27
|
_collection.insert(doc_or_docs.to_bson, options)
|
28
28
|
end
|
29
29
|
alias :<< :insert
|
30
|
-
|
30
|
+
|
31
31
|
def save(doc, options = {})
|
32
32
|
_collection.save(doc.to_bson, options)
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
def update(spec, doc, options = {})
|
36
36
|
_collection.update(spec, doc.to_bson, options)
|
37
|
-
result = MongoDoc.database.
|
37
|
+
result = MongoDoc.database.command({'getlasterror' => 1})
|
38
38
|
(result and result.has_key?('updatedExisting')) ? result['updatedExisting'] : false
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
def self.mongo_collection(name)
|
42
42
|
MongoDoc.database.collection(name)
|
43
43
|
end
|
44
44
|
end
|
45
|
-
end
|
45
|
+
end
|