mingo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +5 -0
- data/lib/mingo/cursor.rb +46 -0
- data/lib/mingo/many_proxy.rb +88 -0
- data/lib/mingo.rb +254 -0
- metadata +118 -0
data/Rakefile
ADDED
data/lib/mingo/cursor.rb
ADDED
@@ -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
|
+
|