durran-mongoid 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_STORE
2
+ coverage/*
3
+ pkg/*
4
+ scratch_directory/*
5
+ tmp/*
data/MIT_LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Durran Jordan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,134 @@
1
+ <h2>Overview</h2>
2
+
3
+ <h3>About Mongoid</h3>
4
+
5
+ <p>
6
+ Mongoid is an ODM (Object-Document-Mapper) framework for MongoDB in Ruby. Mongoid differs from other
7
+ mapping frameworks in that it constrains the models into behaving in a manner appropriate for a
8
+ document database. That is to say there are no relationships between documents in the underlying datastore.
9
+ If a relationship is set up in the Model the child models are automatically embedded within the parent document
10
+ in the database. The concept of a foreign key relationship to another Document does not exist, as that is
11
+ design and thinking for a relational database, not a document database. Mongoloid does however provide
12
+ all the ActiveRecord style functionality you need, with the difference that it stores all associations
13
+ within the parent document.
14
+ </p>
15
+
16
+ <h3>Project Tracking</h3>
17
+
18
+ <a href="http://www.pivotaltracker.com/projects/27482">Mongoid on Pivotal Tracker</a>
19
+
20
+ <h3>Compatibility</h3>
21
+
22
+ <p>
23
+ Mongoid is developed against Ruby 1.8.6, 1.8.7, 1.9.1
24
+ </p>
25
+
26
+ <h2>Mongoid in Action</h2>
27
+
28
+ Initialize Mongoid:
29
+
30
+ <pre>
31
+ Mongoid.connect_to("myapp_database_name")
32
+ </pre>
33
+
34
+ Example of a simple domain model:
35
+
36
+ <pre>
37
+ class Person < Mongoid::Document
38
+ fields :title
39
+ has_many :addresses
40
+ has_one :name
41
+ end
42
+
43
+ class Address < Mongoid::Document
44
+ fields \
45
+ :street,
46
+ :city,
47
+ :state,
48
+ :post_code
49
+ belongs_to :person
50
+ end
51
+
52
+ class Name < Mongoid::Document
53
+ fields \
54
+ :first_name,
55
+ :last_name
56
+ end
57
+ </pre>
58
+
59
+ Create a new Document:
60
+
61
+ <pre>
62
+ Person.create(:title => "Esquire")
63
+ </pre>
64
+
65
+ Save a Document:
66
+
67
+ <pre>
68
+ person.save
69
+ </pre>
70
+
71
+ Delete a Document:
72
+
73
+ <pre>
74
+ person.destroy
75
+ </pre>
76
+
77
+ Update a Document:
78
+
79
+ <pre>
80
+ person.update_attributes(:title => "Sir")
81
+ </pre>
82
+
83
+ Search for a Document in the database:
84
+
85
+ <pre>
86
+ Person.find(:all, :title => "Esquire")
87
+
88
+ Person.find(:first, :title => "Esquire")
89
+ </pre>
90
+
91
+ Paginate Document search results:
92
+
93
+ <pre>
94
+ Person.paginate(:title => "Esquire", :page => 1, :per_page => 20)
95
+ </pre>
96
+
97
+ Validations:
98
+
99
+ Mongoid supports all validations provided by Jay Fields' <tt>Validatable</tt> gem.
100
+ For more information please see the <tt>Validatable</tt> documentation.
101
+
102
+ <a href="http://validatable.rubyforge.org/">Validatable on RubyForge</a>
103
+
104
+ <h2>License</h2>
105
+
106
+ <p>
107
+ Copyright (c) 2009 Durran Jordan
108
+ </p>
109
+ <p>
110
+ Permission is hereby granted, free of charge, to any person obtaining
111
+ a copy of this software and associated documentation files (the
112
+ "Software"), to deal in the Software without restriction, including
113
+ without limitation the rights to use, copy, modify, merge, publish,
114
+ distribute, sublicense, and/or sell copies of the Software, and to
115
+ permit persons to whom the Software is furnished to do so, subject to
116
+ the following conditions:
117
+ </p>
118
+ <p>
119
+ The above copyright notice and this permission notice shall be
120
+ included in all copies or substantial portions of the Software.
121
+ </p>
122
+ <p>
123
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
124
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
125
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
126
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
127
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
128
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
129
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
130
+ </p>
131
+
132
+ <h2>Credits</h2>
133
+
134
+ Durran Jordan: durran at gmail dot com
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require "rubygems"
2
+ require "rake"
3
+ require "rake/rdoctask"
4
+ require "spec/rake/spectask"
5
+ require "metric_fu"
6
+
7
+ begin
8
+ require "jeweler"
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "mongoid"
11
+ gem.summary = %Q{Mongoid}
12
+ gem.email = "durran@gmail.com"
13
+ gem.homepage = "http://github.com/durran/mongoid"
14
+ gem.authors = ["Durran Jordan"]
15
+ gem.add_dependency "active_support"
16
+ gem.add_dependency "mongodb-mongo"
17
+ end
18
+
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
21
+ end
22
+
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << "lib" << "spec"
25
+ spec.pattern = "spec/**/*_spec.rb"
26
+ spec.spec_opts = ['--options', "spec/spec.opts"]
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << "lib" << "spec"
31
+ spec.pattern = "spec/**/*_spec.rb"
32
+ spec.spec_opts = ['--options', "spec/spec.opts"]
33
+ spec.rcov = true
34
+ end
35
+
36
+ Rake::RDocTask.new do |rdoc|
37
+ if File.exist?("VERSION.yml")
38
+ config = YAML.load(File.read("VERSION.yml"))
39
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
40
+ else
41
+ version = ""
42
+ end
43
+ rdoc.rdoc_dir = "rdoc"
44
+ rdoc.title = "my_emma #{version}"
45
+ rdoc.rdoc_files.include("README*")
46
+ rdoc.rdoc_files.include("lib/**/*.rb")
47
+ end
48
+
49
+ task :default => ["rcov", "metrics:all"]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/lib/mongoid.rb ADDED
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2009 Durran Jordan
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+ require "rubygems"
22
+
23
+ gem "activesupport", "2.3.4"
24
+ gem "mongodb-mongo", "0.14.1"
25
+ gem "hashrocket-validatable", "1.7.4"
26
+
27
+ require "validatable"
28
+ require "active_support/callbacks"
29
+ require "active_support/core_ext"
30
+ require "delegate"
31
+ require "mongo"
32
+ require "mongoid/associations/association_factory"
33
+ require "mongoid/associations/belongs_to_association"
34
+ require "mongoid/associations/has_many_association"
35
+ require "mongoid/associations/has_one_association"
36
+ require "mongoid/extensions/array/conversions"
37
+ require "mongoid/extensions/object/conversions"
38
+ require "mongoid/extensions"
39
+ require "mongoid/document"
40
+ require "mongoid/paginator"
41
+
42
+ module Mongoid
43
+
44
+ # Thrown when the database connection has not been set up.
45
+ class NoConnectionError < RuntimeError
46
+ end
47
+
48
+ # Thrown when :document_class is not provided in the attributes
49
+ # hash when creating a new Document
50
+ class ClassNotProvidedError < RuntimeError
51
+ end
52
+
53
+ # Thrown when an association is defined on the class, but the
54
+ # attribute in the hash is not an Array or Hash.
55
+ class TypeMismatchError < RuntimeError
56
+ end
57
+
58
+ # Thrown when an association is defined that is not valid. Must
59
+ # be belongs_to, has_many, has_one
60
+ class InvalidAssociationError < RuntimeError
61
+ end
62
+
63
+ # Connect to the database name supplied. This should be run
64
+ # for initial setup, potentially in a rails initializer.
65
+ def self.connect_to(name)
66
+ @@connection ||= Mongo::Connection.new
67
+ @@database ||= @@connection.db(name)
68
+ end
69
+
70
+ # Get the MongoDB database. If initialization via Mongoid.connect_to()
71
+ # has not happened, an exception will occur.
72
+ def self.database
73
+ raise NoConnectionError unless @@database
74
+ @@database
75
+ end
76
+
77
+ end
@@ -0,0 +1,21 @@
1
+ module Mongoid #:nodoc:
2
+ module Associations #:nodoc:
3
+ class AssociationFactory #:nodoc:
4
+
5
+ # Creates a new association, based on the type provided and
6
+ # passes the name and document into the newly instantiated
7
+ # association.
8
+ #
9
+ # If the type is invalid a InvalidAssociationError will be thrown.
10
+ def self.create(association_type, association_name, document)
11
+ case association_type
12
+ when :belongs_to then BelongsToAssociation.new(document)
13
+ when :has_many then HasManyAssociation.new(association_name, document)
14
+ when :has_one then HasOneAssociation.new(association_name, document)
15
+ else raise InvalidAssociationError
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Mongoid #:nodoc:
2
+ module Associations #:nodoc:
3
+ class BelongsToAssociation #:nodoc:
4
+
5
+ # Creates the new association by setting the internal
6
+ # document as the passed in Document. This should be the
7
+ # parent.
8
+ #
9
+ # All method calls on this object will then be delegated
10
+ # to the internal document itself.
11
+ def initialize(document)
12
+ @document = document.parent
13
+ end
14
+
15
+ # All calls to this association will be delegated straight
16
+ # to the encapsulated document.
17
+ def method_missing(method, *args)
18
+ @document.send(method, *args)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Mongoid #:nodoc:
2
+ module Associations #:nodoc:
3
+ class HasManyAssociation < DelegateClass(Array) #:nodoc:
4
+
5
+ # Creates the new association by finding the attributes in
6
+ # the parent document with its name, and instantiating a
7
+ # new document for each one found. These will then be put in an
8
+ # internal array.
9
+ #
10
+ # This then delegated all methods to the array class since this is
11
+ # essentially a proxy to an array itself.
12
+ def initialize(association_name, document)
13
+ klass = association_name.to_s.classify.constantize
14
+ attributes = document.attributes[association_name]
15
+ @documents = attributes ? attributes.collect do |attribute|
16
+ child = klass.new(attribute)
17
+ child.parent = document
18
+ child
19
+ end : []
20
+ super(@documents)
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Mongoid #:nodoc:
2
+ module Associations #:nodoc:
3
+ class HasOneAssociation #:nodoc:
4
+
5
+ # Creates the new association by finding the attributes in
6
+ # the parent document with its name, and instantiating a
7
+ # new document for it.
8
+ #
9
+ # All method calls on this object will then be delegated
10
+ # to the internal document itself.
11
+ def initialize(association_name, document)
12
+ klass = association_name.to_s.titleize.constantize
13
+ attributes = document.attributes[association_name]
14
+ @document = klass.new(attributes)
15
+ @document.parent = document
16
+ end
17
+
18
+ # All calls to this association will be delegated straight
19
+ # to the encapsulated document.
20
+ def method_missing(method, *args)
21
+ @document.send(method, *args)
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,202 @@
1
+ module Mongoid #:nodoc:
2
+ class Document #:nodoc:
3
+ include ActiveSupport::Callbacks
4
+ include Validatable
5
+
6
+ AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }"
7
+ GROUP_BY_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
8
+
9
+ attr_reader :attributes, :parent
10
+
11
+ define_callbacks \
12
+ :after_create,
13
+ :after_save,
14
+ :before_create,
15
+ :before_save
16
+
17
+ class << self
18
+
19
+ # Get an aggregate count for the supplied group of fields and the
20
+ # selector that is provided.
21
+ def aggregate(fields, selector)
22
+ collection.group(fields, selector, { :count => 0 }, AGGREGATE_REDUCE)
23
+ end
24
+
25
+ # Create an association to a parent Document.
26
+ def belongs_to(association_name)
27
+ add_association(:belongs_to, association_name.to_s.classify, association_name)
28
+ end
29
+
30
+ # Get the Mongo::Collection associated with this Document.
31
+ def collection
32
+ @collection_name = self.to_s.demodulize.tableize
33
+ @collection ||= Mongoid.database.collection(@collection_name)
34
+ end
35
+
36
+ # Create a new Document with the supplied attribtues, and insert it into the database.
37
+ def create(attributes = {})
38
+ new(attributes).save
39
+ end
40
+
41
+ # Defines all the fields that are accessable on the Document
42
+ # For each field that is defined, a getter and setter will be
43
+ # added as an instance method to the Document.
44
+ def fields(*names)
45
+ @fields = []
46
+ names.flatten.each do |name|
47
+ @fields << name
48
+ define_method(name) { read_attribute(name) }
49
+ define_method("#{name}=") { |value| write_attribute(name, value) }
50
+ end
51
+ end
52
+
53
+ # Find all Documents in several ways.
54
+ # Model.find(:first, :attribute => "value")
55
+ # Model.find(:all, :attribute => "value")
56
+ def find(*args)
57
+ type, selector = args[0], args[1]
58
+ case type
59
+ when :all then find_all(selector)
60
+ when :first then find_first(selector)
61
+ else find_first(Mongo::ObjectID.from_string(type.to_s))
62
+ end
63
+ end
64
+
65
+ # Find a single Document given the passed selector, which is a Hash of attributes that
66
+ # must match the Document in the database exactly.
67
+ def find_first(selector = nil)
68
+ new(collection.find_one(selector))
69
+ end
70
+
71
+ # Find all Documents given the passed selector, which is a Hash of attributes that
72
+ # must match the Document in the database exactly.
73
+ def find_all(selector = nil)
74
+ collection.find(selector).collect { |doc| new(doc) }
75
+ end
76
+
77
+ # Find all Documents given the supplied criteria, grouped by the fields
78
+ # provided.
79
+ def group_by(fields, selector)
80
+ collection.group(fields, selector, { :group => [] }, GROUP_BY_REDUCE).collect do |docs|
81
+ group!(docs)
82
+ end
83
+ end
84
+
85
+ # Create a one-to-many association between Documents.
86
+ def has_many(association_name)
87
+ add_association(:has_many, association_name.to_s.classify, association_name)
88
+ end
89
+
90
+ # Create a one-to-many association between Documents.
91
+ def has_one(association_name)
92
+ add_association(:has_one, association_name.to_s.titleize, association_name)
93
+ end
94
+
95
+ # Adds an index on the field specified. Options can be :unique => true or
96
+ # :unique => false. It will default to the latter.
97
+ def index(name, options = { :unique => false })
98
+ collection.create_index(name, options)
99
+ end
100
+
101
+ # Find all documents in paginated fashion given the supplied arguments.
102
+ # If no parameters are passed just default to offset 0 and limit 20.
103
+ def paginate(selector = nil, params = {})
104
+ collection.find(selector, Mongoid::Paginator.new(params).options).collect { |doc| new(doc) }
105
+ end
106
+
107
+ end
108
+
109
+ # Get the Mongo::Collection associated with this Document.
110
+ def collection
111
+ self.class.collection
112
+ end
113
+
114
+ # Delete this Document from the database.
115
+ def destroy
116
+ collection.remove(:_id => id)
117
+ end
118
+
119
+ # Get the Mongo::ObjectID associated with this object.
120
+ # This is in essence the primary key.
121
+ def id
122
+ @attributes[:_id]
123
+ end
124
+
125
+ # Instantiate a new Document, setting the Document's attirbutes if given.
126
+ # If no attributes are provided, they will be initialized with an empty Hash.
127
+ def initialize(attributes = {})
128
+ @attributes = attributes.symbolize_keys if attributes
129
+ @attributes = {} unless attributes
130
+ end
131
+
132
+ # Returns true is the Document has not been persisted to the database, false if it has.
133
+ def new_record?
134
+ @attributes[:_id].nil?
135
+ end
136
+
137
+ # Set the parent to this document.
138
+ def parent=(document)
139
+ @parent = document
140
+ end
141
+
142
+ # Save this document to the database. If this document is the root document
143
+ # in the object graph, it will save itself, and return self. If the
144
+ # document is embedded within another document, or is multiple levels down
145
+ # the tree, the root object will get saved, and return itself.
146
+ def save
147
+ if @parent
148
+ @parent.save
149
+ else
150
+ run_callbacks(:before_save)
151
+ collection.save(@attributes)
152
+ run_callbacks(:after_save)
153
+ return self
154
+ end
155
+ end
156
+
157
+ # Returns the id of the Document
158
+ def to_param
159
+ id.to_s
160
+ end
161
+
162
+ # Update the attributes of this Document and return true
163
+ def update_attributes(attributes)
164
+ @attributes = attributes.symbolize_keys!; save; true
165
+ end
166
+
167
+ private
168
+
169
+ class << self
170
+
171
+ # Adds the association to the associations hash with the type as the key,
172
+ # then adds the accessors for the association.
173
+ def add_association(type, class_name, name)
174
+ define_method(name) do
175
+ Mongoid::Associations::AssociationFactory.create(type, name, self)
176
+ end
177
+ define_method("#{name}=") do |object|
178
+ @attributes[name] = object.mongoidize
179
+ end
180
+ end
181
+
182
+ # Takes the supplied raw grouping of documents and alters it to a
183
+ # grouping of actual document objects.
184
+ # TODO: Me no likey this...
185
+ def group!(docs)
186
+ docs["group"] = docs["group"].collect { |attrs| new(attrs) }; docs
187
+ end
188
+
189
+ end
190
+
191
+ # Read from the attributes hash.
192
+ def read_attribute(name)
193
+ @attributes[name.to_sym]
194
+ end
195
+
196
+ # Write to the attributes hash.
197
+ def write_attribute(name, value)
198
+ @attributes[name.to_sym] = value
199
+ end
200
+
201
+ end
202
+ end