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 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
+