angelo 0.1.24 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +108 -59
- data/lib/angelo.rb +1 -0
- data/lib/angelo/base.rb +39 -2
- data/lib/angelo/minitest/helpers.rb +12 -2
- data/lib/angelo/responder.rb +39 -3
- data/lib/angelo/responder/eventsource.rb +1 -0
- data/lib/angelo/version.rb +1 -1
- data/test/angelo/eventsource_spec.rb +5 -5
- data/test/angelo_spec.rb +56 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71b7b6db8f9dc6f54045817b05e45895e9a30158
|
4
|
+
data.tar.gz: 0f7c1f1b602476811076b98df9829f15476819f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96b4ed9df8cff7a12a62bbd9e5fcbd7b9ffc912587d094f85beb0dbdf1c59c1e7f0501d8dfdab4f66a960845570731c6582739ce23689a5075d595ceb2db88ea
|
7
|
+
data.tar.gz: 3ebb1e69058a7733e05a5f7f990514a3ebdd001382f20129e7e3862404abc242617fc8cc1d6ee8f0111fc5ecb1f92a8c808c9c1d5af3d0b98e628e8f2927458f
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
changelog
|
2
2
|
=========
|
3
3
|
|
4
|
+
### 0.2.0 23 sep 2014
|
5
|
+
|
6
|
+
* chunked responses with `transfer_encoding :chunked` and `return_obj.each`
|
7
|
+
* chunked responses with `chunked_response(){|r| ... }`
|
8
|
+
* `event` and `message` helpers on sse objects
|
9
|
+
|
4
10
|
### 0.1.24 19 sep 2014
|
5
11
|
|
6
12
|
thanks: @chewi
|
data/README.md
CHANGED
@@ -26,16 +26,21 @@ Celluloid::IO and gives you a reactor with evented IO in Ruby!**
|
|
26
26
|
Note: There currently is no "standalone" capability where one can define route handlers at the top level.
|
27
27
|
|
28
28
|
Things will feel very familiar to anyone experienced with Sinatra. Inside the subclass, you can define
|
29
|
-
route handlers denoted by HTTP verb and path.
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
after blocks are handled. See the Errors section below for more info.
|
29
|
+
route handlers denoted by HTTP verb and path. One acceptable return value from a route block is the body
|
30
|
+
of the response in full as a `String`. Another is a `Hash` if the content type is set to `:json`. Finally,
|
31
|
+
you may return any object that responds to `#each(&block)` if the transfer encoding is set to `:chunked`.
|
32
|
+
There is also a `chunked_response` helper that will take a block, set the transfer encoding, and return
|
33
|
+
an appropriate object for you.
|
35
34
|
|
36
35
|
There is also [Mustermann](#mustermann) support for full-on, Sinatra-like path
|
37
36
|
matching and params.
|
38
37
|
|
38
|
+
Angelo also features `before` and `after` blocks, just like Sinatra. Filters are ordered as defined,
|
39
|
+
and called in that order. When defined without a path, they run for all matched requests. With a path,
|
40
|
+
the path must match exactly for the block to be called. If `Angelo::Mustermann` is included, the paths
|
41
|
+
are interpreted as a Mustermann pattern and params are merged. For more info on the difference in how
|
42
|
+
after blocks are handled, see the Errors section below for more info.
|
43
|
+
|
39
44
|
### Websockets!
|
40
45
|
|
41
46
|
One of the main motivations for Angelo was the ability to define websocket handlers with ease. Through
|
@@ -48,7 +53,7 @@ The `websocket` route builder accepts a path and a block, and passes the actual
|
|
48
53
|
as the only argument. This socket is an instance of Reel's
|
49
54
|
[WebSocket](https://github.com/celluloid/reel/blob/master/lib/reel/websocket.rb) class, and, as such,
|
50
55
|
responds to methods like `on_message` and `on_close`. A service-wide `on_pong` handler (defined at the
|
51
|
-
|
56
|
+
|
52
57
|
a connected websocket client.
|
53
58
|
|
54
59
|
##### `websockets` helper
|
@@ -130,14 +135,23 @@ sse.addEventListener('foo', function(e){ console.log("got foo event!\n" + JSON.p
|
|
130
135
|
The `sse_event` helper accepts a normal `String` for the data, but will automatically convert a `Hash`
|
131
136
|
argument to a JSON object.
|
132
137
|
|
138
|
+
NOTE: there is a shortcut helper on the actual SSE object itself:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
eventsource '/sse' do |sse|
|
142
|
+
sse.event :foo, some_key: 'blah', other_key: 'boo'
|
143
|
+
sse.event :close
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
133
147
|
##### `sse_message` helper
|
134
148
|
|
135
149
|
The `sse_message` helper behaves exactly the same as `sse_event`, but does not take an event name:
|
136
150
|
|
137
151
|
```ruby
|
138
152
|
eventsource '/sse' do |s|
|
139
|
-
msg =
|
140
|
-
s.write msg
|
153
|
+
msg = sse_message some_key: 'blah', other_key: 'boo'
|
154
|
+
s.write msg
|
141
155
|
s.close
|
142
156
|
end
|
143
157
|
```
|
@@ -149,6 +163,15 @@ var sse = new EventSource('/sse');
|
|
149
163
|
sse.onmessage = function(e){ console.log("got message!\n" + JSON.parse(e.data)); };
|
150
164
|
```
|
151
165
|
|
166
|
+
NOTE: there is a shortcut helper on the actual SSE object itself:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
eventsource '/sse' do |sse|
|
170
|
+
sse.message some_key: 'blah', other_key: 'boo'
|
171
|
+
sse.event :close
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
152
175
|
##### `sses` helper
|
153
176
|
|
154
177
|
Angelo also includes a "stash" helper for SSE connections. One can `<<` a socket into `sses` from
|
@@ -355,6 +378,67 @@ Content-Length: 18
|
|
355
378
|
everything's fine
|
356
379
|
```
|
357
380
|
|
381
|
+
### [Tilt](https://github.com/rtomayko/tilt) / ERB
|
382
|
+
|
383
|
+
To make `erb` available in route blocks
|
384
|
+
|
385
|
+
1. add `tilt` to your `Gemfile`: `gem 'tilt'`
|
386
|
+
2. require `angelo/tilt/erb`
|
387
|
+
3. include `Angelo::Tilt::ERB` in your app
|
388
|
+
|
389
|
+
```ruby
|
390
|
+
class Foo < Angelo::Base
|
391
|
+
include Angelo::Tilt::ERB
|
392
|
+
|
393
|
+
@@views = 'some/other/path' # defaults to './views'
|
394
|
+
|
395
|
+
get '/' do
|
396
|
+
erb :index
|
397
|
+
end
|
398
|
+
|
399
|
+
end
|
400
|
+
```
|
401
|
+
|
402
|
+
### [Mustermann](https://github.com/rkh/mustermann)
|
403
|
+
|
404
|
+
To make routes blocks match path with Mustermann patterns
|
405
|
+
|
406
|
+
1. be using ruby >=2.0.0
|
407
|
+
2. add 'mustermann' to to your `Gemfile`: `platform(:ruby_20){ gem 'mustermann' }`
|
408
|
+
3. require `angelo/mustermann`
|
409
|
+
4. include `Angelo::Mustermann` in your app
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
class Foo < Angelo::Base
|
413
|
+
include Angelo::Tilt::ERB
|
414
|
+
include Angelo::Mustermann
|
415
|
+
|
416
|
+
get '/:foo/things/:bar' do
|
417
|
+
|
418
|
+
# `params` is merged with the Mustermann object#params hash, so
|
419
|
+
# a "GET /some/things/are_good?foo=other&bar=are_bad" would have:
|
420
|
+
# params: {
|
421
|
+
# 'foo' => 'some',
|
422
|
+
# 'bar' => 'are_good'
|
423
|
+
# }
|
424
|
+
|
425
|
+
@foo = params[:foo]
|
426
|
+
@bar = params[:bar]
|
427
|
+
erb :index
|
428
|
+
end
|
429
|
+
|
430
|
+
before '/:fu/things/*' do
|
431
|
+
|
432
|
+
# `params` is merged with the Mustermann object#params hash, as
|
433
|
+
# parsed with the current request path against this before block's
|
434
|
+
# pattern. in the route handler, `params[:fu]` is no longer available.
|
435
|
+
|
436
|
+
@fu = params[:fu]
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
440
|
+
```
|
441
|
+
|
358
442
|
### WORK LEFT TO DO
|
359
443
|
|
360
444
|
Lots of work left to do!
|
@@ -466,61 +550,26 @@ class Foo < Angelo::Base
|
|
466
550
|
msg
|
467
551
|
end
|
468
552
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
### [Tilt](https://github.com/rtomayko/tilt) / ERB
|
475
|
-
|
476
|
-
To make `erb` available in route blocks
|
477
|
-
|
478
|
-
1. add `tilt` to your `Gemfile`: `gem 'tilt'`
|
479
|
-
2. require `angelo/tilt/erb`
|
480
|
-
3. include `Angelo::Tilt::ERB` in your app
|
481
|
-
|
482
|
-
```ruby
|
483
|
-
class Foo < Angelo::Base
|
484
|
-
include Angelo::Tilt::ERB
|
485
|
-
|
486
|
-
@@views = 'some/other/path' # defaults to './views'
|
553
|
+
# return a chunked response of JSON for 5 seconds
|
554
|
+
#
|
555
|
+
get '/chunky_json' do
|
556
|
+
content_type :json
|
487
557
|
|
488
|
-
|
489
|
-
|
558
|
+
# this helper requires a block that takes one arg, the response
|
559
|
+
# proc to call with each chunk (i.e. the block that is passed to
|
560
|
+
# `#each`)
|
561
|
+
#
|
562
|
+
chunked_response do |response|
|
563
|
+
5.times do
|
564
|
+
response.call time: Time.now.to_i
|
565
|
+
sleep 1
|
566
|
+
end
|
567
|
+
end
|
490
568
|
end
|
491
569
|
|
492
570
|
end
|
493
|
-
```
|
494
571
|
|
495
|
-
|
496
|
-
|
497
|
-
To make routes blocks match path with Mustermann patterns
|
498
|
-
|
499
|
-
1. be using ruby >=2.0.0
|
500
|
-
2. add 'mustermann' to to your `Gemfile`: `platform(:ruby_20){ gem 'mustermann' }`
|
501
|
-
3. require `angelo/mustermann`
|
502
|
-
4. include `Angelo::Mustermann` in your app
|
503
|
-
|
504
|
-
```ruby
|
505
|
-
class Foo < Angelo::Base
|
506
|
-
include Angelo::Tilt::ERB
|
507
|
-
include Angelo::Mustermann
|
508
|
-
|
509
|
-
get '/:foo/things/:bar' do
|
510
|
-
|
511
|
-
# `params` is merged with the Mustermann object#params hash, so
|
512
|
-
# a "GET /some/things/are_good?foo=other&bar=are_bad" would have:
|
513
|
-
# params: {
|
514
|
-
# 'foo' => 'some',
|
515
|
-
# 'bar' => 'are_good'
|
516
|
-
# }
|
517
|
-
|
518
|
-
@foo = params[:foo]
|
519
|
-
@bar = params[:bar]
|
520
|
-
erb :index
|
521
|
-
end
|
522
|
-
|
523
|
-
end
|
572
|
+
Foo.run
|
524
573
|
```
|
525
574
|
|
526
575
|
### Contributing
|
data/lib/angelo.rb
CHANGED
data/lib/angelo/base.rb
CHANGED
@@ -5,7 +5,7 @@ module Angelo
|
|
5
5
|
include Celluloid::Logger
|
6
6
|
|
7
7
|
extend Forwardable
|
8
|
-
def_delegators :@responder, :content_type, :headers, :redirect, :request
|
8
|
+
def_delegators :@responder, :content_type, :headers, :redirect, :request, :transfer_encoding
|
9
9
|
def_delegators :@klass, :websockets, :sses, :sse_event, :sse_message
|
10
10
|
|
11
11
|
@@addr = DEFAULT_ADDR
|
@@ -287,7 +287,7 @@ module Angelo
|
|
287
287
|
|
288
288
|
def eventsource &block
|
289
289
|
headers SSE_HEADER
|
290
|
-
async :handle_event_source, responder.connection.detach.socket, block
|
290
|
+
async :handle_event_source, EventSource.new(responder.connection.detach.socket), block
|
291
291
|
halt 200, :sse
|
292
292
|
end
|
293
293
|
|
@@ -305,6 +305,43 @@ module Angelo
|
|
305
305
|
fs.each {|f| f.bind(self).call}
|
306
306
|
end
|
307
307
|
|
308
|
+
def chunked_response &block
|
309
|
+
transfer_encoding :chunked
|
310
|
+
ChunkedResponse.new &block
|
311
|
+
end
|
312
|
+
|
313
|
+
class EventSource
|
314
|
+
extend Forwardable
|
315
|
+
|
316
|
+
def_delegators :@socket, :close, :closed?, :<<, :write
|
317
|
+
attr_reader :socket
|
318
|
+
|
319
|
+
def initialize socket
|
320
|
+
@socket = socket
|
321
|
+
end
|
322
|
+
|
323
|
+
def event name, data = nil
|
324
|
+
@socket.write Base.sse_event(name, data)
|
325
|
+
end
|
326
|
+
|
327
|
+
def message data
|
328
|
+
@socket.write Base.sse_message(data)
|
329
|
+
end
|
330
|
+
|
331
|
+
end
|
332
|
+
|
333
|
+
class ChunkedResponse
|
334
|
+
|
335
|
+
def initialize &block
|
336
|
+
@chunker = block
|
337
|
+
end
|
338
|
+
|
339
|
+
def each &block
|
340
|
+
@chunker[block]
|
341
|
+
end
|
342
|
+
|
343
|
+
end
|
344
|
+
|
308
345
|
end
|
309
346
|
|
310
347
|
end
|
@@ -38,7 +38,11 @@ module Angelo
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def hc_req method, path, params = {}, headers = {}
|
41
|
-
@last_response =
|
41
|
+
@last_response = if block_given?
|
42
|
+
hc.__send__ method, url(path), params, headers, &Proc.new
|
43
|
+
else
|
44
|
+
hc.__send__ method, url(path), params, headers
|
45
|
+
end
|
42
46
|
end
|
43
47
|
private :hc_req
|
44
48
|
|
@@ -64,8 +68,14 @@ module Angelo
|
|
64
68
|
|
65
69
|
[:get, :post, :put, :delete, :options, :head].each do |m|
|
66
70
|
define_method m do |path, params = {}, headers = {}|
|
71
|
+
|
67
72
|
# http_req m, path, params, headers
|
68
|
-
|
73
|
+
|
74
|
+
if block_given?
|
75
|
+
hc_req m, path, params, headers, &Proc.new
|
76
|
+
else
|
77
|
+
hc_req m, path, params, headers
|
78
|
+
end
|
69
79
|
end
|
70
80
|
end
|
71
81
|
|
data/lib/angelo/responder.rb
CHANGED
@@ -117,6 +117,19 @@ module Angelo
|
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
120
|
+
def transfer_encoding *encodings
|
121
|
+
encodings.flatten.each do |encoding|
|
122
|
+
case encoding
|
123
|
+
when :chunked
|
124
|
+
@chunked = true
|
125
|
+
headers transfer_encoding: :chunked
|
126
|
+
# when :compress, :deflate, :gzip, :identity
|
127
|
+
else
|
128
|
+
raise ArgumentError.new "invalid transfer_conding: #{encoding}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
120
133
|
def respond_with? type
|
121
134
|
case headers[CONTENT_TYPE_HEADER_KEY]
|
122
135
|
when JSON_TYPE
|
@@ -147,14 +160,37 @@ module Angelo
|
|
147
160
|
|
148
161
|
when NilClass
|
149
162
|
@body = EMPTY_STRING
|
163
|
+
|
164
|
+
else
|
165
|
+
unless @chunked and @body.respond_to? :each
|
166
|
+
raise RequestError.new "what is this? #{@body}"
|
167
|
+
end
|
150
168
|
end
|
151
169
|
|
152
170
|
status ||= @redirect.nil? ? :ok : :moved_permanently
|
153
171
|
headers LOCATION_HEADER_KEY => @redirect if @redirect
|
154
|
-
size = @body.nil? ? 0 : @body.size
|
155
172
|
|
156
|
-
|
157
|
-
|
173
|
+
if @chunked
|
174
|
+
Angelo.log @connection, @request, nil, status
|
175
|
+
@request.respond status, headers
|
176
|
+
err = nil
|
177
|
+
begin
|
178
|
+
@body.each do |r|
|
179
|
+
r = r.to_json + NEWLINE if respond_with? :json
|
180
|
+
@request << r
|
181
|
+
end
|
182
|
+
rescue => e
|
183
|
+
err = e
|
184
|
+
ensure
|
185
|
+
@request.finish_response
|
186
|
+
raise err if err
|
187
|
+
end
|
188
|
+
else
|
189
|
+
size = @body.nil? ? 0 : @body.size
|
190
|
+
Angelo.log @connection, @request, nil, status, size
|
191
|
+
@request.respond status, headers, @body
|
192
|
+
end
|
193
|
+
|
158
194
|
rescue => e
|
159
195
|
handle_error e, :internal_server_error
|
160
196
|
end
|
data/lib/angelo/version.rb
CHANGED
@@ -9,17 +9,17 @@ describe Angelo::Responder::Eventsource do
|
|
9
9
|
define_app do
|
10
10
|
|
11
11
|
eventsource '/msg' do |c|
|
12
|
-
c.
|
12
|
+
c.message 'hi'
|
13
13
|
c.close
|
14
14
|
end
|
15
15
|
|
16
16
|
eventsource '/event' do |c|
|
17
|
-
c.
|
17
|
+
c.event :sse, 'bye'
|
18
18
|
c.close
|
19
19
|
end
|
20
20
|
|
21
21
|
eventsource '/headers', foo: 'bar' do |c|
|
22
|
-
c.
|
22
|
+
c.event :sse, 'headers'
|
23
23
|
c.close
|
24
24
|
end
|
25
25
|
|
@@ -59,12 +59,12 @@ describe Angelo::Responder::Eventsource do
|
|
59
59
|
end
|
60
60
|
|
61
61
|
eventsource '/msg' do |c|
|
62
|
-
c.
|
62
|
+
c.message 'hi'
|
63
63
|
c.close
|
64
64
|
end
|
65
65
|
|
66
66
|
eventsource '/other' do |c|
|
67
|
-
c.
|
67
|
+
c.message 'other'
|
68
68
|
c.close
|
69
69
|
end
|
70
70
|
|
data/test/angelo_spec.rb
CHANGED
@@ -379,4 +379,60 @@ describe Angelo::Base do
|
|
379
379
|
|
380
380
|
end
|
381
381
|
|
382
|
+
describe 'chunked responses' do
|
383
|
+
|
384
|
+
define_app do
|
385
|
+
|
386
|
+
get '/chunk' do
|
387
|
+
chunked_response do |r|
|
388
|
+
5.times {|n| r[n + "\n"]}
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
get '/chunk.json' do
|
393
|
+
content_type :json
|
394
|
+
chunked_response do |r|
|
395
|
+
5.times {|n| r[{n: n}]}
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
get '/chunk_each' do
|
400
|
+
transfer_encoding :chunked
|
401
|
+
Object.new.tap do |o|
|
402
|
+
def o.each; 5.times {|n| yield n + "\n"}; end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
407
|
+
|
408
|
+
it 'chunks responses with helper' do
|
409
|
+
i = 0
|
410
|
+
get '/chunk' do |c|
|
411
|
+
c.must_equal i.to_s
|
412
|
+
i += 1
|
413
|
+
end
|
414
|
+
last_response_must_be_html
|
415
|
+
end
|
416
|
+
|
417
|
+
it 'chunks responses with helper in json' do
|
418
|
+
i = 0
|
419
|
+
get '/chunk.json' do |c|
|
420
|
+
JSON.parse(c)['n'].must_equal i
|
421
|
+
i += 1
|
422
|
+
end
|
423
|
+
last_response.status.must_equal 200
|
424
|
+
last_response.headers['Content-Type'].split(';').must_include Angelo::JSON_TYPE
|
425
|
+
end
|
426
|
+
|
427
|
+
it 'chunks responses with any object that responds_to? :each' do
|
428
|
+
i = 0
|
429
|
+
get '/chunk_each' do |c|
|
430
|
+
c.must_equal i.to_s
|
431
|
+
i += 1
|
432
|
+
end
|
433
|
+
last_response_must_be_html
|
434
|
+
end
|
435
|
+
|
436
|
+
end
|
437
|
+
|
382
438
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: angelo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Nakamura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: reel
|