angelo 0.1.7 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -9
- data/README.md +182 -14
- data/Rakefile +7 -0
- data/lib/angelo/base.rb +1 -1
- data/lib/angelo/{rspec → minitest}/helpers.rb +9 -9
- data/lib/angelo/mustermann.rb +2 -2
- data/lib/angelo/responder/websocket.rb +1 -1
- data/lib/angelo/version.rb +1 -1
- data/{spec → test}/angelo/erb_spec.rb +2 -2
- data/{spec → test}/angelo/mustermann_spec.rb +5 -5
- data/{spec → test}/angelo/params_spec.rb +6 -6
- data/test/angelo/static_spec.rb +72 -0
- data/{spec → test}/angelo/websocket_spec.rb +9 -9
- data/{spec → test}/angelo_spec.rb +29 -28
- data/{spec → test}/spec_helper.rb +5 -3
- data/{spec → test}/test_app_root/public/test.css +0 -0
- data/{spec → test}/test_app_root/public/test.html +0 -0
- data/{spec → test}/test_app_root/public/test.js +0 -0
- data/{spec → test}/test_app_root/public/what.png +0 -0
- data/{spec → test}/test_app_root/views/index.html.erb +0 -0
- data/{spec → test}/test_app_root/views/layout.html.erb +0 -0
- metadata +27 -39
- data/spec/angelo/static_spec.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c515e055a1dbd1a233df60640c31b82866edf6a
|
4
|
+
data.tar.gz: 617d5e38dec4a3ea40c27dc16eb094081f7b6b8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 51acea14b590f1ed28b8017c2b9d88b953a77f24ee5c1784e6681d1d0521487faf7c18d228d99a9f3bd98b0b168c6f69d4e58e95d0b6463738053c35282433d1
|
7
|
+
data.tar.gz: 95de633d4b58ffeb51c4f43f03ce09bdae4f68191bf6e0cd278d9c1a77204907ed64ef3ab6cca2371698b98ae6818fe9b431503da77a1671890fbf8eb0957faf
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
changelog
|
2
2
|
=========
|
3
3
|
|
4
|
+
### 0.1.8 6 may 2014
|
5
|
+
|
6
|
+
* leave RSpec for Minitest
|
7
|
+
* fix for Reel::Connection::StateError -> Reel::StateError
|
8
|
+
* rename "socket" route definition to "websocket"
|
9
|
+
|
4
10
|
### 0.1.7 17 apr 2014
|
5
11
|
|
6
12
|
* reel 0.5.0 support (Reel::Server -> Reel::Server::HTTP)
|
data/Gemfile
CHANGED
@@ -5,13 +5,7 @@ gem 'tilt'
|
|
5
5
|
gem 'mime-types'
|
6
6
|
gem 'websocket-driver'
|
7
7
|
|
8
|
-
platform :
|
9
|
-
gem 'rubysl-cgi'
|
10
|
-
gem 'rubysl-erb'
|
11
|
-
gem 'rubysl-prettyprint'
|
12
|
-
end
|
13
|
-
|
14
|
-
platform :ruby_20 do
|
8
|
+
platform :ruby_20, :ruby_21 do
|
15
9
|
gem 'mustermann'
|
16
10
|
end
|
17
11
|
|
@@ -28,6 +22,5 @@ end
|
|
28
22
|
|
29
23
|
group :test do
|
30
24
|
gem 'httpclient'
|
31
|
-
gem '
|
32
|
-
gem 'rspec-pride'
|
25
|
+
gem 'minitest'
|
33
26
|
end
|
data/README.md
CHANGED
@@ -5,18 +5,163 @@ Angelo
|
|
5
5
|
|
6
6
|
A [Sinatra](https://github.com/sinatra/sinatra)-esque DSL for [Reel](https://github.com/celluloid/reel).
|
7
7
|
|
8
|
-
###
|
8
|
+
### tl;dr
|
9
9
|
|
10
|
-
*
|
11
|
-
*
|
12
|
-
*
|
10
|
+
* websocket support via `websocket('/path'){|s| ... }` route builder
|
11
|
+
* contextual websocket stashing via `websockets` helper
|
12
|
+
* `task` handling via `async` and `future` helpers
|
13
13
|
* no rack
|
14
14
|
* optional tilt/erb support
|
15
15
|
* optional mustermann support
|
16
16
|
|
17
|
+
### What is Angelo?
|
18
|
+
|
19
|
+
Just like Sinatra, Angelo gives you an expressive DSL for creating web applications. There are some
|
20
|
+
notable differences, but the basics remain the same. It mostly follows the subclass style of Sinatra:
|
21
|
+
you must define a subclass of `Angelo::Base` but also `.run` on that class for the service to start.
|
22
|
+
In addition, and perhaps more importantly, **Angelo is built upon Reel, which is, in turn, built upon
|
23
|
+
Celluloid::IO and gives you a reactor with evented IO in Ruby!**
|
24
|
+
|
25
|
+
Note: There currently is no "standalone" capability where one can define route handlers at the top level.
|
26
|
+
|
27
|
+
Things will feel very familiar to anyone experienced with Sinatra. Inside the subclass, you can define
|
28
|
+
route handlers denoted by HTTP verb and path. Unlike Sinatra, the only acceptable return value from a
|
29
|
+
route block is the body of the response in full. Chunked response support was recently added to Reel,
|
30
|
+
and look for that support in Angelo soon.
|
31
|
+
|
32
|
+
There is also [Mustermann](#mustermann) support for full-on, Sinatra-like path
|
33
|
+
matching and params.
|
34
|
+
|
35
|
+
### Websockets!
|
36
|
+
|
37
|
+
One of the main motivations for Angelo was the ability to define websocket handlers with ease. Through
|
38
|
+
the addition of a `websocket` route builder and a `websockets` helper, Angelo attempts to make it easy
|
39
|
+
for you to build real-time web applications.
|
40
|
+
|
41
|
+
##### Route Builder
|
42
|
+
|
43
|
+
The `websocket` route builder accepts a path and a block, and passes the actual websocket to the block
|
44
|
+
as the only argument. This socket is an instance of Reel's
|
45
|
+
[WebSocket](https://github.com/celluloid/reel/blob/master/lib/reel/websocket.rb) class, and, as such,
|
46
|
+
responds to methods like `on_message` and `on_close`. A service-wide `on_pong` handler (defined at the
|
47
|
+
class-level of the Angelo app) is available to customize the behavior when a pong frame comes back from
|
48
|
+
a connected websocket client.
|
49
|
+
|
50
|
+
##### `websockets` helper
|
51
|
+
|
52
|
+
Angelo includes a "stash" helper for connected websockets. One can `<<` a websocket into `websockets`
|
53
|
+
from inside a websocket handler block. These can "later" be iterated over so one can do things like
|
54
|
+
emit a message on every connected websocket when the service receives a POST request.
|
55
|
+
|
56
|
+
The `websockets` helper also includes a context ability, so you can stash connected websocket clients
|
57
|
+
into different "sections".
|
58
|
+
|
59
|
+
##### Example!
|
60
|
+
|
61
|
+
Here is an example of the `websocket` route builder, the `websockets` helper, and the context feature:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require 'angelo'
|
65
|
+
|
66
|
+
class Foo < Angelo::Base
|
67
|
+
|
68
|
+
websocket '/' do |ws|
|
69
|
+
websockets << ws
|
70
|
+
end
|
71
|
+
|
72
|
+
websocket '/bar' do |ws|
|
73
|
+
websockets[:bar] << ws
|
74
|
+
end
|
75
|
+
|
76
|
+
post '/' do
|
77
|
+
websockets.each {|ws| ws.write params[:foo]}
|
78
|
+
end
|
79
|
+
|
80
|
+
post '/bar' do
|
81
|
+
websockets[:bar].each {|ws| ws.write params[:bar]}
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
Foo.run
|
87
|
+
```
|
88
|
+
|
89
|
+
In this case, any clients that connected to a websocket at the path '/' would be stashed in the
|
90
|
+
default websockets array; clients that connected to '/bar' would be stashed into the `:bar` section.
|
91
|
+
|
92
|
+
Each "section" can accessed with a familiar, `Hash`-like syntax, and can be iterated over with
|
93
|
+
a `.each` block.
|
94
|
+
|
95
|
+
When a `POST /` with a 'foo' param is received, any value is messaged out to any '/' connected
|
96
|
+
websockets. When a `POST /bar` with a 'bar' param is received, any value is messaged out to all
|
97
|
+
websockets that connected to '/bar'.
|
98
|
+
|
99
|
+
### Tasks + Async / Future
|
100
|
+
|
101
|
+
Angelo is built on Reel and Celluloid::IO, giving your web application class the ability to define
|
102
|
+
"tasks" and call them from route handler blocks in an `async` or `future` style.
|
103
|
+
|
104
|
+
##### `task` builder
|
105
|
+
|
106
|
+
You can define a task on the reactor using the `task` class method and giving it a symbol and a
|
107
|
+
block. The block can take arguments that you can pass later, with `async` or `future`.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# defining a task on the reactor called `:in_sec` which will sleep for
|
111
|
+
# given number of seconds, then return the given message.
|
112
|
+
#
|
113
|
+
task :in_sec do |sec, msg|
|
114
|
+
sleep sec.to_i
|
115
|
+
msg
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
##### `async` helper
|
120
|
+
|
121
|
+
This helper is directly analogous to the Celluoid method of the same name. Once tasks are defined,
|
122
|
+
you can call them with this helper method, passing the symbol of the task name and any arguments.
|
123
|
+
The task will run on the reactor, asynchronously, and return immediately.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
get '/' do
|
127
|
+
# run the task defined above asynchronously, return immediately
|
128
|
+
#
|
129
|
+
async :in_sec, params[:sec], params[:msg]
|
130
|
+
|
131
|
+
# NOTE: params[:msg] is discarded, the return value of tasks called with `async` is nil.
|
132
|
+
|
133
|
+
# return this response body while the task is still running
|
134
|
+
# assuming params[:sec] is > 0
|
135
|
+
#
|
136
|
+
'hi'
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
##### `future` helper
|
141
|
+
|
142
|
+
Just like `async`, this comes from Celluloid as well. It behaves exactly like `async`, with the
|
143
|
+
notable exception of returing a "future" object that you can call `#value` on later to retreive
|
144
|
+
the return value of the task. Once `#value` is called, things will "block" until the task is
|
145
|
+
finished.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
get '/' do
|
149
|
+
# run the task defined above asynchronously, return immediately
|
150
|
+
#
|
151
|
+
f = future :in_sec, params[:sec], params[:msg]
|
152
|
+
|
153
|
+
# now, block until the task is finished and return the task's value
|
154
|
+
# as a response body
|
155
|
+
#
|
156
|
+
f.value
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
### WORK LEFT TO DO
|
161
|
+
|
17
162
|
Lots of work left to do!
|
18
163
|
|
19
|
-
###
|
164
|
+
### Full-ish example
|
20
165
|
|
21
166
|
```ruby
|
22
167
|
require 'angelo'
|
@@ -25,18 +170,28 @@ require 'angelo/mustermann'
|
|
25
170
|
class Foo < Angelo::Base
|
26
171
|
include Angelo::Mustermann
|
27
172
|
|
173
|
+
# just some constants to use in routes later...
|
174
|
+
#
|
28
175
|
TEST = {foo: "bar", baz: 123, bat: false}.to_json
|
29
|
-
|
30
176
|
HEART = '<3'
|
177
|
+
|
178
|
+
# a flag to know if the :heart task is running
|
179
|
+
#
|
31
180
|
@@hearting = false
|
32
181
|
|
182
|
+
# you can define instance methods, just like Sinatra!
|
183
|
+
#
|
33
184
|
def pong; 'pong'; end
|
34
185
|
def foo; params[:foo]; end
|
35
186
|
|
187
|
+
# standard HTTP GET handler
|
188
|
+
#
|
36
189
|
get '/ping' do
|
37
190
|
pong
|
38
191
|
end
|
39
192
|
|
193
|
+
# standard HTTP POST handler
|
194
|
+
#
|
40
195
|
post '/foo' do
|
41
196
|
foo
|
42
197
|
end
|
@@ -45,12 +200,19 @@ class Foo < Angelo::Base
|
|
45
200
|
params.to_json
|
46
201
|
end
|
47
202
|
|
203
|
+
# emit the TEST JSON value on all :emit_test websockets
|
204
|
+
# return the params posted as JSON
|
205
|
+
#
|
48
206
|
post '/emit' do
|
49
207
|
websockets[:emit_test].each {|ws| ws.write TEST}
|
50
208
|
params.to_json
|
51
209
|
end
|
52
210
|
|
53
|
-
|
211
|
+
# handle websocket requests at '/ws'
|
212
|
+
# stash them in the :emit_test context
|
213
|
+
# write 6 messages to the websocket whenever a message is received
|
214
|
+
#
|
215
|
+
websocket '/ws' do |ws|
|
54
216
|
websockets[:emit_test] << ws
|
55
217
|
ws.on_message do |msg|
|
56
218
|
5.times { ws.write TEST }
|
@@ -58,19 +220,24 @@ class Foo < Angelo::Base
|
|
58
220
|
end
|
59
221
|
end
|
60
222
|
|
223
|
+
# emit the TEST JSON value on all :other websockets
|
224
|
+
#
|
61
225
|
post '/other' do
|
62
226
|
websockets[:other].each {|ws| ws.write TEST}
|
227
|
+
''
|
63
228
|
end
|
64
229
|
|
65
|
-
|
230
|
+
# stash '/other/ws' connected websockets in the :other context
|
231
|
+
#
|
232
|
+
websocket '/other/ws' do |ws|
|
66
233
|
websockets[:other] << ws
|
67
234
|
end
|
68
235
|
|
69
|
-
|
236
|
+
websocket '/hearts' do |ws|
|
70
237
|
|
71
|
-
# this is a call to Base#async, actually calling
|
238
|
+
# this is a call to Base#async, actually calling
|
72
239
|
# the reactor to start the task
|
73
|
-
#
|
240
|
+
#
|
74
241
|
async :hearts unless @@hearting
|
75
242
|
|
76
243
|
websockets[:hearts] << ws
|
@@ -87,12 +254,15 @@ class Foo < Angelo::Base
|
|
87
254
|
post '/in/:sec/sec/:msg' do
|
88
255
|
|
89
256
|
# this is a call to Base#future, telling the reactor
|
90
|
-
# do this thing and we'
|
257
|
+
# do this thing and we'll want the value eventually
|
91
258
|
#
|
92
259
|
f = future :in_sec params[:sec], params[:msg]
|
93
260
|
f.value
|
94
261
|
end
|
95
262
|
|
263
|
+
# define a task on the reactor that sleeps for the given number of
|
264
|
+
# seconds and returns the given message
|
265
|
+
#
|
96
266
|
task :in_sec do |sec, msg|
|
97
267
|
sleep sec.to_i
|
98
268
|
msg
|
@@ -155,8 +325,6 @@ class Foo < Angelo::Base
|
|
155
325
|
end
|
156
326
|
```
|
157
327
|
|
158
|
-
NOTE: this always sets the Mustermann object's `type` to `:sinatra`
|
159
|
-
|
160
328
|
### Contributing
|
161
329
|
|
162
330
|
YES, HAVE SOME
|
data/Rakefile
ADDED
data/lib/angelo/base.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'websocket/driver'
|
2
2
|
|
3
3
|
module Angelo
|
4
|
-
module
|
4
|
+
module Minitest
|
5
5
|
|
6
6
|
module Helpers
|
7
7
|
|
@@ -61,16 +61,16 @@ module Angelo
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
def
|
65
|
-
last_response.status.
|
66
|
-
last_response.body.
|
67
|
-
last_response.headers['Content-Type'].split(';').
|
64
|
+
def last_response_must_be_html body = ''
|
65
|
+
last_response.status.must_equal 200
|
66
|
+
last_response.body.must_equal body
|
67
|
+
last_response.headers['Content-Type'].split(';').must_include HTML_TYPE
|
68
68
|
end
|
69
69
|
|
70
|
-
def
|
71
|
-
last_response.status.
|
72
|
-
JSON.parse(last_response.body).
|
73
|
-
last_response.headers['Content-Type'].split(';').
|
70
|
+
def last_response_must_be_json obj = {}
|
71
|
+
last_response.status.must_equal 200
|
72
|
+
JSON.parse(last_response.body).must_equal obj
|
73
|
+
last_response.headers['Content-Type'].split(';').must_include JSON_TYPE
|
74
74
|
end
|
75
75
|
|
76
76
|
end
|
data/lib/angelo/mustermann.rb
CHANGED
data/lib/angelo/version.rb
CHANGED
@@ -42,7 +42,7 @@ locals :bar - bat
|
|
42
42
|
</body>
|
43
43
|
</html>
|
44
44
|
HTML
|
45
|
-
|
45
|
+
last_response_must_be_html expected
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'renders templates without layout' do
|
@@ -51,7 +51,7 @@ HTML
|
|
51
51
|
foo - asdf
|
52
52
|
locals :bar - bat
|
53
53
|
HTML
|
54
|
-
|
54
|
+
last_response_must_be_html expected
|
55
55
|
end
|
56
56
|
|
57
57
|
end
|
@@ -30,13 +30,13 @@ if RUBY_VERSION =~ /^2\./
|
|
30
30
|
it 'matches via mustermann routes objects' do
|
31
31
|
path = '/some/things/are_good'
|
32
32
|
get path
|
33
|
-
|
33
|
+
last_response_must_be_json mm_pattern.params(path)
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'overrides query string params' do
|
37
37
|
path = '/some/things/are_good'
|
38
38
|
get path, foo: 'other', bar: 'are_bad'
|
39
|
-
|
39
|
+
last_response_must_be_json mm_pattern.params(path)
|
40
40
|
end
|
41
41
|
|
42
42
|
it 'overrides post body params' do
|
@@ -44,14 +44,14 @@ if RUBY_VERSION =~ /^2\./
|
|
44
44
|
headers = {Angelo::CONTENT_TYPE_HEADER_KEY => Angelo::JSON_TYPE}
|
45
45
|
[:post, :put].each do |m|
|
46
46
|
__send__ m, path, {foo: 'other', bar: 'are_bad'}.to_json, headers
|
47
|
-
|
47
|
+
last_response_must_be_json mm_pattern.params(path)
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
51
|
it '404s correctly for not found routes' do
|
52
52
|
path = '/bad/monkey'
|
53
53
|
get path
|
54
|
-
last_response.status.
|
54
|
+
last_response.status.must_equal 404
|
55
55
|
end
|
56
56
|
|
57
57
|
end
|
@@ -87,7 +87,7 @@ locals :bar - alpaca
|
|
87
87
|
</body>
|
88
88
|
</html>
|
89
89
|
HTML
|
90
|
-
|
90
|
+
last_response_must_be_html expected
|
91
91
|
end
|
92
92
|
|
93
93
|
end
|
@@ -43,21 +43,21 @@ describe Angelo::ParamsParser do
|
|
43
43
|
let(:parser) { ParamsTester.new }
|
44
44
|
|
45
45
|
it 'parses query string params in the normal, non-racked-up, way' do
|
46
|
-
parser.parse_formencoded(get_params).
|
46
|
+
parser.parse_formencoded(get_params).must_equal params_s
|
47
47
|
end
|
48
48
|
|
49
49
|
it 'parses formencoded POST bodies in the normal, non-racked-up, way' do
|
50
50
|
parser.form_encoded = true
|
51
51
|
parser.json = false
|
52
52
|
parser.body = get_params
|
53
|
-
parser.parse_post_body.
|
53
|
+
parser.parse_post_body.must_equal params_s
|
54
54
|
end
|
55
55
|
|
56
56
|
it 'parses JSON POST bodies params' do
|
57
57
|
parser.form_encoded = false
|
58
58
|
parser.json = true
|
59
59
|
parser.body = json_params
|
60
|
-
parser.parse_post_body.
|
60
|
+
parser.parse_post_body.must_equal post_params
|
61
61
|
end
|
62
62
|
|
63
63
|
it 'should override query string with JSON POST bodies params' do
|
@@ -65,7 +65,7 @@ describe Angelo::ParamsParser do
|
|
65
65
|
parser.json = true
|
66
66
|
parser.query_string = get_params
|
67
67
|
parser.body = json_params
|
68
|
-
parser.parse_post_body.
|
68
|
+
parser.parse_post_body.must_equal post_params
|
69
69
|
end
|
70
70
|
|
71
71
|
it 'does not parse POST bodies if no Content-Type' do
|
@@ -73,8 +73,8 @@ describe Angelo::ParamsParser do
|
|
73
73
|
parser.json = false
|
74
74
|
parser.query_string = get_params
|
75
75
|
parser.body = nil
|
76
|
-
parser.parse_post_body.
|
77
|
-
parser.parse_query_string.
|
76
|
+
parser.parse_post_body.must_equal params_s
|
77
|
+
parser.parse_query_string.must_equal params_s
|
78
78
|
end
|
79
79
|
|
80
80
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
describe Angelo::Server do
|
5
|
+
|
6
|
+
describe 'serving static files' do
|
7
|
+
|
8
|
+
let(:css_etag) do
|
9
|
+
fs = File::Stat.new File.join(TEST_APP_ROOT, 'public', 'test.css')
|
10
|
+
OpenSSL::Digest::SHA.hexdigest fs.ino.to_s + fs.size.to_s + fs.mtime.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
define_app do
|
14
|
+
|
15
|
+
@root = TEST_APP_ROOT
|
16
|
+
|
17
|
+
get '/test.html' do
|
18
|
+
'you should not see this'
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'serves static files for gets' do
|
24
|
+
get '/test.css'
|
25
|
+
last_response.status.must_equal 200
|
26
|
+
last_response.headers['Content-Type'].must_equal 'text/css'
|
27
|
+
last_response.headers['Content-Disposition'].must_equal 'attachment; filename=test.css'
|
28
|
+
last_response.headers['Content-Length'].must_equal '116'
|
29
|
+
last_response.headers['Etag'].must_equal css_etag
|
30
|
+
last_response.body.length.must_equal 116
|
31
|
+
last_response.body.must_equal File.read(File.join TEST_APP_ROOT, 'public', 'test.css')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'serves headers for static files on head' do
|
35
|
+
head '/test.css'
|
36
|
+
last_response.status.must_equal 200
|
37
|
+
last_response.headers['Content-Type'].must_equal 'text/css'
|
38
|
+
last_response.headers['Content-Disposition'].must_equal 'attachment; filename=test.css'
|
39
|
+
last_response.headers['Content-Length'].must_equal '116'
|
40
|
+
last_response.headers['Etag'].must_equal css_etag
|
41
|
+
last_response.body.length.must_equal 0
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'serves static file over route' do
|
45
|
+
get '/test.html'
|
46
|
+
last_response.status.must_equal 200
|
47
|
+
last_response.headers['Content-Type'].must_equal 'text/html'
|
48
|
+
last_response.headers['Content-Disposition'].must_equal 'attachment; filename=test.html'
|
49
|
+
last_response.body.must_equal File.read(File.join TEST_APP_ROOT, 'public', 'test.html')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'not modifieds when if-none-match matched etag' do
|
53
|
+
get '/test.css', {}, {'If-None-Match' => css_etag}
|
54
|
+
last_response.status.must_equal 304
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'serves proper content-types' do
|
58
|
+
{ 'test.js' => 'application/javascript',
|
59
|
+
'test.html' => 'text/html',
|
60
|
+
'test.css' => 'text/css',
|
61
|
+
'what.png' => 'image/png' }.each do |k,v|
|
62
|
+
|
63
|
+
get "/#{k}"
|
64
|
+
last_response.status.must_equal 200
|
65
|
+
last_response.headers['Content-Type'].must_equal v
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -32,7 +32,7 @@ describe Angelo::WebsocketResponder do
|
|
32
32
|
describe 'basics' do
|
33
33
|
|
34
34
|
define_app do
|
35
|
-
|
35
|
+
websocket '/' do |ws|
|
36
36
|
while msg = ws.read do
|
37
37
|
ws.write msg
|
38
38
|
end
|
@@ -44,7 +44,7 @@ describe Angelo::WebsocketResponder do
|
|
44
44
|
latch = CountDownLatch.new 500
|
45
45
|
|
46
46
|
wsh.on_message = ->(e) {
|
47
|
-
|
47
|
+
assert_match /hi there \d/, e.data
|
48
48
|
latch.count_down
|
49
49
|
}
|
50
50
|
|
@@ -72,7 +72,7 @@ describe Angelo::WebsocketResponder do
|
|
72
72
|
Reactor.testers[:wshs] = Array.new(CONCURRENCY).map do
|
73
73
|
wsh = socket '/'
|
74
74
|
wsh.on_message = ->(e) {
|
75
|
-
|
75
|
+
assert_match /hi there \d/, e.data
|
76
76
|
latch.count_down
|
77
77
|
}
|
78
78
|
wsh.init
|
@@ -118,7 +118,7 @@ describe Angelo::WebsocketResponder do
|
|
118
118
|
end
|
119
119
|
end
|
120
120
|
|
121
|
-
|
121
|
+
websocket '/concur' do |ws|
|
122
122
|
websockets << ws
|
123
123
|
end
|
124
124
|
|
@@ -129,7 +129,7 @@ describe Angelo::WebsocketResponder do
|
|
129
129
|
latch = CountDownLatch.new CONCURRENCY * Angelo::HTTPABLE.length
|
130
130
|
|
131
131
|
expectation = ->(e){
|
132
|
-
|
132
|
+
assert_match /from http (#{Angelo::HTTPABLE.map(&:to_s).join('|')})/, e.data
|
133
133
|
}
|
134
134
|
|
135
135
|
socket_wait_for '/concur', latch, expectation do
|
@@ -143,7 +143,7 @@ describe Angelo::WebsocketResponder do
|
|
143
143
|
|
144
144
|
describe 'helper contexts' do
|
145
145
|
let(:obj){ {'foo' => 'bar'} }
|
146
|
-
let(:wait_for_block){ ->(e){
|
146
|
+
let(:wait_for_block){ ->(e){ assert_equal obj, JSON.parse(e.data) }}
|
147
147
|
|
148
148
|
define_app do
|
149
149
|
|
@@ -152,7 +152,7 @@ describe Angelo::WebsocketResponder do
|
|
152
152
|
''
|
153
153
|
end
|
154
154
|
|
155
|
-
|
155
|
+
websocket '/' do |ws|
|
156
156
|
websockets << ws
|
157
157
|
while msg = ws.read do
|
158
158
|
ws.write msg.to_json
|
@@ -164,7 +164,7 @@ describe Angelo::WebsocketResponder do
|
|
164
164
|
''
|
165
165
|
end
|
166
166
|
|
167
|
-
|
167
|
+
websocket '/one' do |ws|
|
168
168
|
websockets[:one] << ws
|
169
169
|
while msg = ws.read do
|
170
170
|
ws.write msg.to_json
|
@@ -176,7 +176,7 @@ describe Angelo::WebsocketResponder do
|
|
176
176
|
''
|
177
177
|
end
|
178
178
|
|
179
|
-
|
179
|
+
websocket '/other' do |ws|
|
180
180
|
websockets[:other] << ws
|
181
181
|
while msg = ws.read do
|
182
182
|
ws.write msg.to_json
|
@@ -2,12 +2,13 @@ require_relative './spec_helper'
|
|
2
2
|
|
3
3
|
describe Angelo::Base do
|
4
4
|
|
5
|
-
|
5
|
+
def obj
|
6
6
|
{'foo' => 'bar', 'bar' => 123.4567890123456, 'bat' => true}
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
|
+
def obj_s
|
9
10
|
obj.keys.reduce({}){|h,k| h[k] = obj[k].to_s; h}
|
10
|
-
|
11
|
+
end
|
11
12
|
|
12
13
|
describe 'the basics' do
|
13
14
|
|
@@ -35,24 +36,24 @@ describe Angelo::Base do
|
|
35
36
|
it 'responds to http requests properly' do
|
36
37
|
Angelo::HTTPABLE.each do |m|
|
37
38
|
__send__ m, '/'
|
38
|
-
|
39
|
+
last_response_must_be_html m.to_s
|
39
40
|
end
|
40
41
|
end
|
41
42
|
|
42
43
|
it 'responds to get requests with json properly' do
|
43
44
|
get '/json', obj
|
44
|
-
|
45
|
+
last_response_must_be_json obj_s
|
45
46
|
end
|
46
47
|
|
47
48
|
it 'responds to post requests with json properly' do
|
48
49
|
post '/json', obj.to_json, {'Content-Type' => Angelo::JSON_TYPE}
|
49
|
-
|
50
|
+
last_response_must_be_json obj
|
50
51
|
end
|
51
52
|
|
52
53
|
it 'redirects' do
|
53
54
|
get '/redirect'
|
54
|
-
|
55
|
-
|
55
|
+
last_response.status.must_equal 301
|
56
|
+
last_response.headers['Location'].must_equal '/'
|
56
57
|
end
|
57
58
|
|
58
59
|
end
|
@@ -77,11 +78,11 @@ describe Angelo::Base do
|
|
77
78
|
it 'runs before filters before routes' do
|
78
79
|
|
79
80
|
get '/before', obj
|
80
|
-
|
81
|
+
last_response_must_be_json obj_s
|
81
82
|
|
82
83
|
[:post, :put].each do |m|
|
83
84
|
__send__ m, '/before', obj.to_json, {Angelo::CONTENT_TYPE_HEADER_KEY => Angelo::JSON_TYPE}
|
84
|
-
|
85
|
+
last_response_must_be_json obj
|
85
86
|
end
|
86
87
|
|
87
88
|
end
|
@@ -115,8 +116,8 @@ describe Angelo::Base do
|
|
115
116
|
b = [4, 12, 28, 60]
|
116
117
|
Angelo::HTTPABLE.each_with_index do |m,i|
|
117
118
|
__send__ m, '/after', obj
|
118
|
-
|
119
|
-
invoked.
|
119
|
+
last_response_must_be_html a[i]
|
120
|
+
invoked.must_equal b[i]
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
@@ -138,16 +139,16 @@ describe Angelo::Base do
|
|
138
139
|
|
139
140
|
it 'sets headers for a response' do
|
140
141
|
put '/incr'
|
141
|
-
|
142
|
+
last_response.headers['X-Http-Angelo-Server'].must_equal 'catbutt'
|
142
143
|
end
|
143
144
|
|
144
145
|
it 'does not carry headers over responses' do
|
145
146
|
headers_count = 0
|
146
147
|
put '/incr'
|
147
|
-
|
148
|
+
last_response.headers['X-Http-Angelo-Server'].must_equal 'catbutt'
|
148
149
|
|
149
150
|
put '/incr'
|
150
|
-
|
151
|
+
last_response.headers['X-Http-Angelo-Server'].must_be_nil
|
151
152
|
end
|
152
153
|
|
153
154
|
end
|
@@ -190,35 +191,35 @@ describe Angelo::Base do
|
|
190
191
|
it 'sets html content type for current route' do
|
191
192
|
Angelo::HTTPABLE.each do |m|
|
192
193
|
__send__ m, '/html'
|
193
|
-
|
194
|
+
last_response_must_be_html '<html><body>hi</body></html>'
|
194
195
|
end
|
195
196
|
end
|
196
197
|
|
197
198
|
it 'sets json content type for current route and to_jsons hashes' do
|
198
199
|
Angelo::HTTPABLE.each do |m|
|
199
200
|
__send__ m, '/json'
|
200
|
-
|
201
|
+
last_response_must_be_json 'hi' => 'there'
|
201
202
|
end
|
202
203
|
end
|
203
204
|
|
204
205
|
it 'does not to_json strings' do
|
205
206
|
Angelo::HTTPABLE.each do |m|
|
206
207
|
__send__ m, '/json_s'
|
207
|
-
|
208
|
+
last_response_must_be_json 'woo' => 'woo'
|
208
209
|
end
|
209
210
|
end
|
210
211
|
|
211
212
|
it '500s on html hashes' do
|
212
213
|
Angelo::HTTPABLE.each do |m|
|
213
214
|
__send__ m, '/bad_html_h'
|
214
|
-
last_response.status.
|
215
|
+
last_response.status.must_equal 500
|
215
216
|
end
|
216
217
|
end
|
217
218
|
|
218
219
|
it '500s on bad json strings' do
|
219
220
|
Angelo::HTTPABLE.each do |m|
|
220
221
|
__send__ m, '/bad_json_s'
|
221
|
-
last_response.status.
|
222
|
+
last_response.status.must_equal 500
|
222
223
|
end
|
223
224
|
end
|
224
225
|
|
@@ -240,7 +241,7 @@ describe Angelo::Base do
|
|
240
241
|
it 'sets default content type' do
|
241
242
|
Angelo::HTTPABLE.each do |m|
|
242
243
|
__send__ m, '/html'
|
243
|
-
|
244
|
+
last_response_must_be_html '<html><body>hi</body></html>'
|
244
245
|
end
|
245
246
|
end
|
246
247
|
|
@@ -260,7 +261,7 @@ describe Angelo::Base do
|
|
260
261
|
it 'sets default content type' do
|
261
262
|
Angelo::HTTPABLE.each do |m|
|
262
263
|
__send__ m, '/json'
|
263
|
-
|
264
|
+
last_response_must_be_json 'hi' => 'there'
|
264
265
|
end
|
265
266
|
end
|
266
267
|
end
|
@@ -284,7 +285,7 @@ describe Angelo::Base do
|
|
284
285
|
it 'sets html content type for current route when default is set json' do
|
285
286
|
Angelo::HTTPABLE.each do |m|
|
286
287
|
__send__ m, '/json'
|
287
|
-
|
288
|
+
last_response_must_be_json 'hi' => 'there'
|
288
289
|
end
|
289
290
|
end
|
290
291
|
|
@@ -305,7 +306,7 @@ describe Angelo::Base do
|
|
305
306
|
it 'sets json content type for current route when default is set html' do
|
306
307
|
Angelo::HTTPABLE.each do |m|
|
307
308
|
__send__ m, '/html'
|
308
|
-
|
309
|
+
last_response_must_be_html '<html><body>hi</body></html>'
|
309
310
|
end
|
310
311
|
end
|
311
312
|
|
@@ -330,17 +331,17 @@ describe Angelo::Base do
|
|
330
331
|
|
331
332
|
it 'parses formencoded body when content-type is formencoded' do
|
332
333
|
post '/json', obj, {'Content-Type' => Angelo::FORM_TYPE}
|
333
|
-
|
334
|
+
last_response_must_be_json obj_s
|
334
335
|
end
|
335
336
|
|
336
337
|
it 'does not parse JSON body when content-type is formencoded' do
|
337
338
|
post '/json', obj.to_json, {'Content-Type' => Angelo::FORM_TYPE}
|
338
|
-
last_response.status.
|
339
|
+
last_response.status.must_equal 400
|
339
340
|
end
|
340
341
|
|
341
342
|
it 'does not parse body when request content-type not set' do
|
342
343
|
post '/json', obj, {'Content-Type' => ''}
|
343
|
-
|
344
|
+
last_response_must_be_json({})
|
344
345
|
end
|
345
346
|
|
346
347
|
end
|
@@ -375,7 +376,7 @@ describe Angelo::Base do
|
|
375
376
|
}
|
376
377
|
|
377
378
|
get '/rh', ps, hs
|
378
|
-
|
379
|
+
last_response_must_be_json 'values' => hs.values
|
379
380
|
end
|
380
381
|
|
381
382
|
end
|
@@ -1,11 +1,13 @@
|
|
1
|
-
$:.unshift File.expand_path '
|
1
|
+
$:.unshift File.expand_path '../../lib', __FILE__
|
2
2
|
|
3
3
|
require 'bundler'
|
4
4
|
Bundler.require :default, :development, :test
|
5
|
+
require 'minitest/pride'
|
6
|
+
require 'minitest/autorun'
|
5
7
|
require 'angelo'
|
6
|
-
require 'angelo/
|
8
|
+
require 'angelo/minitest/helpers'
|
7
9
|
Celluloid.logger.level = ::Logger::ERROR
|
8
|
-
include Angelo::
|
10
|
+
include Angelo::Minitest::Helpers
|
9
11
|
|
10
12
|
TEST_APP_ROOT = File.expand_path '../test_app_root', __FILE__
|
11
13
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
metadata
CHANGED
@@ -1,41 +1,41 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: angelo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
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-
|
11
|
+
date: 2014-05-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: reel
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: mime-types
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
description: A Sinatra-esque DSL for Reel
|
@@ -45,36 +45,37 @@ executables: []
|
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
|
-
- .gitignore
|
49
|
-
- .travis.yml
|
48
|
+
- ".gitignore"
|
49
|
+
- ".travis.yml"
|
50
50
|
- CHANGELOG.md
|
51
51
|
- Gemfile
|
52
52
|
- LICENSE
|
53
53
|
- README.md
|
54
|
+
- Rakefile
|
54
55
|
- angelo.gemspec
|
55
56
|
- lib/angelo.rb
|
56
57
|
- lib/angelo/base.rb
|
58
|
+
- lib/angelo/minitest/helpers.rb
|
57
59
|
- lib/angelo/mustermann.rb
|
58
60
|
- lib/angelo/params_parser.rb
|
59
61
|
- lib/angelo/responder.rb
|
60
62
|
- lib/angelo/responder/websocket.rb
|
61
|
-
- lib/angelo/rspec/helpers.rb
|
62
63
|
- lib/angelo/server.rb
|
63
64
|
- lib/angelo/tilt/erb.rb
|
64
65
|
- lib/angelo/version.rb
|
65
|
-
-
|
66
|
-
-
|
67
|
-
-
|
68
|
-
-
|
69
|
-
-
|
70
|
-
-
|
71
|
-
-
|
72
|
-
-
|
73
|
-
-
|
74
|
-
-
|
75
|
-
-
|
76
|
-
-
|
77
|
-
-
|
66
|
+
- test/angelo/erb_spec.rb
|
67
|
+
- test/angelo/mustermann_spec.rb
|
68
|
+
- test/angelo/params_spec.rb
|
69
|
+
- test/angelo/static_spec.rb
|
70
|
+
- test/angelo/websocket_spec.rb
|
71
|
+
- test/angelo_spec.rb
|
72
|
+
- test/spec_helper.rb
|
73
|
+
- test/test_app_root/public/test.css
|
74
|
+
- test/test_app_root/public/test.html
|
75
|
+
- test/test_app_root/public/test.js
|
76
|
+
- test/test_app_root/public/what.png
|
77
|
+
- test/test_app_root/views/index.html.erb
|
78
|
+
- test/test_app_root/views/layout.html.erb
|
78
79
|
homepage: https://github.com/kenichi/angelo
|
79
80
|
licenses:
|
80
81
|
- apache
|
@@ -85,31 +86,18 @@ require_paths:
|
|
85
86
|
- lib
|
86
87
|
required_ruby_version: !ruby/object:Gem::Requirement
|
87
88
|
requirements:
|
88
|
-
- -
|
89
|
+
- - ">="
|
89
90
|
- !ruby/object:Gem::Version
|
90
91
|
version: '0'
|
91
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
93
|
requirements:
|
93
|
-
- -
|
94
|
+
- - ">="
|
94
95
|
- !ruby/object:Gem::Version
|
95
96
|
version: '0'
|
96
97
|
requirements: []
|
97
98
|
rubyforge_project:
|
98
|
-
rubygems_version: 2.
|
99
|
+
rubygems_version: 2.2.2
|
99
100
|
signing_key:
|
100
101
|
specification_version: 4
|
101
102
|
summary: A Sinatra-esque DSL for Reel
|
102
|
-
test_files:
|
103
|
-
- spec/angelo/erb_spec.rb
|
104
|
-
- spec/angelo/mustermann_spec.rb
|
105
|
-
- spec/angelo/params_spec.rb
|
106
|
-
- spec/angelo/static_spec.rb
|
107
|
-
- spec/angelo/websocket_spec.rb
|
108
|
-
- spec/angelo_spec.rb
|
109
|
-
- spec/spec_helper.rb
|
110
|
-
- spec/test_app_root/public/test.css
|
111
|
-
- spec/test_app_root/public/test.html
|
112
|
-
- spec/test_app_root/public/test.js
|
113
|
-
- spec/test_app_root/public/what.png
|
114
|
-
- spec/test_app_root/views/index.html.erb
|
115
|
-
- spec/test_app_root/views/layout.html.erb
|
103
|
+
test_files: []
|
data/spec/angelo/static_spec.rb
DELETED
@@ -1,72 +0,0 @@
|
|
1
|
-
require_relative '../spec_helper'
|
2
|
-
require 'openssl'
|
3
|
-
|
4
|
-
describe Angelo::Server do
|
5
|
-
|
6
|
-
describe 'serving static files' do
|
7
|
-
|
8
|
-
let(:test_css_etag) do
|
9
|
-
fs = File::Stat.new File.join(TEST_APP_ROOT, 'public', 'test.css')
|
10
|
-
OpenSSL::Digest::SHA.hexdigest fs.ino.to_s + fs.size.to_s + fs.mtime.to_s
|
11
|
-
end
|
12
|
-
|
13
|
-
define_app do
|
14
|
-
|
15
|
-
@root = TEST_APP_ROOT
|
16
|
-
|
17
|
-
get '/test.html' do
|
18
|
-
'you should not see this'
|
19
|
-
end
|
20
|
-
|
21
|
-
end
|
22
|
-
|
23
|
-
it 'serves static files for gets' do
|
24
|
-
get '/test.css'
|
25
|
-
expect(last_response.status).to be(200)
|
26
|
-
expect(last_response.headers['Content-Type']).to eq('text/css')
|
27
|
-
expect(last_response.headers['Content-Disposition']).to eq('attachment; filename=test.css')
|
28
|
-
expect(last_response.headers['Content-Length']).to eq('116')
|
29
|
-
expect(last_response.headers['Etag']).to eq(test_css_etag)
|
30
|
-
expect(last_response.body.length).to be(116)
|
31
|
-
expect(last_response.body).to eq(File.read(File.join TEST_APP_ROOT, 'public', 'test.css'))
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'serves headers for static files on head' do
|
35
|
-
head '/test.css'
|
36
|
-
expect(last_response.status).to be(200)
|
37
|
-
expect(last_response.headers['Content-Type']).to eq('text/css')
|
38
|
-
expect(last_response.headers['Content-Disposition']).to eq('attachment; filename=test.css')
|
39
|
-
expect(last_response.headers['Content-Length']).to eq('116')
|
40
|
-
expect(last_response.headers['Etag']).to eq(test_css_etag)
|
41
|
-
expect(last_response.body.length).to be(0)
|
42
|
-
end
|
43
|
-
|
44
|
-
it 'serves static file over route' do
|
45
|
-
get '/test.html'
|
46
|
-
expect(last_response.status).to be(200)
|
47
|
-
expect(last_response.headers['Content-Type']).to eq('text/html')
|
48
|
-
expect(last_response.headers['Content-Disposition']).to eq('attachment; filename=test.html')
|
49
|
-
expect(last_response.body).to eq(File.read(File.join TEST_APP_ROOT, 'public', 'test.html'))
|
50
|
-
end
|
51
|
-
|
52
|
-
it 'not modifieds when if-none-match matched etag' do
|
53
|
-
get '/test.css', {}, {'If-None-Match' => test_css_etag}
|
54
|
-
expect(last_response.status).to be(304)
|
55
|
-
end
|
56
|
-
|
57
|
-
it 'serves proper content-types' do
|
58
|
-
{ 'test.js' => 'application/javascript',
|
59
|
-
'test.html' => 'text/html',
|
60
|
-
'test.css' => 'text/css',
|
61
|
-
'what.png' => 'image/png' }.each do |k,v|
|
62
|
-
|
63
|
-
get "/#{k}"
|
64
|
-
expect(last_response.status).to be(200)
|
65
|
-
expect(last_response.headers['Content-Type']).to eq(v)
|
66
|
-
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
end
|