mingo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|