angelo 0.4.1 → 0.5.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/.travis.yml +2 -6
- data/CHANGELOG.md +15 -1
- data/Gemfile +3 -7
- data/LICENSE +1 -1
- data/README.md +42 -5
- data/angelo.gemspec +1 -1
- data/code_of_conduct.md +50 -0
- data/lib/angelo.rb +18 -12
- data/lib/angelo/base.rb +81 -77
- data/lib/angelo/minitest/helpers.rb +10 -4
- data/lib/angelo/params_parser.rb +12 -3
- data/lib/angelo/responder.rb +14 -10
- data/lib/angelo/responder/eventsource.rb +2 -2
- data/lib/angelo/responder/websocket.rb +3 -12
- data/lib/angelo/server.rb +16 -5
- data/lib/angelo/stash.rb +8 -3
- data/lib/angelo/templates.rb +166 -0
- data/lib/angelo/tilt/erb.rb +15 -100
- data/lib/angelo/version.rb +1 -1
- data/test/angelo/erb_spec.rb +6 -0
- data/test/angelo/filter_spec.rb +17 -0
- data/test/angelo/params_spec.rb +21 -0
- data/test/angelo/stash_spec.rb +33 -3
- data/test/angelo/websocket_spec.rb +12 -12
- data/test/angelo_spec.rb +50 -8
- data/test/spec_helper.rb +2 -2
- metadata +7 -5
data/lib/angelo/tilt/erb.rb
CHANGED
@@ -1,118 +1,33 @@
|
|
1
|
-
require 'erb'
|
2
|
-
require 'tilt'
|
1
|
+
require 'tilt/erb'
|
3
2
|
|
4
3
|
module Angelo
|
5
4
|
module Tilt
|
6
5
|
module ERB
|
7
6
|
|
8
|
-
DEFAULT_LAYOUT = 'layout.%s
|
7
|
+
DEFAULT_LAYOUT = 'layout.%s'
|
9
8
|
DEFAULT_TYPE = :html
|
10
9
|
LAYOUTS_DIR = 'layouts'
|
11
10
|
ACCEPT_ALL = '*/*'
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
base.extend ClassMethods
|
17
|
-
end
|
18
|
-
|
19
|
-
module ClassMethods
|
20
|
-
|
21
|
-
@reload_templates = false
|
22
|
-
|
23
|
-
def view_glob *glob
|
24
|
-
File.join views_dir, *glob
|
25
|
-
end
|
26
|
-
|
27
|
-
def templatify *glob
|
28
|
-
Dir[view_glob *glob].reduce({}) do |h,v|
|
29
|
-
sym = v.gsub views_dir + File::SEPARATOR, ''
|
30
|
-
return h if (block_given? && yield(v))
|
31
|
-
sym.gsub! File::SEPARATOR, UNDERSCORE
|
32
|
-
sym.gsub! /\.\w+?\.erb$/, EMPTY_STRING
|
33
|
-
sym.gsub! /^#{LAYOUTS_DIR}#{UNDERSCORE}/, EMPTY_STRING
|
34
|
-
h[sym.to_sym] = ::Tilt::ERBTemplate.new v
|
35
|
-
h
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def templates type = DEFAULT_TYPE
|
40
|
-
@templates ||= {}
|
41
|
-
if reload_templates?
|
42
|
-
@templates[type] = load_templates(type)
|
43
|
-
else
|
44
|
-
@templates[type] ||= load_templates(type)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def load_templates type = DEFAULT_TYPE
|
49
|
-
templatify('**', "*.#{type}.erb") do |v|
|
50
|
-
v =~ /^#{LAYOUTS_DIR}#{File::SEPARATOR}/
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def layout_templates type = DEFAULT_TYPE
|
55
|
-
if reload_templates?
|
56
|
-
@layout_templates = load_layout_templates(type)
|
57
|
-
else
|
58
|
-
@layout_templates ||= load_layout_templates(type)
|
59
|
-
end
|
60
|
-
end
|
12
|
+
def erb view, opts = {}
|
13
|
+
type = opts.delete(:type) || template_type
|
14
|
+
content_type type
|
61
15
|
|
62
|
-
|
63
|
-
|
16
|
+
if view.is_a? Symbol
|
17
|
+
view = :"#{view}.#{type}"
|
64
18
|
end
|
65
19
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
20
|
+
layout =
|
21
|
+
case opts[:layout]
|
22
|
+
when false
|
23
|
+
false
|
24
|
+
when Symbol
|
25
|
+
:"#{LAYOUTS_DIR}/#{layout}.#{type}"
|
70
26
|
else
|
71
|
-
|
27
|
+
:"#{DEFAULT_LAYOUT % type}"
|
72
28
|
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def load_default_layout type = DEFAULT_TYPE
|
76
|
-
l = view_glob(DEFAULT_LAYOUT % type)
|
77
|
-
::Tilt::ERBTemplate.new l if File.exist? l
|
78
|
-
end
|
79
|
-
|
80
|
-
def reload_templates! on = true
|
81
|
-
@reload_templates = on
|
82
|
-
end
|
83
|
-
|
84
|
-
def reload_templates?
|
85
|
-
@reload_templates
|
86
|
-
end
|
87
29
|
|
88
|
-
|
89
|
-
|
90
|
-
def erb view, opts = {locals: {}}
|
91
|
-
type = opts[:type] || template_type
|
92
|
-
content_type type
|
93
|
-
locals = Hash === opts[:locals] ? opts[:locals] : {}
|
94
|
-
render = case view
|
95
|
-
when String
|
96
|
-
->{ view }
|
97
|
-
when Symbol
|
98
|
-
->{self.class.templates(type)[view].render self, locals}
|
99
|
-
end
|
100
|
-
case opts[:layout]
|
101
|
-
when false
|
102
|
-
render[]
|
103
|
-
when Symbol
|
104
|
-
if lt = self.class.layout_templates(type)[opts[:layout]]
|
105
|
-
lt.render self, locals, &render
|
106
|
-
else
|
107
|
-
raise ArgumentError.new "unknown layout - :#{opts[:layout]}"
|
108
|
-
end
|
109
|
-
else
|
110
|
-
if self.class.default_layout(type)
|
111
|
-
self.class.default_layout(type).render self, locals, &render
|
112
|
-
else
|
113
|
-
render[]
|
114
|
-
end
|
115
|
-
end
|
30
|
+
_erb view, layout: layout, locals: opts[:locals]
|
116
31
|
end
|
117
32
|
|
118
33
|
def template_type
|
data/lib/angelo/version.rb
CHANGED
data/test/angelo/erb_spec.rb
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
# Having to require this sucks because tilt is an implementation
|
2
|
+
# detail we shouldn't have to know about. But this avoids a "tilt
|
3
|
+
# autoloading 'tilt/erb' in a non thread-safe way" wanring.
|
4
|
+
#
|
5
|
+
require 'tilt/erb'
|
6
|
+
|
1
7
|
require_relative '../spec_helper'
|
2
8
|
|
3
9
|
describe Angelo::Base do
|
data/test/angelo/filter_spec.rb
CHANGED
@@ -90,6 +90,10 @@ describe Angelo::Base do
|
|
90
90
|
@bat = params[:bat] if @foo
|
91
91
|
end
|
92
92
|
|
93
|
+
before %r{/before/(\d+)} do
|
94
|
+
@id = params[:id].to_i
|
95
|
+
end
|
96
|
+
|
93
97
|
[:get, :post, :put].each do |m|
|
94
98
|
|
95
99
|
__send__ m, '/before' do
|
@@ -106,6 +110,11 @@ describe Angelo::Base do
|
|
106
110
|
content_type :json
|
107
111
|
{ foo: @foo, bar: @bar, bat: @bat }.select! {|k,v| !v.nil?}
|
108
112
|
end
|
113
|
+
|
114
|
+
__send__ m, '/before/:id' do
|
115
|
+
content_type :json
|
116
|
+
{ id: @id }
|
117
|
+
end
|
109
118
|
end
|
110
119
|
|
111
120
|
end
|
@@ -142,6 +151,14 @@ describe Angelo::Base do
|
|
142
151
|
|
143
152
|
end
|
144
153
|
|
154
|
+
it 'matches regexes' do
|
155
|
+
[:get, :post, :put].each do |m|
|
156
|
+
id = rand 1000
|
157
|
+
__send__ m, "/before/#{id}"
|
158
|
+
last_response_must_be_json({'id' => id})
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
145
162
|
end
|
146
163
|
|
147
164
|
end
|
data/test/angelo/params_spec.rb
CHANGED
@@ -67,6 +67,27 @@ describe Angelo::ParamsParser do
|
|
67
67
|
parser.parse_post_body.must_equal({})
|
68
68
|
end
|
69
69
|
|
70
|
+
it "doesn't barf on JSON array POST bodies" do
|
71
|
+
parser.form_encoded = false
|
72
|
+
parser.json = true
|
73
|
+
parser.body = [123,234].to_json
|
74
|
+
->{ parser.parse_post_body }.must_be_silent
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'raises properly on malformed JSON' do
|
78
|
+
parser.form_encoded = false
|
79
|
+
parser.json = true
|
80
|
+
parser.body = "{F(34nfnlv,-935;:fho2fhlj}}}function(){console.log('hi');}"
|
81
|
+
->{ parser.parse_post_body }.must_raise JSON::ParserError
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'parses JSON array POST bodies' do
|
85
|
+
parser.form_encoded = false
|
86
|
+
parser.json = true
|
87
|
+
parser.body = [123,234].to_json
|
88
|
+
parser.parse_post_body.must_equal [123,234]
|
89
|
+
end
|
90
|
+
|
70
91
|
it 'recursively symhashes JSON POST bodies params' do
|
71
92
|
nested = {
|
72
93
|
foo: {
|
data/test/angelo/stash_spec.rb
CHANGED
@@ -25,23 +25,53 @@ describe Angelo::Stash do
|
|
25
25
|
stashes[@context] << s
|
26
26
|
end
|
27
27
|
|
28
|
+
def self.clear
|
29
|
+
stashes.values.each &:clear
|
30
|
+
end
|
31
|
+
|
28
32
|
end
|
29
33
|
|
30
|
-
|
31
|
-
|
34
|
+
after do
|
35
|
+
TestStash.clear
|
36
|
+
end
|
32
37
|
|
38
|
+
def mock_good_sock
|
33
39
|
good_sock = Minitest::Mock.new
|
34
40
|
good_sock.expect :read, "hi"
|
35
41
|
good_sock.expect :hash, 123
|
36
|
-
def good_sock.== o
|
42
|
+
def good_sock.== o
|
43
|
+
o == self
|
44
|
+
end
|
45
|
+
def good_sock.eql? o
|
46
|
+
self.object_id == o.object_id
|
47
|
+
end
|
48
|
+
good_sock
|
49
|
+
end
|
37
50
|
|
51
|
+
it 'does not skip live sockets when removing dead sockets' do
|
38
52
|
stash = TestStash.new nil
|
53
|
+
good_sock = mock_good_sock
|
54
|
+
err_sock = ErrorSocket.new
|
39
55
|
|
40
56
|
stash << err_sock
|
41
57
|
stash << good_sock
|
42
58
|
|
43
59
|
stash.each {|s| s.read}
|
44
60
|
good_sock.verify
|
61
|
+
stash.each {|s| err_sock.wont_equal s}
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'removes sockets from contexts during all_each' do
|
65
|
+
stash = TestStash.new nil
|
66
|
+
good_sock = mock_good_sock
|
67
|
+
err_sock = ErrorSocket.new
|
68
|
+
|
69
|
+
stash[:foo] << err_sock
|
70
|
+
stash[:bar] << good_sock
|
71
|
+
|
72
|
+
stash.all_each {|s| s.read}
|
73
|
+
good_sock.verify
|
74
|
+
stash.all_each {|s| err_sock.wont_equal s}
|
45
75
|
end
|
46
76
|
|
47
77
|
end
|
@@ -14,10 +14,10 @@ describe Angelo::Responder::Websocket do
|
|
14
14
|
end
|
15
15
|
action = (key.to_s + '_go').to_sym
|
16
16
|
Reactor.define_action action do |n|
|
17
|
-
every(0.01){ terminate if Reactor.stop? }
|
18
17
|
Reactor.testers[key][n].go
|
19
18
|
end
|
20
19
|
Reactor.unstop!
|
20
|
+
$reactor.async.wait_for_stop
|
21
21
|
CONCURRENCY.times {|n| $reactor.async.__send__(action, n)}
|
22
22
|
|
23
23
|
sleep 0.01 * CONCURRENCY
|
@@ -51,10 +51,10 @@ describe Angelo::Responder::Websocket do
|
|
51
51
|
wsh.init
|
52
52
|
Reactor.testers[:tester] = wsh
|
53
53
|
Reactor.define_action :go do
|
54
|
-
every(0.01){ terminate if Reactor.stop? }
|
55
54
|
Reactor.testers[:tester].go
|
56
55
|
end
|
57
56
|
Reactor.unstop!
|
57
|
+
$reactor.async.wait_for_stop
|
58
58
|
$reactor.async.go
|
59
59
|
|
60
60
|
500.times {|n| wsh.text "hi there #{n}"}
|
@@ -69,7 +69,7 @@ describe Angelo::Responder::Websocket do
|
|
69
69
|
it 'responds on multiple websockets properly' do
|
70
70
|
latch = CountDownLatch.new CONCURRENCY * 500
|
71
71
|
|
72
|
-
Reactor.testers[:wshs] = Array.new
|
72
|
+
Reactor.testers[:wshs] = Array.new CONCURRENCY do
|
73
73
|
wsh = websocket_helper '/'
|
74
74
|
wsh.on_message = ->(e) {
|
75
75
|
assert_match /hi there \d/, e.data
|
@@ -80,18 +80,18 @@ describe Angelo::Responder::Websocket do
|
|
80
80
|
end
|
81
81
|
|
82
82
|
Reactor.define_action :go do |n|
|
83
|
-
every(0.01){ terminate if Reactor.stop? }
|
84
83
|
Reactor.testers[:wshs][n].go
|
85
84
|
end
|
86
85
|
Reactor.unstop!
|
86
|
+
$reactor.async.wait_for_stop
|
87
87
|
CONCURRENCY.times {|n| $reactor.async.go n}
|
88
88
|
|
89
89
|
sleep 0.01 * CONCURRENCY
|
90
90
|
|
91
|
-
|
91
|
+
Actor.define_action :go do |n|
|
92
92
|
500.times {|x| Reactor.testers[:wshs][n].text "hi there #{x}"}
|
93
93
|
end
|
94
|
-
CONCURRENCY.times {|n| $pool.async.go n}
|
94
|
+
CONCURRENCY.times {|n| $pool[n].async.go n}
|
95
95
|
latch.wait
|
96
96
|
|
97
97
|
Reactor.testers[:wshs].map &:close
|
@@ -99,7 +99,7 @@ describe Angelo::Responder::Websocket do
|
|
99
99
|
Reactor.testers.delete :wshs
|
100
100
|
Reactor.remove_action :go
|
101
101
|
|
102
|
-
|
102
|
+
Actor.remove_action :go
|
103
103
|
end
|
104
104
|
|
105
105
|
end
|
@@ -127,10 +127,10 @@ describe Angelo::Responder::Websocket do
|
|
127
127
|
wsh.init
|
128
128
|
Reactor.testers[:tester] = wsh
|
129
129
|
Reactor.define_action :go do
|
130
|
-
every(0.01){ terminate if Reactor.stop? }
|
131
130
|
Reactor.testers[:tester].go
|
132
131
|
end
|
133
132
|
Reactor.unstop!
|
133
|
+
$reactor.async.wait_for_stop
|
134
134
|
$reactor.async.go
|
135
135
|
|
136
136
|
latch.wait
|
@@ -168,10 +168,10 @@ describe Angelo::Responder::Websocket do
|
|
168
168
|
wsh.init
|
169
169
|
Reactor.testers[:tester] = wsh
|
170
170
|
Reactor.define_action :go do
|
171
|
-
every(0.01){ terminate if Reactor.stop? }
|
172
171
|
Reactor.testers[:tester].go
|
173
172
|
end
|
174
173
|
Reactor.unstop!
|
174
|
+
$reactor.async.wait_for_stop
|
175
175
|
$reactor.async.go
|
176
176
|
|
177
177
|
latch.wait
|
@@ -286,7 +286,7 @@ describe Angelo::Responder::Websocket do
|
|
286
286
|
|
287
287
|
latch = CountDownLatch.new CONCURRENCY
|
288
288
|
|
289
|
-
Reactor.testers[:hmc] = Array.new
|
289
|
+
Reactor.testers[:hmc] = Array.new CONCURRENCY do
|
290
290
|
wsh = websocket_helper '/'
|
291
291
|
wsh.on_message = ->(e) {
|
292
292
|
wait_for_block[e]
|
@@ -298,7 +298,7 @@ describe Angelo::Responder::Websocket do
|
|
298
298
|
|
299
299
|
one_latch = CountDownLatch.new CONCURRENCY
|
300
300
|
|
301
|
-
Reactor.testers[:hmc_one] = Array.new
|
301
|
+
Reactor.testers[:hmc_one] = Array.new CONCURRENCY do
|
302
302
|
wsh = websocket_helper '/one'
|
303
303
|
wsh.on_message = ->(e) {
|
304
304
|
wait_for_block[e]
|
@@ -310,7 +310,7 @@ describe Angelo::Responder::Websocket do
|
|
310
310
|
|
311
311
|
other_latch = CountDownLatch.new CONCURRENCY
|
312
312
|
|
313
|
-
Reactor.testers[:hmc_other] = Array.new
|
313
|
+
Reactor.testers[:hmc_other] = Array.new CONCURRENCY do
|
314
314
|
wsh = websocket_helper '/other'
|
315
315
|
wsh.on_message = ->(e) {
|
316
316
|
wait_for_block[e]
|
data/test/angelo_spec.rb
CHANGED
@@ -23,6 +23,10 @@ describe Angelo::Base do
|
|
23
23
|
redirect '/'
|
24
24
|
end
|
25
25
|
|
26
|
+
get '/redirect_forever' do
|
27
|
+
redirect! '/'
|
28
|
+
end
|
29
|
+
|
26
30
|
get '/wait' do
|
27
31
|
sleep 3
|
28
32
|
nil
|
@@ -49,6 +53,12 @@ describe Angelo::Base do
|
|
49
53
|
|
50
54
|
it 'redirects' do
|
51
55
|
get '/redirect'
|
56
|
+
last_response.status.must_equal 302
|
57
|
+
last_response.headers['Location'].must_equal '/'
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'redirects permanently' do
|
61
|
+
get '/redirect_forever'
|
52
62
|
last_response.status.must_equal 301
|
53
63
|
last_response.headers['Location'].must_equal '/'
|
54
64
|
end
|
@@ -58,29 +68,29 @@ describe Angelo::Base do
|
|
58
68
|
get_end = nil
|
59
69
|
latch = CountDownLatch.new 2
|
60
70
|
|
61
|
-
|
71
|
+
Actor.define_action :do_wait do
|
62
72
|
get '/wait'
|
63
73
|
wait_end = Time.now
|
64
74
|
latch.count_down
|
65
75
|
end
|
66
76
|
|
67
|
-
|
77
|
+
Actor.define_action :do_get do
|
68
78
|
sleep 1
|
69
79
|
get '/'
|
70
80
|
get_end = Time.now
|
71
81
|
latch.count_down
|
72
82
|
end
|
73
83
|
|
74
|
-
|
75
|
-
$pool.async :do_wait
|
76
|
-
$pool.async :do_get
|
84
|
+
Actor.unstop!
|
85
|
+
$pool[0].async :do_wait
|
86
|
+
$pool[1].async :do_get
|
77
87
|
|
78
88
|
latch.wait
|
79
89
|
get_end.must_be :<, wait_end
|
80
90
|
|
81
|
-
|
82
|
-
|
83
|
-
|
91
|
+
Actor.stop!
|
92
|
+
Actor.remove_action :do_wait
|
93
|
+
Actor.remove_action :do_get
|
84
94
|
end
|
85
95
|
|
86
96
|
it 'does not crash when receiving unknown http request type' do
|
@@ -318,6 +328,11 @@ describe Angelo::Base do
|
|
318
328
|
end
|
319
329
|
end
|
320
330
|
|
331
|
+
post '/json_array' do
|
332
|
+
content_type :json
|
333
|
+
{params: params, body: request_body}
|
334
|
+
end
|
335
|
+
|
321
336
|
end
|
322
337
|
|
323
338
|
it 'parses formencoded body when content-type is formencoded' do
|
@@ -349,6 +364,19 @@ describe Angelo::Base do
|
|
349
364
|
end
|
350
365
|
end
|
351
366
|
|
367
|
+
it 'parses JSON array bodies but does not merge into params' do
|
368
|
+
post '/json_array?foo=bar', [123,234].to_json, {'Content-Type' => Angelo::JSON_TYPE}
|
369
|
+
last_response_must_be_json({
|
370
|
+
"params" => {"foo" => "bar"},
|
371
|
+
"body" => [123,234]
|
372
|
+
})
|
373
|
+
end
|
374
|
+
|
375
|
+
it 'does not die on malformed JSON' do
|
376
|
+
post '/json_array?foo=bar', '{]sdfj2if08yth4j]j,:jsd;f; function()', {'Content-Type' => Angelo::JSON_TYPE}
|
377
|
+
last_response.status.must_equal 400
|
378
|
+
end
|
379
|
+
|
352
380
|
end
|
353
381
|
|
354
382
|
describe 'request_headers helper' do
|
@@ -555,6 +583,20 @@ describe Angelo::Base do
|
|
555
583
|
|
556
584
|
end
|
557
585
|
|
586
|
+
describe 'default_headers' do
|
587
|
+
|
588
|
+
define_app do
|
589
|
+
default_headers "Access-Control-Allow-Origin" => "*"
|
590
|
+
get('/'){ 'hi' }
|
591
|
+
end
|
592
|
+
|
593
|
+
it 'adds a default headers' do
|
594
|
+
get '/'
|
595
|
+
last_response.headers['Access-Control-Allow-Origin'].must_equal "*"
|
596
|
+
end
|
597
|
+
|
598
|
+
end
|
599
|
+
|
558
600
|
end
|
559
601
|
|
560
602
|
end
|