mongo_odm 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/Gemfile +11 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +251 -0
  4. data/Rakefile +70 -0
  5. data/lib/mongo_odm.rb +48 -0
  6. data/lib/mongo_odm/collection.rb +42 -0
  7. data/lib/mongo_odm/core_ext/conversions.rb +262 -0
  8. data/lib/mongo_odm/cursor.rb +11 -0
  9. data/lib/mongo_odm/document.rb +34 -0
  10. data/lib/mongo_odm/document/associations.rb +39 -0
  11. data/lib/mongo_odm/document/associations/has_many.rb +19 -0
  12. data/lib/mongo_odm/document/associations/has_one.rb +19 -0
  13. data/lib/mongo_odm/document/attribute_methods.rb +103 -0
  14. data/lib/mongo_odm/document/attribute_methods/dirty.rb +51 -0
  15. data/lib/mongo_odm/document/attribute_methods/localization.rb +67 -0
  16. data/lib/mongo_odm/document/attribute_methods/query.rb +35 -0
  17. data/lib/mongo_odm/document/attribute_methods/read.rb +39 -0
  18. data/lib/mongo_odm/document/attribute_methods/write.rb +37 -0
  19. data/lib/mongo_odm/document/callbacks.rb +29 -0
  20. data/lib/mongo_odm/document/fields.rb +66 -0
  21. data/lib/mongo_odm/document/inspect.rb +28 -0
  22. data/lib/mongo_odm/document/persistence.rb +96 -0
  23. data/lib/mongo_odm/document/validations.rb +46 -0
  24. data/lib/mongo_odm/errors.rb +18 -0
  25. data/lib/mongo_odm/version.rb +7 -0
  26. data/spec/models/00-blank_slate.rb +4 -0
  27. data/spec/models/01-shape.rb +8 -0
  28. data/spec/models/02-circle.rb +7 -0
  29. data/spec/models/03-big_red_circle.rb +8 -0
  30. data/spec/mongo_odm/core_ext/conversions_spec.rb +423 -0
  31. data/spec/mongo_odm/document/fields_spec.rb +187 -0
  32. data/spec/mongo_odm/document/inspect_spec.rb +19 -0
  33. data/spec/mongo_odm/document_spec.rb +12 -0
  34. data/spec/mongo_odm/mongo_odm_spec.rb +84 -0
  35. data/spec/spec.opts +4 -0
  36. data/spec/spec_helper.rb +12 -0
  37. metadata +185 -0
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem "activesupport", "3.0.0.beta3"
4
+ gem "activemodel", "3.0.0.beta3"
5
+ gem "mongo", "~>1.0.2"
6
+ gem "bson_ext", "~>1.0.1"
7
+ gem "tzinfo", "~>0.3.22"
8
+
9
+ group :test do
10
+ gem 'rspec', '~>1.3.0'
11
+ end
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