mongo_odm 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +11 -0
- data/LICENSE +20 -0
- data/README.rdoc +251 -0
- data/Rakefile +70 -0
- data/lib/mongo_odm.rb +48 -0
- data/lib/mongo_odm/collection.rb +42 -0
- data/lib/mongo_odm/core_ext/conversions.rb +262 -0
- data/lib/mongo_odm/cursor.rb +11 -0
- data/lib/mongo_odm/document.rb +34 -0
- data/lib/mongo_odm/document/associations.rb +39 -0
- data/lib/mongo_odm/document/associations/has_many.rb +19 -0
- data/lib/mongo_odm/document/associations/has_one.rb +19 -0
- data/lib/mongo_odm/document/attribute_methods.rb +103 -0
- data/lib/mongo_odm/document/attribute_methods/dirty.rb +51 -0
- data/lib/mongo_odm/document/attribute_methods/localization.rb +67 -0
- data/lib/mongo_odm/document/attribute_methods/query.rb +35 -0
- data/lib/mongo_odm/document/attribute_methods/read.rb +39 -0
- data/lib/mongo_odm/document/attribute_methods/write.rb +37 -0
- data/lib/mongo_odm/document/callbacks.rb +29 -0
- data/lib/mongo_odm/document/fields.rb +66 -0
- data/lib/mongo_odm/document/inspect.rb +28 -0
- data/lib/mongo_odm/document/persistence.rb +96 -0
- data/lib/mongo_odm/document/validations.rb +46 -0
- data/lib/mongo_odm/errors.rb +18 -0
- data/lib/mongo_odm/version.rb +7 -0
- data/spec/models/00-blank_slate.rb +4 -0
- data/spec/models/01-shape.rb +8 -0
- data/spec/models/02-circle.rb +7 -0
- data/spec/models/03-big_red_circle.rb +8 -0
- data/spec/mongo_odm/core_ext/conversions_spec.rb +423 -0
- data/spec/mongo_odm/document/fields_spec.rb +187 -0
- data/spec/mongo_odm/document/inspect_spec.rb +19 -0
- data/spec/mongo_odm/document_spec.rb +12 -0
- data/spec/mongo_odm/mongo_odm_spec.rb +84 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +12 -0
- metadata +185 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2010 Carlos Paramio
|
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.rdoc
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
= mongo_odm
|
2
|
+
|
3
|
+
Flexible persistence module for any Ruby class to MongoDB.
|
4
|
+
|
5
|
+
= Why another ODM for MongoDB?
|
6
|
+
|
7
|
+
MongoMapper, Mongoid, and others, are amazing projects. But I still think it's too much for an ODM for MongoDB.
|
8
|
+
The Mongo Ruby driver allows you to query the database with a nice interface, the only required thing is a way to instanciate
|
9
|
+
the results as objects of predefined classes, so you can encapsulate all the logic under a particular class and don't have to
|
10
|
+
worry about type conversions.
|
11
|
+
|
12
|
+
= Basics
|
13
|
+
|
14
|
+
A piece of code is better than a hundred words:
|
15
|
+
|
16
|
+
class Shape
|
17
|
+
include MongoODM::Document
|
18
|
+
field :name
|
19
|
+
field :x, Float, :default => 0.0
|
20
|
+
field :y, Float, :default => 0.0
|
21
|
+
end
|
22
|
+
|
23
|
+
shape = Shape.new(:name => "Point", :x => 0, :y => 5)
|
24
|
+
shape.save
|
25
|
+
|
26
|
+
# Saves:
|
27
|
+
# { "_id" : ObjectId("4be97178715dd2c4be000006"),
|
28
|
+
# "_class" : "Shape",
|
29
|
+
# "x" : 0,
|
30
|
+
# "y" : 5,
|
31
|
+
# "color" : null,
|
32
|
+
# "name" : "Point"
|
33
|
+
# }
|
34
|
+
|
35
|
+
class Circle < Shape
|
36
|
+
set_collection :shapes # Because we don't want them to be saved on a separated collection
|
37
|
+
field :radius, Float, :default => 1.0
|
38
|
+
end
|
39
|
+
|
40
|
+
circle = Circle.new.save
|
41
|
+
|
42
|
+
# Saves:
|
43
|
+
# { "_id" : ObjectId("4be97203715dd2c4be000007"),
|
44
|
+
# "_class" : "Circle",
|
45
|
+
# "x" : 1,
|
46
|
+
# "y" : 1,
|
47
|
+
# "color" : null,
|
48
|
+
# "radius" : 1 }
|
49
|
+
|
50
|
+
all_shapes = Shape.find.to_a
|
51
|
+
|
52
|
+
# Returns all the shapes; notice they are of different classes:
|
53
|
+
# [ #<Shape x: 0.0, y: 5.0, color: nil, name: "Point", _id: {"$oid"=>"4be97178715dd2c4be000006"}>,
|
54
|
+
# #<Circle x: 1.0, y: 1.0, color: nil, radius: 1.0, _id: {"$oid"=>"4be97293715dd2c4be000008"}> ]
|
55
|
+
|
56
|
+
In fact, you can instanciate any document stored as a hash to the appropiate class. The document just need
|
57
|
+
to have the attribute "_class" set to the name of the class you want to use as the object type. Example:
|
58
|
+
|
59
|
+
MongoODM.instanciate({ :x => 12, :y => 5, '_class' => 'Circle' })
|
60
|
+
|
61
|
+
# Returns:
|
62
|
+
# #<Circle x: 12.0, y: 5.0, color: nil, radius: 1.0>
|
63
|
+
|
64
|
+
= Associations
|
65
|
+
|
66
|
+
To embed just one copy of another class, just define the field type of that class. The class just need to respond to the "type_cast" class method and the "to_mongo" instance method. Example:
|
67
|
+
|
68
|
+
class RGB
|
69
|
+
def initialize(r, g, b)
|
70
|
+
@r, @g, @b = r, g, b
|
71
|
+
end
|
72
|
+
|
73
|
+
def inspect
|
74
|
+
"RGB(#{@r},#{@g},#{@b})"
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_mongo
|
78
|
+
[@r, @g, @b]
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.type_cast(value)
|
82
|
+
return nil if value.nil?
|
83
|
+
return value if value.is_a?(RGB)
|
84
|
+
return new(value[0], value[1], value[2]) if value.is_a?(Array)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Color
|
89
|
+
include MongoODM::Document
|
90
|
+
field :name
|
91
|
+
field :rgb, RGB
|
92
|
+
|
93
|
+
create_index :name, :unique => true
|
94
|
+
end
|
95
|
+
|
96
|
+
color = Color.new(:name => "red", :rgb => RGB.new(255,0,0))
|
97
|
+
color.save
|
98
|
+
|
99
|
+
# Saves:
|
100
|
+
# {"_class":"Color","name":"red","rgb":[255,0,0],"_id":{"$oid": "4bf070fb715dd271c2000001"}}
|
101
|
+
|
102
|
+
red = Color.find({:name => "red"}).first
|
103
|
+
|
104
|
+
# Returns:
|
105
|
+
# #<Color name: "red", rgb: RGB(255,0,0), _id: {"$oid"=>"4bf070fb715dd271c2000001"}>
|
106
|
+
|
107
|
+
|
108
|
+
Of course, if the embedded object's class includes the MongoODM::Document module, you don't need to
|
109
|
+
define those methods. Just define the field as that class:
|
110
|
+
|
111
|
+
class RGB
|
112
|
+
include MongoODM::Document
|
113
|
+
field :r, Fixnum
|
114
|
+
field :g, Fixnum
|
115
|
+
field :b, Fixnum
|
116
|
+
end
|
117
|
+
|
118
|
+
class Color
|
119
|
+
include MongoODM::Document
|
120
|
+
field :name
|
121
|
+
field :rgb, RGB
|
122
|
+
end
|
123
|
+
|
124
|
+
color = Color.new(:name => "red", :rgb => RGB.new(:r => 255, :g => 0, :b => 0))
|
125
|
+
color.save
|
126
|
+
|
127
|
+
# Saves:
|
128
|
+
# {"_class":"Color","name":"red","rgb":{"_class":"RGB","r":255,"g":0,"b":0},"_id":{"$oid": "4bf073e3715dd27212000001"}}
|
129
|
+
|
130
|
+
red = Color.find({:name => "red"}).first
|
131
|
+
|
132
|
+
# Returns:
|
133
|
+
# #<Color name: "red", rgb: #<RGB r: 255, g: 0, b: 0>, _id: {"$oid"=>"4bf073e3715dd27212000001"}>
|
134
|
+
|
135
|
+
|
136
|
+
If you want to save a collection of objects, just define the field as an Array. You can even store objects of different types!
|
137
|
+
|
138
|
+
class Shape
|
139
|
+
include MongoODM::Document
|
140
|
+
field :x, Float
|
141
|
+
field :y, Float
|
142
|
+
end
|
143
|
+
|
144
|
+
class Circle < Shape
|
145
|
+
include MongoODM::Document
|
146
|
+
field :radius, Float
|
147
|
+
end
|
148
|
+
|
149
|
+
class Line < Shape
|
150
|
+
include MongoODM::Document
|
151
|
+
field :dx, Float
|
152
|
+
field :dy, Float
|
153
|
+
end
|
154
|
+
|
155
|
+
class Draw
|
156
|
+
include MongoODM::Document
|
157
|
+
field :objects, Array
|
158
|
+
end
|
159
|
+
|
160
|
+
circle1 = Circle.new(:x => 1, :y => 1, :radius => 10)
|
161
|
+
circle2 = Circle.new(:x => 2, :y => 2, :radius => 20)
|
162
|
+
line = Line.new(:x => 0, :y => 0, :dx => 10, :dy => 5)
|
163
|
+
|
164
|
+
draw = Draw.new(:objects => [circle1, line, circle2])
|
165
|
+
draw.save
|
166
|
+
|
167
|
+
# Saves:
|
168
|
+
# { "_class" : "Draw",
|
169
|
+
# "objects" : [ { "_class" : "Circle",
|
170
|
+
# "x" : 1.0,
|
171
|
+
# "y" : 1.0,
|
172
|
+
# "color" : null,
|
173
|
+
# "radius" : 10.0 },
|
174
|
+
# { "_class" : "Line",
|
175
|
+
# "x" : 0.0,
|
176
|
+
# "y" : 0.0,
|
177
|
+
# "color" : null,
|
178
|
+
# "dx" : 10.0,
|
179
|
+
# "dy" : 5.0},
|
180
|
+
# { "_class" : "Circle",
|
181
|
+
# "x" : 2.0,
|
182
|
+
# "y" : 2.0,
|
183
|
+
# "color" : null,
|
184
|
+
# "radius" : 20.0 } ],
|
185
|
+
# "_id":{"$oid": "4bf0775d715dd2725a000001"}}
|
186
|
+
|
187
|
+
Draw.find_one
|
188
|
+
|
189
|
+
# Returns
|
190
|
+
# #<Draw objects: [#<Circle x: 1.0, y: 1.0, color: nil, radius: 10.0>, #<Line x: 0.0, y: 0.0, color: nil, dx: 10.0, dy: 5.0>, #<Circle x: 2.0, y: 2.0, color: nil, radius: 20.0>], _id: {"$oid"=>"4bf0775d715dd2725a000001"}>
|
191
|
+
|
192
|
+
|
193
|
+
To reference the associated objects instead of embed them, for now you need to define your own methods. Example:
|
194
|
+
|
195
|
+
class Flag
|
196
|
+
include MongoODM::Document
|
197
|
+
field :colors, Array
|
198
|
+
|
199
|
+
def colors
|
200
|
+
Color.find(:_id => {'$in' => read_attribute(:colors)})
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class Color
|
205
|
+
include MongoODM::Document
|
206
|
+
field :name
|
207
|
+
end
|
208
|
+
|
209
|
+
Color.new(:name => "red").save
|
210
|
+
Color.new(:name => "green").save
|
211
|
+
|
212
|
+
flag = Flag.new(:colors => [ Color.find_one(:name => "red").id, Color.find_one(:name => "green").id ])
|
213
|
+
flag.save
|
214
|
+
|
215
|
+
# Saves:
|
216
|
+
# { "_id" : ObjectId("4be96c15715dd2c4be000003"),
|
217
|
+
# "_class" : "Flag",
|
218
|
+
# "colors" : [ ObjectId("4be96bfe715dd2c4be000001"), ObjectId("4be96c08715dd2c4be000002") ]
|
219
|
+
# }
|
220
|
+
|
221
|
+
flag.colors
|
222
|
+
|
223
|
+
# Returns a cursor
|
224
|
+
|
225
|
+
flag.colors.to_a
|
226
|
+
|
227
|
+
# Returns:
|
228
|
+
# [#<Color name: "red", _id: {"$oid"=>"4be96bfe715dd2c4be000001"}>, #<Color name: "green", _id: {"$oid"=>"4be96c08715dd2c4be000002"}>]
|
229
|
+
|
230
|
+
= Callbacks
|
231
|
+
|
232
|
+
For now, the available callbacks are: after_initialize, before_save, after_save
|
233
|
+
|
234
|
+
= Validations
|
235
|
+
|
236
|
+
All the validation methods defined in ActiveModel::Validations are included
|
237
|
+
|
238
|
+
= Dirty
|
239
|
+
|
240
|
+
All the dirty object methods defined in ActiveModel::Dirty are included
|
241
|
+
|
242
|
+
= TODO
|
243
|
+
|
244
|
+
* Add helpers to define attributes as referenced objects
|
245
|
+
* Increase rspec coverage
|
246
|
+
* Document, document, document!
|
247
|
+
* Create useful modules to make common operations easier (versioning, localization, etc)
|
248
|
+
|
249
|
+
= Copyright
|
250
|
+
|
251
|
+
Copyright © 2010 Carlos Paramio. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
## gem building
|
5
|
+
require File.dirname(__FILE__) + "/lib/mongo_odm/version.rb"
|
6
|
+
if Gem.available? 'jeweler'
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gemspec|
|
9
|
+
gemspec.name = "mongo_odm"
|
10
|
+
gemspec.summary = "mongo_odm is a flexible persistence module for any Ruby class to MongoDB."
|
11
|
+
gemspec.description = "mongo_odm is a flexible persistence module for any Ruby class to MongoDB."
|
12
|
+
gemspec.email = "carlos@evolve.st"
|
13
|
+
gemspec.homepage = "http://github.com/carlosparamio/mongo_odm"
|
14
|
+
gemspec.authors = ["Carlos Paramio"]
|
15
|
+
gemspec.files = FileList["[A-Z]*", "{lib,spec}/**/*"]
|
16
|
+
gemspec.version = MongoODM::VERSION
|
17
|
+
gemspec.add_dependency 'activesupport', ['3.0.0.beta3']
|
18
|
+
gemspec.add_dependency 'activemodel', ['3.0.0.beta3']
|
19
|
+
gemspec.add_dependency 'mongo', ['1.0.1']
|
20
|
+
gemspec.add_dependency 'bson', ['1.0.1']
|
21
|
+
gemspec.add_dependency 'bson_ext', ['1.0.1']
|
22
|
+
end
|
23
|
+
Jeweler::GemcutterTasks.new
|
24
|
+
else
|
25
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: gem install jeweler"
|
26
|
+
end
|
27
|
+
|
28
|
+
## rspec tasks
|
29
|
+
if Gem.available? 'rspec'
|
30
|
+
require 'spec/rake/spectask'
|
31
|
+
desc "Run all specs"
|
32
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
33
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
34
|
+
end
|
35
|
+
|
36
|
+
## rcov task
|
37
|
+
if Gem.available? 'rcov'
|
38
|
+
require 'rcov/rcovtask'
|
39
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
40
|
+
spec.libs << "lib" << "spec"
|
41
|
+
spec.pattern = "spec/**/*_spec.rb"
|
42
|
+
spec.spec_opts = ["--options", "spec/spec.opts"]
|
43
|
+
spec.rcov = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
else
|
47
|
+
puts "WARNING: Install rspec to run tests"
|
48
|
+
end
|
49
|
+
|
50
|
+
## yard task
|
51
|
+
if Gem.available? 'yard'
|
52
|
+
require 'yard'
|
53
|
+
YARD::Rake::YardocTask.new do |yard|
|
54
|
+
docfiles = FileList['lib/**/*.rb', 'README.rdoc']
|
55
|
+
yard.files = docfiles
|
56
|
+
yard.options = ["--no-private"]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
task :release => :build do
|
61
|
+
puts "Tagging #{MongoODM::VERSION}..."
|
62
|
+
system "git tag -a #{MongoODM::VERSION} -m 'Release #{MongoODM::VERSION}'"
|
63
|
+
puts "Pushing to Github..."
|
64
|
+
system "git push --tags"
|
65
|
+
puts "Pushing to Gemcutter..."
|
66
|
+
system "gem push pkg/mongo_odm-#{MongoODM::VERSION}.gem"
|
67
|
+
end
|
68
|
+
|
69
|
+
## default task
|
70
|
+
task :default => :spec
|
data/lib/mongo_odm.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rubygems'
|
3
|
+
require 'mongo'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_model'
|
6
|
+
|
7
|
+
# Contains the classes and modules related to the ODM built on top of the basic Mongo client.
|
8
|
+
module MongoODM
|
9
|
+
|
10
|
+
extend ActiveSupport::Autoload
|
11
|
+
include ActiveSupport::Configurable
|
12
|
+
|
13
|
+
autoload :VERSION
|
14
|
+
autoload :Cursor
|
15
|
+
autoload :Collection
|
16
|
+
autoload :Document
|
17
|
+
autoload :Errors
|
18
|
+
|
19
|
+
def self.connection
|
20
|
+
Thread.current[:mongo_odm_connection] ||= Mongo::Connection.new(config[:host], config[:port], config)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.connection=(value)
|
24
|
+
Thread.current[:mongo_odm_connection] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.database
|
28
|
+
connection.db(config[:database] || 'test')
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.config=(value)
|
32
|
+
self.connection = nil
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.instanciate(value)
|
37
|
+
return nil if value.nil?
|
38
|
+
return instanciate_doc(value) if value.is_a?(Hash)
|
39
|
+
return value if value.class.included_modules.include?(MongoODM::Document)
|
40
|
+
value.class.type_cast(value.to_mongo)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.instanciate_doc(doc)
|
44
|
+
return doc if doc["_class"].nil?
|
45
|
+
doc["_class"].constantize.new(doc)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module MongoODM
|
3
|
+
|
4
|
+
class Collection < Mongo::Collection
|
5
|
+
def find(selector={}, opts={})
|
6
|
+
fields = opts.delete(:fields)
|
7
|
+
fields = ["_id"] if fields && fields.empty?
|
8
|
+
skip = opts.delete(:skip) || skip || 0
|
9
|
+
limit = opts.delete(:limit) || 0
|
10
|
+
sort = opts.delete(:sort)
|
11
|
+
hint = opts.delete(:hint)
|
12
|
+
snapshot = opts.delete(:snapshot)
|
13
|
+
batch_size = opts.delete(:batch_size)
|
14
|
+
if opts[:timeout] == false && !block_given?
|
15
|
+
raise ArgumentError, "Timeout can be set to false only when #find is invoked with a block."
|
16
|
+
end
|
17
|
+
timeout = block_given? ? (opts.delete(:timeout) || true) : true
|
18
|
+
if hint
|
19
|
+
hint = normalize_hint_fields(hint)
|
20
|
+
else
|
21
|
+
hint = @hint # assumed to be normalized already
|
22
|
+
end
|
23
|
+
raise RuntimeError, "Unknown options [#{opts.inspect}]" unless opts.empty?
|
24
|
+
|
25
|
+
cursor = MongoODM::Cursor.new(self, :selector => selector, :fields => fields, :skip => skip, :limit => limit,
|
26
|
+
:order => sort, :hint => hint, :snapshot => snapshot, :timeout => timeout, :batch_size => batch_size)
|
27
|
+
if block_given?
|
28
|
+
yield cursor
|
29
|
+
cursor.close()
|
30
|
+
nil
|
31
|
+
else
|
32
|
+
cursor
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def insert(doc_or_docs, options={})
|
37
|
+
doc_or_docs = [doc_or_docs] unless doc_or_docs.is_a?(Array)
|
38
|
+
super doc_or_docs.map{|doc| doc.respond_to?(:to_mongo) ? doc.to_mongo : doc}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|