mongodoc 0.0.0 → 0.1.0

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/README.rdoc CHANGED
@@ -1,16 +1,57 @@
1
1
  = mongodoc
2
2
 
3
- Description goes here.
3
+ Version: 0.1 12/7/09
4
+
5
+ == Introduction
6
+
7
+ mongodoc is yet another ODM for MongoDB in Ruby.
8
+
9
+ Here is an example:
10
+
11
+ require 'mongodoc'
12
+
13
+ class Address < MongoDoc::Document
14
+ key :street
15
+ key :city
16
+ key :state
17
+ key :zip_code
18
+ end
19
+
20
+ class Contact < MongoDoc::Document
21
+ key :name
22
+ key :interests
23
+ has_many :addresses
24
+ end
25
+
26
+ MongoDoc.connect
27
+ contact = Contact.new(:name => 'Joe Strummer', :interests => ['music', 'art', 'acting'])
28
+ contact.addresses << Address.new(:street => '1 Main Street', :city => 'Anywhere', :state => 'ID', :zip_code => '56789')
29
+ contact.save
30
+
31
+ Contact.criteria.in({'interests' => ['ruby', 'art']}).first
32
+ Contact.criteria.where('addresses.state' => 'ID').first
33
+
34
+ == Installation
35
+
36
+ == Credits
37
+
38
+ Les Hill, leshill on github
39
+
40
+ mongodoc
41
+
42
+ === Thanks
43
+
44
+ Thanks to Sandro and Durran for some great conversations and some lovely code.
4
45
 
5
46
  == Note on Patches/Pull Requests
6
-
47
+
7
48
  * Fork the project.
8
49
  * Make your feature addition or bug fix.
9
50
  * Add tests for it. This is important so I don't break it in a
10
51
  future version unintentionally.
11
52
  * Commit, do not mess with rakefile, version, or history.
12
53
  (if you want to have your own version, that is fine but
13
- bump version in a commit by itself I can ignore when I pull)
54
+ bump version in a commit by itself I can ignore when I pull)
14
55
  * Send me a pull request. Bonus points for topic branches.
15
56
 
16
57
  == Copyright
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.0
@@ -1,6 +1,6 @@
1
1
  Given /a new collection named '(.*)'/ do |name|
2
2
  @db.drop_collection(name)
3
- @collection = @db.collection(name)
3
+ @collection = MongoDoc::Collection.new(name)
4
4
  end
5
5
 
6
6
  Given /^an empty (\w+) collection$/ do |name|
@@ -0,0 +1,79 @@
1
+ def query(klass_name = nil)
2
+ return @query if @query or klass_name.nil?
3
+ klass = klass_name.singularize.camelize.constantize
4
+ @query = klass.criteria
5
+ end
6
+
7
+ # When /^I query (.*) with all "([^\"]*)"$/ do |doc, selections_text|
8
+ # selections = eval(selections_text)
9
+ # query(doc).all(selections)
10
+ # end
11
+
12
+ When /^I query (.*) to select fields? "([^\"]*)"$/ do |doc, fields|
13
+ fields = fields.split
14
+ query(doc).select(*fields)
15
+ end
16
+
17
+ When /^I query (.*) where "([^\"]*)"$/ do |doc, where_text|
18
+ where = eval(where_text)
19
+ query(doc).where(where)
20
+ end
21
+
22
+ When /^I query (.*) that excludes "([^\"]*)"$/ do |doc, exclude_text|
23
+ exclude = eval(exclude_text)
24
+ query(doc).excludes(exclude)
25
+ end
26
+
27
+ When /^I set the query on (.*) to (limit|skip) (.*)$/ do |doc, op, count|
28
+ query(doc).send(op, count.to_i)
29
+ end
30
+
31
+
32
+ When /^I query (.*) with (every|not in|in) "([^\"]*)"$/ do |doc, op, hash_text|
33
+ hash = eval(hash_text)
34
+ query(doc).send(op.gsub(' ', '_'), hash)
35
+ end
36
+
37
+ When /^I query (.*) with '(.*)' id$/ do |doc, name|
38
+ object = instance_variable_get("@#{name}")
39
+ query(doc).id(object.id)
40
+ end
41
+
42
+ When /^I set the query extras limit on (.*) to (.*)$/ do |doc, count|
43
+ query(doc).limit(count.to_i)
44
+ end
45
+
46
+ When /^I order the (.*) query by "([^\"]*)"$/ do |doc, order_text|
47
+ order = eval(order_text)
48
+ query(doc).order_by(order)
49
+ end
50
+
51
+ Then /^the (first|last) query result is equal to the document '(.*)'$/ do |position, name|
52
+ object = instance_variable_get("@#{name}")
53
+ query.send(position).should == object
54
+ end
55
+
56
+ Then /^one of the query results is the document '(.*)'$/ do |name|
57
+ object = instance_variable_get("@#{name}")
58
+ query.any? {|doc| doc == object}
59
+ end
60
+
61
+ Then /^the aggregate query result with "(.*)" == "(.*)" has a count of (.*)$/ do |key, value, count|
62
+ result = query.aggregate
63
+ result.find {|r| r.has_key?(key) and r[key] == value }['count'].should == count.to_i
64
+ end
65
+
66
+ Then /^the query result has (.*) documents*$/ do |count|
67
+ query.count.should == count.to_i
68
+ end
69
+
70
+ Then /^the size of the query result is (.*)$/ do |count|
71
+ query.to_a.size.should == count.to_i
72
+ end
73
+
74
+ Then /^the group query result with "([^\"]*)" == "([^\"]*)" has the document '(.*)'$/ do |key, value, name|
75
+ object = instance_variable_get("@#{name}")
76
+ result = query.group
77
+ result.find {|r| r.has_key?(key) and r[key] == value }['group'].should include(object)
78
+ end
79
+
@@ -85,4 +85,3 @@ end
85
85
  Then /^the last return value is false$/ do
86
86
  @last_return.should be_false
87
87
  end
88
-
@@ -0,0 +1,19 @@
1
+ class Address < MongoDoc::Document
2
+ key :street
3
+ key :city
4
+ key :state
5
+ key :zip_code
6
+ end
7
+
8
+ class Place < MongoDoc::Document
9
+ key :name
10
+ key :type
11
+ has_one :address
12
+ end
13
+
14
+ class Contact < MongoDoc::Document
15
+ key :name
16
+ key :type
17
+ key :interests
18
+ has_many :addresses
19
+ end
@@ -1,9 +1,9 @@
1
1
  When /^I save the json '(\{.*\})'$/ do |json_text|
2
- bson = JSON.parse(json_text).to_bson
3
- @last_save = @collection.save(bson)
2
+ json = JSON.parse(json_text)
3
+ @last_save = @collection.save(json)
4
4
  end
5
5
 
6
6
  Then /^the json '(\{.*\})' roundtrips$/ do |json_text|
7
- bson = JSON.parse(json_text).to_bson
8
- MongoDoc::BSON.decode(@collection.find_one(@last_save)).should be_mongo_eql(bson, false)
7
+ json = JSON.parse(json_text)
8
+ @collection.find_one(@last_save).should be_mongo_eql(json, false)
9
9
  end
@@ -22,22 +22,29 @@ Given /^a hash named '(.*)':$/ do |name, table|
22
22
  @all << @last
23
23
  end
24
24
  instance_variable_set("@#{name}", @last)
25
-
26
25
  end
27
26
 
27
+ Given /^'(.*)' has (.*), an array of:$/ do |name, attribute, table|
28
+ object = instance_variable_get("@#{name}")
29
+ object.send(attribute + "=", [])
30
+ table.hashes.each do |hash|
31
+ hash.each {|key, value| object.send(attribute) << value}
32
+ end
33
+ end
34
+
35
+
28
36
  When /^I save the object '(.*)'$/ do |name|
29
37
  object = instance_variable_get("@#{name}")
30
- @last_save = @collection.save(object.to_bson)
38
+ @last_save = @collection.save(object)
31
39
  end
32
40
 
33
41
  Then /^the object '(.*)' roundtrips$/ do |name|
34
42
  object = instance_variable_get("@#{name}")
35
43
  object.instance_variable_set("@_id", @last_save)
36
- MongoDoc::BSON.decode(@collection.find_one(@last_save)).should == object
44
+ @collection.find_one(@last_save).should == object
37
45
  end
38
46
 
39
47
  Then /^the attribute '(.*)' of '(.*)' is '(.*)'$/ do |attr, var, value|
40
48
  object = instance_variable_get("@#{var}")
41
49
  object.send(attr).to_s.should == value
42
50
  end
43
-
@@ -0,0 +1,24 @@
1
+ module ValueEquals
2
+ def ==(other)
3
+ return false unless instance_variables.size == other.instance_variables.size
4
+ instance_variables.all? {|var| self.instance_variable_get(var) == other.instance_variable_get(var)}
5
+ end
6
+ end
7
+
8
+ class Movie
9
+ include ValueEquals
10
+
11
+ attr_accessor :title, :director, :writers
12
+ end
13
+
14
+ class Director
15
+ include ValueEquals
16
+
17
+ attr_accessor :name, :awards
18
+ end
19
+
20
+ class AcademyAward
21
+ include ValueEquals
22
+
23
+ attr_accessor :year, :category
24
+ end
@@ -3,7 +3,5 @@ require 'cucumber'
3
3
  require 'spec/expectations'
4
4
  require 'spec/bson_matchers'
5
5
  require 'mongodoc'
6
- require File.join(File.dirname(__FILE__), '..', '..', 'spec', 'test_classes')
7
- require File.join(File.dirname(__FILE__), '..', '..', 'spec', 'test_documents')
8
6
 
9
7
  World(BsonMatchers)
@@ -0,0 +1,146 @@
1
+ Feature: MongoDoc::Base
2
+
3
+ Background:
4
+ Given a valid connection to the 'test' database
5
+ And an empty Contact document collection
6
+ And a Contact document named 'hashrocket' :
7
+ | Name | Type |
8
+ | Hashrocket | company |
9
+ And 'hashrocket' has interests, an array of:
10
+ | Interest |
11
+ | ruby |
12
+ | rails |
13
+ | employment |
14
+ | contract work |
15
+ | restaurants |
16
+ | hotels |
17
+ | flights |
18
+ | car rentals |
19
+ And 'hashrocket' has many addresses :
20
+ | Street | City | State | Zip Code |
21
+ | 320 First Street North | Jacksonville Beach | FL | 32250 |
22
+ | 1 Lake Michigan Street | Chicago | IL | 60611 |
23
+ | 1 Main Street | Santiago | Chile | |
24
+ And I save the document 'hashrocket'
25
+ And a Contact document named 'rocketeer' :
26
+ | Name |
27
+ | Rocketeer Mike |
28
+ And 'rocketeer' has interests, an array of:
29
+ | Interest |
30
+ | ruby |
31
+ | rails |
32
+ | restaurants |
33
+ | employment |
34
+ And 'rocketeer' has many addresses :
35
+ | Street | City | State | Zip Code |
36
+ | 1 Main Street | Atlantic Beach | FL | 32233 |
37
+ And I save the document 'rocketeer'
38
+ And a Contact document named 'contractor' :
39
+ | Name |
40
+ | Contractor Joe |
41
+ And 'contractor' has interests, an array of:
42
+ | Interest |
43
+ | ruby |
44
+ | rails |
45
+ | contract work |
46
+ | flights |
47
+ | car rentals |
48
+ | hotels |
49
+ | restaurants |
50
+ And 'contractor' has many addresses :
51
+ | Street | City | State | Zip Code |
52
+ | 1 Main St. | Jacksonville | FL | 32218 |
53
+ And I save the document 'contractor'
54
+ And an empty Place document collection
55
+ And a Place document named 'one_ocean' :
56
+ | Name | Type |
57
+ | One Ocean | hotel |
58
+ And 'one_ocean' has one Address as address :
59
+ | Street | City | State | Zip Code |
60
+ | 1 Ocean Street | Atlantic Beach | FL | 32233 |
61
+ And I save the document 'one_ocean'
62
+ And a Place document named 'sea_horse' :
63
+ | Name | Type |
64
+ | Sea Horse | hotel |
65
+ And 'sea_horse' has one Address as address :
66
+ | Street | City | State | Zip Code |
67
+ | 1401 Atlantic Blvd | Neptune Beach | FL | 32266 |
68
+ And I save the document 'sea_horse'
69
+ And a Place document named 'jax' :
70
+ | Name | Type |
71
+ | Jacksonville International Airport | airport |
72
+ And 'jax' has one Address as address :
73
+ | Street | City | State | Zip Code |
74
+ | | Jacksonville | FL | 32218 |
75
+ And I save the document 'jax'
76
+
77
+ Scenario: Counting results
78
+ When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
79
+ Then the query result has 2 documents
80
+
81
+ Scenario: Finding contacts with interests in ruby and rails
82
+ When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
83
+ Then the query result has 2 documents
84
+ And one of the query results is the document 'rocketeer'
85
+
86
+ Scenario: Finding contacts with interests in restaurants and hotels
87
+ When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
88
+ Then the query result has 2 documents
89
+ And one of the query results is the document 'contractor'
90
+
91
+ Scenario: Aggregating Places
92
+ When I query places to select field "type"
93
+ And I query places where "{'address.state' => 'FL'}"
94
+ Then the aggregate query result with "type" == "hotel" has a count of 2
95
+
96
+ Scenario: Excluding places in Neptune Beach
97
+ When I query places to select field "type"
98
+ And I query places that excludes "{'address.city' => 'Neptune Beach'}"
99
+ Then the aggregate query result with "type" == "hotel" has a count of 1
100
+
101
+ Scenario: Using extras to limit results
102
+ When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
103
+ And I set the query extras limit on contacts to 1
104
+ Then the size of the query result is 1
105
+
106
+ Scenario: Finding the first result
107
+ When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
108
+ Then the first query result is equal to the document 'hashrocket'
109
+
110
+ Scenario: Grouping places by type
111
+ When I query places to select field "type"
112
+ And I query places where "{'type' => 'hotel'}"
113
+ Then the group query result with "type" == "hotel" has the document 'one_ocean'
114
+
115
+ Scenario: Selecting contacts with in operator
116
+ When I query contacts with in "{'interests' => ['ruby', 'rails', 'employment']}"
117
+ Then the query result has 3 documents
118
+
119
+ Scenario: Selecting a contact with the id operator
120
+ When I query contacts with 'hashrocket' id
121
+ Then the query result has 1 documents
122
+ And the first query result is equal to the document 'hashrocket'
123
+
124
+ Scenario: Finding the last result
125
+ When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
126
+ Then the last query result is equal to the document 'rocketeer'
127
+
128
+ Scenario: Using limit on results
129
+ When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
130
+ And I set the query on contacts to limit 1
131
+ Then the size of the query result is 1
132
+
133
+ Scenario: Selecting contacts with not in operator
134
+ When I query contacts with not in "{'interests' => ['contract work', 'employment']}"
135
+ Then the query result has 0 documents
136
+
137
+ Scenario: Ordering contacts
138
+ When I query contacts with in "{'interests' => ['ruby', 'rails']}"
139
+ And I order the contacts query by "[[:name, :asc]]"
140
+ Then the first query result is equal to the document 'contractor'
141
+ Then the last query result is equal to the document 'rocketeer'
142
+
143
+ Scenario: Using skip on results
144
+ When I query contacts with every "{'interests' => ['ruby', 'rails']}"
145
+ And I set the query on contacts to skip 1
146
+ Then the size of the query result is 2
@@ -2,96 +2,94 @@ require 'mongodoc/proxy'
2
2
  require 'mongodoc/parent_proxy'
3
3
 
4
4
  module MongoDoc
5
- module Document
6
- module Attributes
7
- def self.extended(klass)
8
- klass.class_inheritable_array :_keys
9
- klass._keys = []
10
- klass.class_inheritable_array :_associations
11
- klass._associations = []
12
-
13
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
14
- attr_accessor :_parent
15
-
16
- def _root
17
- @_root
18
- end
5
+ module Attributes
6
+ def self.extended(klass)
7
+ klass.class_inheritable_array :_keys
8
+ klass._keys = []
9
+ klass.class_inheritable_array :_associations
10
+ klass._associations = []
11
+
12
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
+ attr_accessor :_parent
14
+
15
+ def _root
16
+ @_root
17
+ end
19
18
 
20
- def _root=(root)
21
- @_root = root
22
- _associations.each do|a|
23
- association = send(a)
24
- association._root = root if association
25
- end
19
+ def _root=(root)
20
+ @_root = root
21
+ _associations.each do|a|
22
+ association = send(a)
23
+ association._root = root if association
26
24
  end
25
+ end
27
26
 
28
- def path_to_root(prev)
29
- return prev unless _parent
30
- _parent.path_to_root(prev)
31
- end
32
- RUBY
33
- end
27
+ def path_to_root(prev)
28
+ return prev unless _parent
29
+ _parent.path_to_root(prev)
30
+ end
31
+ RUBY
32
+ end
34
33
 
35
- def _attributes
36
- _keys + _associations
37
- end
34
+ def _attributes
35
+ _keys + _associations
36
+ end
38
37
 
39
- def key(*args)
40
- args.each do |name|
41
- _keys << name unless _keys.include?(name)
42
- attr_accessor name
43
- end
38
+ def key(*args)
39
+ args.each do |name|
40
+ _keys << name unless _keys.include?(name)
41
+ attr_accessor name
44
42
  end
43
+ end
45
44
 
46
- def has_one(*args)
47
- args.each do |name|
48
- _associations << name unless _associations.include?(name)
49
- attr_reader name
50
-
51
- define_method("#{name}=") do |value|
52
- if value
53
- raise NotADocumentError unless Document === value
54
- value._parent = ParentProxy.new(self, name)
55
- value._root = _root || self
56
- end
57
- instance_variable_set("@#{name}", value)
58
- end
45
+ def has_one(*args)
46
+ args.each do |name|
47
+ _associations << name unless _associations.include?(name)
48
+ attr_reader name
59
49
 
60
- validates_associated name
50
+ define_method("#{name}=") do |value|
51
+ if value
52
+ raise NotADocumentError unless Document === value
53
+ value._parent = ParentProxy.new(self, name)
54
+ value._root = _root || self
55
+ end
56
+ instance_variable_set("@#{name}", value)
61
57
  end
58
+
59
+ validates_associated name
62
60
  end
61
+ end
63
62
 
64
- def has_many(*args)
65
- options = args.extract_options!
66
- collection_class = if class_name = options.delete(:class_name)
67
- type_name_with_module(class_name).constantize
68
- end
63
+ def has_many(*args)
64
+ options = args.extract_options!
65
+ collection_class = if class_name = options.delete(:class_name)
66
+ type_name_with_module(class_name).constantize
67
+ end
69
68
 
70
- args.each do |name|
71
- _associations << name unless _associations.include?(name)
69
+ args.each do |name|
70
+ _associations << name unless _associations.include?(name)
72
71
 
73
- define_method("#{name}") do
74
- association = instance_variable_get("@#{name}")
75
- unless association
76
- 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)
77
- instance_variable_set("@#{name}", association)
78
- end
79
- association
72
+ define_method("#{name}") do
73
+ association = instance_variable_get("@#{name}")
74
+ unless association
75
+ 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)
76
+ instance_variable_set("@#{name}", association)
80
77
  end
78
+ association
79
+ end
81
80
 
82
- validates_associated name
81
+ validates_associated name
83
82
 
84
- define_method("#{name}=") do |array|
85
- proxy = send("#{name}")
86
- proxy.clear
87
- proxy << array
88
- end
83
+ define_method("#{name}=") do |array|
84
+ proxy = send("#{name}")
85
+ proxy.clear
86
+ proxy << array
89
87
  end
90
88
  end
89
+ end
91
90
 
92
- def type_name_with_module(type_name)
93
- (/^::/ =~ type_name) ? type_name : "#{parents}::#{type_name}"
94
- end
91
+ def type_name_with_module(type_name)
92
+ (/^::/ =~ type_name) ? type_name : "#{parents}::#{type_name}"
95
93
  end
96
94
  end
97
95
  end
@@ -0,0 +1,45 @@
1
+ require 'mongodoc/cursor'
2
+
3
+ module MongoDoc
4
+ class Collection
5
+ attr_accessor :_collection
6
+ delegate :[], :clear, :count, :create_index, :db, :drop, :drop_index, :drop_indexes, :group, :hint, :index_information, :name, :options, :remove, :rename, :size, :to => :_collection
7
+
8
+ def initialize(name)
9
+ self._collection = self.class.mongo_collection(name)
10
+ end
11
+
12
+ def find(query = {}, options = {})
13
+ cursor = MongoDoc::Cursor.new(_collection.find(query, options))
14
+ if block_given?
15
+ yield cursor
16
+ cursor.close
17
+ else
18
+ cursor
19
+ end
20
+ end
21
+
22
+ def find_one(spec_or_object_id = nil, options = {})
23
+ MongoDoc::BSON.decode(_collection.find_one(spec_or_object_id, options))
24
+ end
25
+
26
+ def insert(doc_or_docs, options = {})
27
+ _collection.insert(doc_or_docs.to_bson, options)
28
+ end
29
+ alias :<< :insert
30
+
31
+ def save(doc, options = {})
32
+ _collection.save(doc.to_bson, options)
33
+ end
34
+
35
+ def update(spec, doc, options = {})
36
+ _collection.update(spec, doc.to_bson, options)
37
+ result = MongoDoc.database.db_command({'getlasterror' => 1})
38
+ (result and result.has_key?('updatedExisting')) ? result['updatedExisting'] : false
39
+ end
40
+
41
+ def self.mongo_collection(name)
42
+ MongoDoc.database.collection(name)
43
+ end
44
+ end
45
+ end
@@ -3,13 +3,13 @@ module MongoDoc
3
3
  if name
4
4
  @@database = connection.db(name)
5
5
  else
6
- raise NoDatabaseError unless defined? @@database
6
+ raise NoDatabaseError unless defined? @@database and @@database
7
7
  @@database
8
8
  end
9
9
  end
10
10
 
11
11
  def self.connection
12
- raise NoConnectionError unless defined? @@connection
12
+ raise NoConnectionError unless defined? @@connection and @@connection
13
13
  @@connection
14
14
  end
15
15