mongodoc 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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