mingo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ task :default => :spec
2
+
3
+ task :spec do
4
+ exec *%w[bundle exec ruby lib/mingo.rb --color]
5
+ end
@@ -0,0 +1,46 @@
1
+ class Mingo
2
+ # TODO: contribute this to the official driver
3
+ class Cursor < Mongo::Cursor
4
+ module CollectionPlugin
5
+ def find(selector={}, opts={})
6
+ opts = opts.dup
7
+ convert = opts.delete(:convert)
8
+ cursor = Cursor.from_mongo(super(selector, opts), convert)
9
+
10
+ if block_given?
11
+ yield cursor
12
+ cursor.close()
13
+ nil
14
+ else
15
+ cursor
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.from_mongo(cursor, convert)
21
+ new(cursor.collection, :convert => convert).tap do |sub|
22
+ cursor.instance_variables.each { |ivar|
23
+ sub.instance_variable_set(ivar, cursor.instance_variable_get(ivar))
24
+ }
25
+ end
26
+ end
27
+
28
+ def initialize(collection, options={})
29
+ super
30
+ @convert = options[:convert]
31
+ end
32
+
33
+ def next_document
34
+ convert_document super
35
+ end
36
+
37
+ private
38
+
39
+ def convert_document(doc)
40
+ if @convert.nil? or doc.nil? then doc
41
+ elsif @convert.respond_to?(:call) then @convert.call(doc)
42
+ else @convert.new(doc)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,88 @@
1
+ class Mingo
2
+ class ManyProxy
3
+ def self.decorate_with(mod = nil, &block)
4
+ if mod or block_given?
5
+ @decorate_with = mod || Module.new(&block)
6
+ else
7
+ @decorate_with
8
+ end
9
+ end
10
+
11
+ def self.decorate_each(&block)
12
+ if block_given?
13
+ @decorate_each = block
14
+ else
15
+ @decorate_each
16
+ end
17
+ end
18
+
19
+ def initialize(parent, property, model)
20
+ @parent = parent
21
+ @property = property
22
+ @model = model
23
+ @collection = nil
24
+ @embedded = (@parent[@property] ||= [])
25
+ @parent.changes.delete(@property)
26
+ end
27
+
28
+ def find_options
29
+ @find_options ||= begin
30
+ decorator = self.class.decorate_with
31
+ decorate_block = self.class.decorate_each
32
+
33
+ if decorator or decorate_block
34
+ {:convert => lambda { |doc|
35
+ @model.new(doc).tap do |obj|
36
+ obj.extend decorator if decorator
37
+ decorate_block.call(obj, @embedded) if decorate_block
38
+ end
39
+ }}
40
+ else
41
+ {}
42
+ end
43
+ end
44
+ end
45
+
46
+ undef :to_a, :inspect
47
+
48
+ def object_ids
49
+ @embedded
50
+ end
51
+
52
+ def convert(doc)
53
+ doc.id
54
+ end
55
+
56
+ def <<(doc)
57
+ doc = convert(doc)
58
+ @parent.update '$addToSet' => { @property => doc }
59
+ unload_collection
60
+ @embedded << doc
61
+ self
62
+ end
63
+
64
+ def delete(doc)
65
+ doc = convert(doc)
66
+ @parent.update '$pull' => { @property => doc }
67
+ unload_collection
68
+ @embedded.delete doc
69
+ end
70
+
71
+ private
72
+
73
+ def method_missing(method, *args, &block)
74
+ load_collection
75
+ @collection.send(method, *args, &block)
76
+ end
77
+
78
+ def unload_collection
79
+ @collection = nil
80
+ end
81
+
82
+ def load_collection
83
+ @collection ||= if @embedded.empty? then []
84
+ else @model.find({:_id => {'$in' => self.object_ids}}, find_options)
85
+ end
86
+ end
87
+ end
88
+ end
data/lib/mingo.rb ADDED
@@ -0,0 +1,254 @@
1
+ require 'mongo'
2
+ require 'active_model'
3
+ require 'hashie/dash'
4
+
5
+ BSON::ObjectId.class_eval do
6
+ def self.[](id)
7
+ self === id ? id : from_string(id)
8
+ end
9
+ end
10
+
11
+ class Mingo < Hashie::Dash
12
+ # ActiveModel::Callbacks
13
+ include ActiveModel::Conversion
14
+ extend ActiveModel::Translation
15
+
16
+ autoload :Cursor, 'mingo/cursor'
17
+ autoload :ManyProxy, 'mingo/many_proxy'
18
+
19
+ class << self
20
+ attr_writer :db, :collection
21
+
22
+ def db
23
+ @db || superclass.db
24
+ end
25
+
26
+ def connect(dbname_or_uri)
27
+ self.collection = nil
28
+ self.db = if dbname_or_uri.index('mongodb://') == 0
29
+ connection = Mongo::Connection.from_uri(dbname_or_uri)
30
+ connection.db(connection.auths.last['db_name'])
31
+ else
32
+ Mongo::Connection.new.db(dbname_or_uri)
33
+ end
34
+ end
35
+
36
+ def collection_name
37
+ self.name
38
+ end
39
+
40
+ def collection
41
+ @collection ||= db.collection(collection_name).tap { |col|
42
+ col.extend Cursor::CollectionPlugin
43
+ }
44
+ end
45
+
46
+ def first(id_or_selector = nil, options = {})
47
+ unless id_or_selector.nil? or Hash === id_or_selector
48
+ id_or_selector = BSON::ObjectId[id_or_selector]
49
+ end
50
+ collection.find_one(id_or_selector, {:convert => self}.update(options))
51
+ end
52
+
53
+ def find(selector = {}, options = {}, &block)
54
+ collection.find(selector, {:convert => self}.update(options), &block)
55
+ end
56
+
57
+ def create(obj = nil)
58
+ new(obj).tap { |doc| doc.save }
59
+ end
60
+
61
+ def many(property, model, &block)
62
+ proxy_class = block_given?? Class.new(ManyProxy, &block) : ManyProxy
63
+ ivar = "@#{property}"
64
+
65
+ define_method(property) {
66
+ (instance_variable_defined?(ivar) && instance_variable_get(ivar)) ||
67
+ instance_variable_set(ivar, proxy_class.new(self, property, model))
68
+ }
69
+ end
70
+ end
71
+
72
+ attr_reader :changes
73
+
74
+ def initialize(obj = nil)
75
+ @changes = Hash.new { |c, key| c[key] = [self[key]] }
76
+ @destroyed = false
77
+
78
+ if obj and obj['_id'].is_a? BSON::ObjectId
79
+ # a doc loaded straight from the db
80
+ merge!(obj)
81
+ else
82
+ super
83
+ end
84
+ end
85
+
86
+ # overwrite these to avoid checking for declared properties
87
+ # (which is default behavior in Dash)
88
+ def [](property)
89
+ _regular_reader(property.to_s)
90
+ end
91
+
92
+ def []=(property, value)
93
+ _regular_writer(property.to_s, value)
94
+ end
95
+
96
+ def id
97
+ self['_id']
98
+ end
99
+
100
+ def persisted?
101
+ !!id
102
+ end
103
+
104
+ def save(options = {})
105
+ if persisted?
106
+ update(values_for_update, options)
107
+ else
108
+ self['_id'] = self.class.collection.insert(self.to_hash, options)
109
+ end.
110
+ tap { changes.clear }
111
+ end
112
+
113
+ def update(doc, options = {})
114
+ self.class.collection.update({'_id' => self.id}, doc, options)
115
+ end
116
+
117
+ def reload
118
+ doc = self.class.first(id, :convert => nil)
119
+ replace doc
120
+ end
121
+
122
+ def destroy
123
+ self.class.collection.remove('_id' => self.id)
124
+ @destroyed = true
125
+ self.freeze
126
+ end
127
+
128
+ def destroyed?
129
+ @destroyed
130
+ end
131
+
132
+ def changed?
133
+ changes.any?
134
+ end
135
+
136
+ def ==(other)
137
+ other.is_a?(self.class) and other.id == self.id
138
+ end
139
+
140
+ private
141
+
142
+ def values_for_update
143
+ changes.inject('$set' => {}, '$unset' => {}) do |doc, (key, values)|
144
+ value = values[1]
145
+ value.nil? ? (doc['$unset'][key] = 1) : (doc['$set'][key] = value)
146
+ doc
147
+ end
148
+ end
149
+
150
+ def _regular_writer(key, value)
151
+ old_value = _regular_reader(key)
152
+ changes[key.to_sym][1] = value unless value == old_value
153
+ super
154
+ end
155
+ end
156
+
157
+ if $0 == __FILE__
158
+ require 'rspec'
159
+
160
+ Mingo.connect('mingo')
161
+
162
+ class User < Mingo
163
+ property :name
164
+ property :age
165
+ end
166
+
167
+ describe User do
168
+ before :all do
169
+ User.collection.remove
170
+ end
171
+
172
+ it "tracks changes attribute" do
173
+ user = build
174
+ user.should_not be_persisted
175
+ user.should_not be_changed
176
+ user.name = 'Mislav'
177
+ user.should be_changed
178
+ user.changes.keys.should include(:name)
179
+ user.name = 'Mislav2'
180
+ user.changes[:name].should == [nil, 'Mislav2']
181
+ user.save
182
+ user.should be_persisted
183
+ user.should_not be_changed
184
+ user.id.should be_a(BSON::ObjectId)
185
+ end
186
+
187
+ it "has a human model name" do
188
+ described_class.model_name.human.should == 'User'
189
+ end
190
+
191
+ it "can reload values from the db" do
192
+ user = create :name => 'Mislav'
193
+ user.update '$unset' => {:name => 1}, '$set' => {:age => 26}
194
+ user.age.should be_nil
195
+ user.reload
196
+ user.age.should == 26
197
+ user.name.should be_nil
198
+ end
199
+
200
+ it "saves only changed values" do
201
+ user = create :name => 'Mislav', :age => 26
202
+ user.update '$inc' => {:age => 1}
203
+ user.name = 'Mislav2'
204
+ user.save
205
+ user.reload
206
+ user.name.should == 'Mislav2'
207
+ user.age.should == 27
208
+ end
209
+
210
+ it "unsets values set to nil" do
211
+ user = create :name => 'Mislav', :age => 26
212
+ user.age = nil
213
+ user.save
214
+
215
+ raw_doc(user.id).tap do |doc|
216
+ doc.should_not have_key('age')
217
+ doc.should have_key('name')
218
+ end
219
+ end
220
+
221
+ it "finds a doc by string ID" do
222
+ user = create :name => 'Mislav'
223
+ user_dup = described_class.first(user.id.to_s)
224
+ user_dup.id.should == user.id
225
+ user_dup.name.should == 'Mislav'
226
+ end
227
+
228
+ it "returns nil for non-existing doc" do
229
+ doc = described_class.first('nonexist' => 1)
230
+ doc.should be_nil
231
+ end
232
+
233
+ it "compares with another record" do
234
+ one = create :name => "One"
235
+ two = create :name => "Two"
236
+ one.should_not == two
237
+
238
+ one_dup = described_class.first(one.id)
239
+ one_dup.should == one
240
+ end
241
+
242
+ def build(*args)
243
+ described_class.new(*args)
244
+ end
245
+
246
+ def create(*args)
247
+ described_class.create(*args)
248
+ end
249
+
250
+ def raw_doc(selector)
251
+ described_class.first(selector, :convert => nil)
252
+ end
253
+ end
254
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mingo
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - "Mislav Marohni\xC4\x87"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-09-06 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: mongo
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 15
30
+ segments:
31
+ - 1
32
+ - 0
33
+ version: "1.0"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: hashie
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 15
45
+ segments:
46
+ - 0
47
+ - 4
48
+ - 0
49
+ version: 0.4.0
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 62196427
61
+ segments:
62
+ - 2
63
+ - 0
64
+ - 0
65
+ - beta
66
+ - 20
67
+ version: 2.0.0.beta.20
68
+ type: :development
69
+ version_requirements: *id003
70
+ description: Mingo is a minimal document-object mapper for MongoDB.
71
+ email: mislav.marohnic@gmail.com
72
+ executables: []
73
+
74
+ extensions: []
75
+
76
+ extra_rdoc_files: []
77
+
78
+ files:
79
+ - Rakefile
80
+ - lib/mingo/cursor.rb
81
+ - lib/mingo/many_proxy.rb
82
+ - lib/mingo.rb
83
+ has_rdoc: false
84
+ homepage: http://github.com/mislav/mingo
85
+ licenses: []
86
+
87
+ post_install_message:
88
+ rdoc_options: []
89
+
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ hash: 3
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ requirements: []
111
+
112
+ rubyforge_project:
113
+ rubygems_version: 1.3.7
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Minimal Mongo
117
+ test_files: []
118
+