em-mongo 0.1.1
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/lib/em-mongo.rb +30 -0
- data/lib/em-mongo/buffer.rb +414 -0
- data/lib/em-mongo/collection.rb +46 -0
- data/lib/em-mongo/connection.rb +209 -0
- data/spec/buffer_spec.rb +123 -0
- data/spec/collection_spec.rb +142 -0
- data/spec/connection_spec.rb +52 -0
- data/spec/spec_helper.rb +48 -0
- metadata +81 -0
data/lib/em-mongo.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'securerandom'
|
6
|
+
rescue LoadError
|
7
|
+
require 'uuid'
|
8
|
+
end
|
9
|
+
|
10
|
+
module EM::Mongo
|
11
|
+
VERSION = '0.0.1'
|
12
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
13
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
14
|
+
|
15
|
+
class Util
|
16
|
+
def self.unique_id
|
17
|
+
if defined? SecureRandom
|
18
|
+
SecureRandom.hex(12)
|
19
|
+
else
|
20
|
+
UUID.new.generate(:compact).gsub(/^(.{20})(.{8})(.{4})$/){ $1+$3 }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require File.join(EM::Mongo::LIBPATH, "em-mongo/connection")
|
27
|
+
require File.join(EM::Mongo::LIBPATH, "em-mongo/buffer")
|
28
|
+
require File.join(EM::Mongo::LIBPATH, "em-mongo/collection")
|
29
|
+
|
30
|
+
EMMongo = EM::Mongo
|
@@ -0,0 +1,414 @@
|
|
1
|
+
if [].map.respond_to? :with_index
|
2
|
+
class Array
|
3
|
+
def enum_with_index
|
4
|
+
each.with_index
|
5
|
+
end
|
6
|
+
end
|
7
|
+
else
|
8
|
+
require 'enumerator'
|
9
|
+
end
|
10
|
+
|
11
|
+
module EM::Mongo
|
12
|
+
class Buffer
|
13
|
+
class Overflow < Exception; end
|
14
|
+
class InvalidType < Exception; end
|
15
|
+
|
16
|
+
def initialize data = ''
|
17
|
+
@data = data
|
18
|
+
@pos = 0
|
19
|
+
end
|
20
|
+
attr_reader :pos
|
21
|
+
|
22
|
+
def data
|
23
|
+
@data.clone
|
24
|
+
end
|
25
|
+
alias :contents :data
|
26
|
+
alias :to_s :data
|
27
|
+
|
28
|
+
def << data
|
29
|
+
@data << data.to_s
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def length
|
34
|
+
@data.length
|
35
|
+
end
|
36
|
+
alias :size :length
|
37
|
+
|
38
|
+
def empty?
|
39
|
+
pos == length
|
40
|
+
end
|
41
|
+
|
42
|
+
def rewind
|
43
|
+
@pos = 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def read *types
|
47
|
+
values = types.map do |type|
|
48
|
+
case type
|
49
|
+
when :byte
|
50
|
+
_read(1, 'C')
|
51
|
+
when :short
|
52
|
+
_read(2, 'n')
|
53
|
+
when :int
|
54
|
+
_read(4, 'I')
|
55
|
+
when :double
|
56
|
+
_read(8, 'd')
|
57
|
+
when :long
|
58
|
+
_read(4, 'N')
|
59
|
+
when :longlong
|
60
|
+
upper, lower = _read(8, 'NN')
|
61
|
+
upper << 32 | lower
|
62
|
+
when :cstring
|
63
|
+
str = @data.unpack("@#{@pos}Z*").first
|
64
|
+
@data.slice!(@pos, str.size+1)
|
65
|
+
str
|
66
|
+
when :oid
|
67
|
+
# _read(12, 'i*').enum_with_index.inject([]) do |a, (num, i)|
|
68
|
+
# a[ i == 0 ? 1 : i == 1 ? 0 : i ] = num # swap first two
|
69
|
+
# a
|
70
|
+
# end.map do |num|
|
71
|
+
# num.to_s(16).rjust(8,'0')
|
72
|
+
# end.join('')
|
73
|
+
|
74
|
+
_read(12, 'I*').map do |num|
|
75
|
+
num.to_s(16).rjust(8,'0')
|
76
|
+
end.join('')
|
77
|
+
when :bson
|
78
|
+
bson = {}
|
79
|
+
data = Buffer.new _read(read(:int)-4)
|
80
|
+
|
81
|
+
until data.empty?
|
82
|
+
type = data.read(:byte)
|
83
|
+
next if type == 0 # end of object
|
84
|
+
|
85
|
+
key = data.read(:cstring).intern
|
86
|
+
|
87
|
+
bson[key] = case type
|
88
|
+
when 1 # number
|
89
|
+
data.read(:double)
|
90
|
+
when 2 # string
|
91
|
+
data.read(:int)
|
92
|
+
data.read(:cstring)
|
93
|
+
when 3 # object
|
94
|
+
data.read(:bson)
|
95
|
+
when 4 # array
|
96
|
+
data.read(:bson).inject([]){ |a, (k,v)| a[k.to_s.to_i] = v; a }
|
97
|
+
when 5 # binary
|
98
|
+
data._read data.read(:int)
|
99
|
+
when 6 # undefined
|
100
|
+
when 7 # oid
|
101
|
+
data.read(:oid)
|
102
|
+
when 8 # bool
|
103
|
+
data.read(:byte) == 1 ? true : false
|
104
|
+
when 9 # time
|
105
|
+
Time.at data.read(:longlong)/1000.0
|
106
|
+
when 10 # nil
|
107
|
+
nil
|
108
|
+
when 11 # regex
|
109
|
+
source = data.read(:cstring)
|
110
|
+
options = data.read(:cstring).split('')
|
111
|
+
|
112
|
+
options = { 'i' => 1, 'm' => 2, 'x' => 4 }.inject(0) do |s, (o, n)|
|
113
|
+
s |= n if options.include?(o)
|
114
|
+
s
|
115
|
+
end
|
116
|
+
|
117
|
+
Regexp.new(source, options)
|
118
|
+
when 12 # ref
|
119
|
+
ref = {}
|
120
|
+
ref[:_ns] = data.read(:cstring)
|
121
|
+
ref[:_id] = data.read(:oid)
|
122
|
+
ref
|
123
|
+
when 13 # code
|
124
|
+
data.read(:int)
|
125
|
+
data.read(:cstring)
|
126
|
+
when 14 # symbol
|
127
|
+
data.read(:int)
|
128
|
+
data.read(:cstring).intern
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
bson
|
133
|
+
else
|
134
|
+
raise InvalidType, "Cannot read data of type #{type}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
types.size == 1 ? values.first : values
|
139
|
+
end
|
140
|
+
|
141
|
+
def write *args
|
142
|
+
args.each_slice(2) do |type, data|
|
143
|
+
case type
|
144
|
+
when :byte
|
145
|
+
_write(data, 'C')
|
146
|
+
when :short
|
147
|
+
_write(data, 'n')
|
148
|
+
when :int
|
149
|
+
_write(data, 'I')
|
150
|
+
when :double
|
151
|
+
_write(data, 'd')
|
152
|
+
when :long
|
153
|
+
_write(data, 'N')
|
154
|
+
when :longlong
|
155
|
+
lower = data & 0xffffffff
|
156
|
+
upper = (data & ~0xffffffff) >> 32
|
157
|
+
_write([upper, lower], 'NN')
|
158
|
+
when :cstring
|
159
|
+
_write(data.to_s + "\0")
|
160
|
+
when :oid
|
161
|
+
# data.scan(/.{8}/).enum_with_index.inject([]) do |a, (num, i)|
|
162
|
+
# a[ i == 0 ? 1 : i == 1 ? 0 : i ] = num # swap first two
|
163
|
+
# a
|
164
|
+
# end.each do |num|
|
165
|
+
# write(:int, num.to_i(16))
|
166
|
+
# end
|
167
|
+
data.scan(/.{8}/).each do |num|
|
168
|
+
write(:int, num.to_i(16))
|
169
|
+
end
|
170
|
+
when :bson
|
171
|
+
buf = Buffer.new
|
172
|
+
data.each do |key,value|
|
173
|
+
case value
|
174
|
+
when Numeric
|
175
|
+
id = 1
|
176
|
+
type = :double
|
177
|
+
when String
|
178
|
+
if key == :_id
|
179
|
+
id = 7
|
180
|
+
type = :oid
|
181
|
+
else
|
182
|
+
id = 2
|
183
|
+
type = proc{ |out|
|
184
|
+
out.write(:int, value.length+1)
|
185
|
+
out.write(:cstring, value)
|
186
|
+
}
|
187
|
+
end
|
188
|
+
when Hash
|
189
|
+
if data.keys.map{|k| k.to_s}.sort == %w[ _id _ns ]
|
190
|
+
id = 12 # ref
|
191
|
+
type = proc{ |out|
|
192
|
+
out.write(:cstring, data[:_ns])
|
193
|
+
out.write(:oid, data[:_id])
|
194
|
+
}
|
195
|
+
else
|
196
|
+
id = 3
|
197
|
+
type = :bson
|
198
|
+
end
|
199
|
+
when Array
|
200
|
+
id = 4
|
201
|
+
type = :bson
|
202
|
+
value = value.enum_with_index.inject({}){ |h, (v, i)| h.update i => v }
|
203
|
+
when TrueClass, FalseClass
|
204
|
+
id = 8
|
205
|
+
type = :byte
|
206
|
+
value = value ? 1 : 0
|
207
|
+
when Time
|
208
|
+
id = 9
|
209
|
+
type = :longlong
|
210
|
+
value = value.to_i * 1000 + (value.tv_usec/1000)
|
211
|
+
when NilClass
|
212
|
+
id = 10
|
213
|
+
type = nil
|
214
|
+
when Regexp
|
215
|
+
id = 11
|
216
|
+
type = proc{ |out|
|
217
|
+
out.write(:cstring, value.source)
|
218
|
+
out.write(:cstring, { 'i' => 1, 'm' => 2, 'x' => 4 }.inject('') do |s, (o, n)|
|
219
|
+
s += o if value.options & n > 0
|
220
|
+
s
|
221
|
+
end)
|
222
|
+
}
|
223
|
+
when Symbol
|
224
|
+
id = 14
|
225
|
+
type = proc{ |out|
|
226
|
+
out.write(:int, value.to_s.length+1)
|
227
|
+
out.write(:cstring, value.to_s)
|
228
|
+
}
|
229
|
+
end
|
230
|
+
|
231
|
+
buf.write(:byte, id)
|
232
|
+
buf.write(:cstring, key)
|
233
|
+
|
234
|
+
if type.respond_to? :call
|
235
|
+
type.call(buf)
|
236
|
+
elsif type
|
237
|
+
buf.write(type, value)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
buf.write(:byte, 0) # eoo
|
241
|
+
|
242
|
+
write(:int, buf.size+4)
|
243
|
+
_write(buf.to_s)
|
244
|
+
else
|
245
|
+
raise InvalidType, "Cannot write data of type #{type}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
self
|
249
|
+
end
|
250
|
+
|
251
|
+
def _peek(pos, size, pack)
|
252
|
+
data = @data[pos,size]
|
253
|
+
data = data.unpack(pack)
|
254
|
+
data = data.pop if data.size == 1
|
255
|
+
data
|
256
|
+
end
|
257
|
+
|
258
|
+
def _read size, pack = nil
|
259
|
+
if @pos + size > length
|
260
|
+
raise Overflow
|
261
|
+
else
|
262
|
+
data = @data[@pos,size]
|
263
|
+
@data[@pos,size] = ''
|
264
|
+
if pack
|
265
|
+
data = data.unpack(pack)
|
266
|
+
data = data.pop if data.size == 1
|
267
|
+
end
|
268
|
+
data
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def _write data, pack = nil
|
273
|
+
data = [*data].pack(pack) if pack
|
274
|
+
@data[@pos,0] = data
|
275
|
+
@pos += data.length
|
276
|
+
end
|
277
|
+
|
278
|
+
def extract
|
279
|
+
begin
|
280
|
+
cur_data, cur_pos = @data.clone, @pos
|
281
|
+
yield self
|
282
|
+
rescue Overflow
|
283
|
+
@data, @pos = cur_data, cur_pos
|
284
|
+
nil
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
if $0 =~ /bacon/ or $0 == __FILE__
|
291
|
+
require 'bacon'
|
292
|
+
Bacon.summary_on_exit
|
293
|
+
include Mongo
|
294
|
+
|
295
|
+
describe Buffer do
|
296
|
+
before do
|
297
|
+
@buf = Buffer.new
|
298
|
+
end
|
299
|
+
|
300
|
+
should 'have contents' do
|
301
|
+
@buf.contents.should == ''
|
302
|
+
end
|
303
|
+
|
304
|
+
should 'initialize with data' do
|
305
|
+
@buf = Buffer.new('abc')
|
306
|
+
@buf.contents.should == 'abc'
|
307
|
+
end
|
308
|
+
|
309
|
+
should 'append raw data' do
|
310
|
+
@buf << 'abc'
|
311
|
+
@buf << 'def'
|
312
|
+
@buf.contents.should == 'abcdef'
|
313
|
+
end
|
314
|
+
|
315
|
+
should 'append other buffers' do
|
316
|
+
@buf << Buffer.new('abc')
|
317
|
+
@buf.data.should == 'abc'
|
318
|
+
end
|
319
|
+
|
320
|
+
should 'have a position' do
|
321
|
+
@buf.pos.should == 0
|
322
|
+
end
|
323
|
+
|
324
|
+
should 'have a length' do
|
325
|
+
@buf.length.should == 0
|
326
|
+
@buf << 'abc'
|
327
|
+
@buf.length.should == 3
|
328
|
+
end
|
329
|
+
|
330
|
+
should 'know the end' do
|
331
|
+
@buf.empty?.should == true
|
332
|
+
end
|
333
|
+
|
334
|
+
should 'read and write data' do
|
335
|
+
@buf._write('abc')
|
336
|
+
@buf.rewind
|
337
|
+
@buf._read(2).should == 'ab'
|
338
|
+
@buf._read(1).should == 'c'
|
339
|
+
end
|
340
|
+
|
341
|
+
should 'raise on overflow' do
|
342
|
+
lambda{ @buf._read(1) }.should.raise Buffer::Overflow
|
343
|
+
end
|
344
|
+
|
345
|
+
should 'raise on invalid types' do
|
346
|
+
lambda{ @buf.read(:junk) }.should.raise Buffer::InvalidType
|
347
|
+
lambda{ @buf.write(:junk, 1) }.should.raise Buffer::InvalidType
|
348
|
+
end
|
349
|
+
|
350
|
+
{ :byte => 0b10101010,
|
351
|
+
:short => 100,
|
352
|
+
:int => 65536,
|
353
|
+
:double => 123.456,
|
354
|
+
:long => 100_000_000,
|
355
|
+
:longlong => 666_555_444_333_222_111,
|
356
|
+
:cstring => 'hello',
|
357
|
+
}.each do |type, value|
|
358
|
+
|
359
|
+
should "read and write a #{type}" do
|
360
|
+
@buf.write(type, value)
|
361
|
+
@buf.rewind
|
362
|
+
@buf.read(type).should == value
|
363
|
+
@buf.should.be.empty
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
367
|
+
|
368
|
+
should "read and write multiple times" do
|
369
|
+
arr = [ :byte, 0b10101010, :short, 100 ]
|
370
|
+
@buf.write(*arr)
|
371
|
+
@buf.rewind
|
372
|
+
@buf.read(arr.shift).should == arr.shift
|
373
|
+
@buf.read(arr.shift).should == arr.shift
|
374
|
+
end
|
375
|
+
|
376
|
+
[
|
377
|
+
{ :num => 1 },
|
378
|
+
{ :symbol => :abc },
|
379
|
+
{ :object => {} },
|
380
|
+
{ :array => [1, 2, 3] },
|
381
|
+
{ :string => 'abcdefg' },
|
382
|
+
{ :oid => { :_id => '51d9ca7053a2012be4ecd660' } },
|
383
|
+
{ :ref => { :_ns => 'namespace',
|
384
|
+
:_id => '51d9ca7053a2012be4ecd660' } },
|
385
|
+
{ :boolean => true },
|
386
|
+
{ :time => Time.at(Time.now.to_i) },
|
387
|
+
{ :null => nil },
|
388
|
+
{ :regex => /^.*?def/im }
|
389
|
+
]. each do |bson|
|
390
|
+
|
391
|
+
should "read and write bson with #{bson.keys.first}s" do
|
392
|
+
@buf.write(:bson, bson)
|
393
|
+
@buf.rewind
|
394
|
+
@buf.read(:bson).should == bson
|
395
|
+
@buf.should.be.empty
|
396
|
+
end
|
397
|
+
|
398
|
+
end
|
399
|
+
|
400
|
+
should 'do transactional reads with #extract' do
|
401
|
+
@buf.write :byte, 8
|
402
|
+
orig = @buf.to_s
|
403
|
+
|
404
|
+
@buf.rewind
|
405
|
+
@buf.extract do |b|
|
406
|
+
b.read :byte
|
407
|
+
b.read :short
|
408
|
+
end
|
409
|
+
|
410
|
+
@buf.pos.should == 0
|
411
|
+
@buf.data.should == orig
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EM::Mongo
|
2
|
+
class Collection
|
3
|
+
attr_accessor :connection
|
4
|
+
|
5
|
+
def initialize(db, ns, connection = nil)
|
6
|
+
@db = db || "db"
|
7
|
+
@ns = ns || "ns"
|
8
|
+
@name = [@db,@ns].join('.')
|
9
|
+
@connection = connection || EM::Mongo::Connection.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(selector={}, opts={}, &blk)
|
13
|
+
raise "find requires a block" if not block_given?
|
14
|
+
|
15
|
+
skip = opts.delete(:skip) || 0
|
16
|
+
limit = opts.delete(:limit) || 0
|
17
|
+
|
18
|
+
@connection.find(@name, skip, limit, selector, &blk)
|
19
|
+
end
|
20
|
+
|
21
|
+
def first(selector={}, opts={}, &blk)
|
22
|
+
opts[:limit] = 1
|
23
|
+
find(selector, opts) do |res|
|
24
|
+
yield res.first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def insert(obj)
|
29
|
+
obj[:_id] ||= EM::Mongo::Util.unique_id
|
30
|
+
@connection.insert(@name, obj)
|
31
|
+
obj
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove(obj = {})
|
35
|
+
@connection.delete(@name, obj)
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
#def method_missing meth
|
40
|
+
# puts meth
|
41
|
+
# raise ArgumentError, 'collection cannot take block' if block_given?
|
42
|
+
# (@subns ||= {})[meth] ||= self.class.new("#{@ns}.#{meth}", @client)
|
43
|
+
#end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module EM::Mongo
|
2
|
+
DEFAULT_IP = "127.0.0.1"
|
3
|
+
DEFAULT_PORT = 27017
|
4
|
+
DEFAULT_DB = "db"
|
5
|
+
DEFAULT_NS = "ns"
|
6
|
+
|
7
|
+
module EMConnection
|
8
|
+
class Error < Exception;
|
9
|
+
class ConnectionNotBound
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
include EM::Deferrable
|
14
|
+
|
15
|
+
RESERVED = 0
|
16
|
+
OP_REPLY = 1
|
17
|
+
OP_MSG = 1000
|
18
|
+
OP_UPDATE = 2001
|
19
|
+
OP_INSERT = 2002
|
20
|
+
OP_QUERY = 2004
|
21
|
+
OP_DELETE = 2006
|
22
|
+
|
23
|
+
attr_reader :connection
|
24
|
+
|
25
|
+
def responses_pending?
|
26
|
+
@responses.size >= 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def connected?
|
30
|
+
@is_connected
|
31
|
+
end
|
32
|
+
|
33
|
+
# RMongo interface
|
34
|
+
def collection(db = DEFAULT_DB, ns = DEFAULT_NS)
|
35
|
+
raise "Not connected" if not connected?
|
36
|
+
EM::Mongo::Collection.new(db, ns, self)
|
37
|
+
end
|
38
|
+
|
39
|
+
# MongoDB Commands
|
40
|
+
|
41
|
+
def send_command(id, *args, &blk)
|
42
|
+
request_id = @request_id += 1
|
43
|
+
|
44
|
+
callback {
|
45
|
+
buf = Buffer.new
|
46
|
+
buf.write :int, request_id,
|
47
|
+
:int, response = 0,
|
48
|
+
:int, operation = id
|
49
|
+
|
50
|
+
buf.write *args
|
51
|
+
send_data [ buf.size + 4 ].pack('i') # header length first
|
52
|
+
send_data buf.data
|
53
|
+
}
|
54
|
+
|
55
|
+
@responses[request_id] = blk if blk
|
56
|
+
request_id
|
57
|
+
end
|
58
|
+
|
59
|
+
def insert(collection_name, documents)
|
60
|
+
# XXX multiple documents?
|
61
|
+
send_command(OP_INSERT, :int, RESERVED,
|
62
|
+
:cstring, collection_name,
|
63
|
+
:bson, documents)
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete(collection_name, selector)
|
67
|
+
send_command(OP_DELETE, :int, RESERVED,
|
68
|
+
:cstring, collection_name,
|
69
|
+
:int, RESERVED,
|
70
|
+
:bson, selector)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find(collection_name, skip, limit, query, &blk)
|
74
|
+
send_command(OP_QUERY, :int, RESERVED,
|
75
|
+
:cstring, collection_name,
|
76
|
+
:int, skip,
|
77
|
+
:int, limit,
|
78
|
+
:bson, query,
|
79
|
+
&blk)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# EM hooks
|
84
|
+
def initialize(options={})
|
85
|
+
@request_id = 0
|
86
|
+
@responses = {}
|
87
|
+
@is_connected = false
|
88
|
+
@host = options[:host] || DEFAULT_IP
|
89
|
+
@port = options[:port] || DEFAULT_PORT
|
90
|
+
|
91
|
+
@on_close = proc{
|
92
|
+
raise Error, "could not connect to server #{@host}:#{@port}"
|
93
|
+
}
|
94
|
+
timeout options[:timeout] if options[:timeout]
|
95
|
+
errback{ @on_close.call }
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.connect(host = DEFAULT_IP, port = DEFAUL_PORT, timeout = nil)
|
99
|
+
opt = {:host => host, :port => port, :timeout => timeout}
|
100
|
+
EM.connect(host, port, self, opt)
|
101
|
+
end
|
102
|
+
|
103
|
+
def connection_completed
|
104
|
+
log 'connected'
|
105
|
+
@buf = Buffer.new
|
106
|
+
@is_connected = true
|
107
|
+
@on_close = proc{
|
108
|
+
}
|
109
|
+
succeed
|
110
|
+
end
|
111
|
+
|
112
|
+
def receive_data data
|
113
|
+
log "receive_data: #{data.size}"#, data
|
114
|
+
|
115
|
+
@buf << data
|
116
|
+
|
117
|
+
until @buf.empty?
|
118
|
+
size = @buf._peek(0, 4, 'I')
|
119
|
+
|
120
|
+
break unless @buf.size >= size-4
|
121
|
+
|
122
|
+
size, id, response, operation = @buf.read(:int, :int, :int, :int)
|
123
|
+
reserved, cursor, start, num = @buf.read(:int, :longlong, :int, :int)
|
124
|
+
|
125
|
+
results = (1..num).map do
|
126
|
+
@buf.read(:bson)
|
127
|
+
end
|
128
|
+
|
129
|
+
if cb = @responses.delete(response)
|
130
|
+
cb.call(results)
|
131
|
+
end
|
132
|
+
close_connection if @close_pending and @responses.size == 0
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def send_data data
|
137
|
+
log "send_data:#{data.size}"#, data
|
138
|
+
super data
|
139
|
+
end
|
140
|
+
|
141
|
+
def unbind
|
142
|
+
log "unbind"
|
143
|
+
@is_connected = false
|
144
|
+
@on_close.call unless $!
|
145
|
+
end
|
146
|
+
|
147
|
+
def close
|
148
|
+
log "close"
|
149
|
+
@on_close = proc{ yield if block_given? }
|
150
|
+
if @responses.empty?
|
151
|
+
close_connection
|
152
|
+
else
|
153
|
+
@close_pending = true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def log *args
|
160
|
+
return
|
161
|
+
pp args
|
162
|
+
puts
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Make EM::Mongo look like mongo-ruby-driver
|
169
|
+
module EM::Mongo
|
170
|
+
class Database
|
171
|
+
def initialize(name = DEFAULT_DB, connection = nil)
|
172
|
+
@db_name = name
|
173
|
+
@em_connection = connection || EM::Mongo::Connection.new
|
174
|
+
@collection = nil
|
175
|
+
end
|
176
|
+
|
177
|
+
def collection(name = DEFAULT_NS)
|
178
|
+
@collection = EM::Mongo::Collection.new(@db_name, name, @em_connection)
|
179
|
+
end
|
180
|
+
|
181
|
+
def close
|
182
|
+
@em_connection.close
|
183
|
+
end
|
184
|
+
end
|
185
|
+
class Connection
|
186
|
+
def initialize(host = DEFAULT_IP, port = DEFAULT_PORT, timeout = nil)
|
187
|
+
@em_connection = EMConnection.connect(host, port, timeout)
|
188
|
+
@db = {}
|
189
|
+
self
|
190
|
+
end
|
191
|
+
|
192
|
+
def db(name = DEFAULT_DB)
|
193
|
+
@db[name] = EM::Mongo::Database.new(name, @em_connection)
|
194
|
+
end
|
195
|
+
|
196
|
+
def collection(db = DEFAULT_DB, ns = DEFAULT_NS)
|
197
|
+
@em_connection.collection(db, ns)
|
198
|
+
end
|
199
|
+
|
200
|
+
def close
|
201
|
+
@em_connection.close
|
202
|
+
end
|
203
|
+
|
204
|
+
def connected?
|
205
|
+
@em_connection.connected?
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
end
|
data/spec/buffer_spec.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
include EMMongo
|
4
|
+
|
5
|
+
describe Buffer do
|
6
|
+
before do
|
7
|
+
@buf = Buffer.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should have contents' do
|
11
|
+
@buf.contents.should == ''
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should initialize with data' do
|
15
|
+
@buf = Buffer.new('abc')
|
16
|
+
@buf.contents.should == 'abc'
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should append raw data' do
|
20
|
+
@buf << 'abc'
|
21
|
+
@buf << 'def'
|
22
|
+
@buf.contents.should == 'abcdef'
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should append other buffers' do
|
26
|
+
@buf << Buffer.new('abc')
|
27
|
+
@buf.data.should == 'abc'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should have a position' do
|
31
|
+
@buf.pos.should == 0
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should have a length' do
|
35
|
+
@buf.length.should == 0
|
36
|
+
@buf << 'abc'
|
37
|
+
@buf.length.should == 3
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should know the end' do
|
41
|
+
@buf.empty?.should == true
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should read and write data' do
|
45
|
+
@buf._write('abc')
|
46
|
+
@buf.rewind
|
47
|
+
@buf._read(2).should == 'ab'
|
48
|
+
@buf._read(1).should == 'c'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should raise on overflow' do
|
52
|
+
lambda{ @buf._read(1) }.should { raise Buffer::Overflow }
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should raise on invalid types' do
|
56
|
+
lambda{ @buf.read(:junk) }.should { raise Buffer::InvalidType }
|
57
|
+
lambda{ @buf.write(:junk, 1) }.should { raise Buffer::InvalidType }
|
58
|
+
end
|
59
|
+
|
60
|
+
{ :byte => 0b10101010,
|
61
|
+
:short => 100,
|
62
|
+
:int => 65536,
|
63
|
+
:double => 123.456,
|
64
|
+
:long => 100_000_000,
|
65
|
+
:longlong => 666_555_444_333_222_111,
|
66
|
+
:cstring => 'hello',
|
67
|
+
}.each do |type, value|
|
68
|
+
|
69
|
+
it "should read and write a #{type}" do
|
70
|
+
@buf.write(type, value)
|
71
|
+
@buf.rewind
|
72
|
+
@buf.read(type).should == value
|
73
|
+
#@buf.should.be.empty
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should read and write multiple times" do
|
79
|
+
arr = [ :byte, 0b10101010, :short, 100 ]
|
80
|
+
@buf.write(*arr)
|
81
|
+
@buf.rewind
|
82
|
+
@buf.read(arr.shift).should == arr.shift
|
83
|
+
@buf.read(arr.shift).should == arr.shift
|
84
|
+
end
|
85
|
+
|
86
|
+
[
|
87
|
+
{ :num => 1 },
|
88
|
+
{ :symbol => :abc },
|
89
|
+
{ :object => {} },
|
90
|
+
{ :array => [1, 2, 3] },
|
91
|
+
{ :string => 'abcdefg' },
|
92
|
+
{ :oid => { :_id => '51d9ca7053a2012be4ecd660' } },
|
93
|
+
{ :ref => { :_ns => 'namespace',
|
94
|
+
:_id => '51d9ca7053a2012be4ecd660' } },
|
95
|
+
{ :boolean => true },
|
96
|
+
{ :time => Time.at(Time.now.to_i) },
|
97
|
+
{ :null => nil },
|
98
|
+
{ :regex => /^.*?def/im }
|
99
|
+
]. each do |bson|
|
100
|
+
|
101
|
+
it "should read and write bson with #{bson.keys.first}s" do
|
102
|
+
@buf.write(:bson, bson)
|
103
|
+
@buf.rewind
|
104
|
+
@buf.read(:bson).should == bson
|
105
|
+
#@buf.should.be.empty
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should do transactional reads with #extract' do
|
111
|
+
@buf.write :byte, 8
|
112
|
+
orig = @buf.to_s
|
113
|
+
|
114
|
+
@buf.rewind
|
115
|
+
@buf.extract do |b|
|
116
|
+
b.read :byte
|
117
|
+
b.read :short
|
118
|
+
end
|
119
|
+
|
120
|
+
@buf.pos.should == 0
|
121
|
+
@buf.data.should == orig
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe EMMongo::Collection do
|
4
|
+
include EM::SpecHelper
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
@numbers = {
|
8
|
+
1 => 'one',
|
9
|
+
2 => 'two',
|
10
|
+
3 => 'three',
|
11
|
+
4 => 'four',
|
12
|
+
5 => 'five',
|
13
|
+
6 => 'six',
|
14
|
+
7 => 'seven',
|
15
|
+
8 => 'eight',
|
16
|
+
9 => 'nine'
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
after(:all) do
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should insert an object' do
|
24
|
+
EM::Spec::Mongo.collection do |collection|
|
25
|
+
obj = collection.insert(:hello => 'world')
|
26
|
+
obj.keys.should include :_id
|
27
|
+
obj[:_id].should be_a_kind_of String
|
28
|
+
obj[:_id].length.should == 24
|
29
|
+
EM::Spec::Mongo.close
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should find an object' do
|
34
|
+
EM::Spec::Mongo.collection do |collection|
|
35
|
+
collection.insert(:hello => 'world')
|
36
|
+
r = collection.find({:hello => "world"},{}) do |res|
|
37
|
+
res.size.should >= 1
|
38
|
+
res[0][:hello].should == "world"
|
39
|
+
EM::Spec::Mongo.close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should find all objects' do
|
45
|
+
EM::Spec::Mongo.collection do |collection|
|
46
|
+
collection.insert(:one => 'one')
|
47
|
+
collection.insert(:two => 'two')
|
48
|
+
collection.find do |res|
|
49
|
+
res.size.should >= 2
|
50
|
+
EM::Spec::Mongo.close
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should remove an object' do
|
56
|
+
EM::Spec::Mongo.collection do |collection|
|
57
|
+
obj = collection.insert(:hello => 'world')
|
58
|
+
collection.remove(obj)
|
59
|
+
collection.find({:hello => "world"}) do |res|
|
60
|
+
res.size.should == 0
|
61
|
+
EM::Spec::Mongo.close
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should remove all objects' do
|
67
|
+
EM::Spec::Mongo.collection do |collection|
|
68
|
+
collection.insert(:one => 'one')
|
69
|
+
collection.insert(:two => 'two')
|
70
|
+
collection.remove
|
71
|
+
collection.find do |res|
|
72
|
+
res.size.should == 0
|
73
|
+
EM::Spec::Mongo.close
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should insert a complex object' do
|
79
|
+
EM::Spec::Mongo.collection do |collection|
|
80
|
+
obj = {
|
81
|
+
:array => [1,2,3],
|
82
|
+
:float => 123.456,
|
83
|
+
:hash => {:boolean => true},
|
84
|
+
:nil => nil,
|
85
|
+
:symbol => :name,
|
86
|
+
:string => 'hello world',
|
87
|
+
:time => Time.at(Time.now.to_i),
|
88
|
+
:regex => /abc$/ix
|
89
|
+
}
|
90
|
+
obj = collection.insert(obj)
|
91
|
+
collection.find(:_id => obj[:_id]) do |ret|
|
92
|
+
ret.should == [ obj ]
|
93
|
+
EM::Spec::Mongo.close
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should find an object using nested properties' do
|
99
|
+
EM::Spec::Mongo.collection do |collection|
|
100
|
+
collection.insert({
|
101
|
+
:name => 'Google',
|
102
|
+
:address => {
|
103
|
+
:city => 'Mountain View',
|
104
|
+
:state => 'California'}
|
105
|
+
})
|
106
|
+
|
107
|
+
collection.first('address.city' => 'Mountain View') do |res|
|
108
|
+
res[:name].should == 'Google'
|
109
|
+
EM::Spec::Mongo.close
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should find objects with specific values' do
|
115
|
+
EM::Spec::Mongo.collection do |collection|
|
116
|
+
@numbers.each do |num, word|
|
117
|
+
collection.insert(:num => num, :word => word)
|
118
|
+
end
|
119
|
+
|
120
|
+
collection.find({:num => {'$in' => [1,3,5]}}) do |res|
|
121
|
+
res.size.should == 3
|
122
|
+
res.map{|r| r[:num] }.sort.should == [1,3,5]
|
123
|
+
EM::Spec::Mongo.close
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should find objects greater than something' do
|
129
|
+
EM::Spec::Mongo.collection do |collection|
|
130
|
+
@numbers.each do |num, word|
|
131
|
+
collection.insert(:num => num, :word => word)
|
132
|
+
end
|
133
|
+
|
134
|
+
collection.find({:num => {'$gt' => 3}}) do |res|
|
135
|
+
res.size.should == 6
|
136
|
+
res.map{|r| r[:num] }.sort.should == [4,5,6,7,8,9]
|
137
|
+
EM::Spec::Mongo.close
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe EMMongo::Connection do
|
4
|
+
include EM::SpecHelper
|
5
|
+
|
6
|
+
it 'should connect' do
|
7
|
+
em do
|
8
|
+
connection = EMMongo::Connection.new
|
9
|
+
EM.next_tick do
|
10
|
+
connection.connected?.should == true
|
11
|
+
done
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should close' do
|
17
|
+
em do
|
18
|
+
connection = EMMongo::Connection.new
|
19
|
+
EM.next_tick do
|
20
|
+
connection.connected?.should == true
|
21
|
+
connection.close
|
22
|
+
end
|
23
|
+
EM.add_timer(1) do
|
24
|
+
EM.next_tick do
|
25
|
+
connection.connected?.should == false
|
26
|
+
done
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Support the old RMongo interface for now
|
33
|
+
it 'should instantiate a Collection' do
|
34
|
+
EM::Spec::Mongo.connection do |connection|
|
35
|
+
connection.collection.is_a?(Collection).should == true
|
36
|
+
EM::Spec::Mongo.close
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should instantiate a Databse' do
|
41
|
+
EM::Spec::Mongo.connection do |connection|
|
42
|
+
db1 = connection.db
|
43
|
+
db1.is_a?(Database).should == true
|
44
|
+
db2 = connection.db('db2')
|
45
|
+
db2.is_a?(Database).should == true
|
46
|
+
db2.should_not == db1
|
47
|
+
EM::Spec::Mongo.close
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/em-mongo'
|
2
|
+
|
3
|
+
#$LOAD_PATH << File.dirname(__FILE__)+'/../../em-spec/lib'
|
4
|
+
#require File.dirname(__FILE__)+'/../../em-spec/lib/em/spec'
|
5
|
+
#require 'spec'
|
6
|
+
#require File.dirname(__FILE__)+'/../../em-spec/lib/em/spec/rspec'
|
7
|
+
#EM.spec_backend = EventMachine::Spec::Rspec
|
8
|
+
|
9
|
+
require "em-spec/rspec"
|
10
|
+
|
11
|
+
module EM
|
12
|
+
module Spec
|
13
|
+
module Mongo
|
14
|
+
extend EM::SpecHelper
|
15
|
+
|
16
|
+
@@clean_collection_up = nil
|
17
|
+
|
18
|
+
def self.close
|
19
|
+
@@clean_collection_up.call if @@clean_collection_up
|
20
|
+
done
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.collection
|
24
|
+
self.database do |database|
|
25
|
+
database.collection.remove
|
26
|
+
yield database.collection
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.database
|
31
|
+
self.connection do |connection|
|
32
|
+
yield connection.db
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.connection
|
37
|
+
em do
|
38
|
+
connection = EMMongo::Connection.new
|
39
|
+
EM.next_tick do
|
40
|
+
yield connection
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-mongo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- tmm1, bcg
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-03 00:00:00 -05:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 12
|
30
|
+
- 10
|
31
|
+
version: 0.12.10
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: em-mongo
|
35
|
+
email:
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- lib/em-mongo/buffer.rb
|
44
|
+
- lib/em-mongo/collection.rb
|
45
|
+
- lib/em-mongo/connection.rb
|
46
|
+
- lib/em-mongo.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage:
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options:
|
53
|
+
- --charset=UTF-8
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.3.6
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: em-mongo based on rmongo
|
77
|
+
test_files:
|
78
|
+
- spec/buffer_spec.rb
|
79
|
+
- spec/collection_spec.rb
|
80
|
+
- spec/connection_spec.rb
|
81
|
+
- spec/spec_helper.rb
|