angelo 0.3.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile +5 -4
- data/README.md +101 -57
- data/angelo.gemspec +13 -10
- data/lib/angelo.rb +5 -2
- data/lib/angelo/base.rb +136 -54
- data/lib/angelo/main.rb +47 -0
- data/lib/angelo/minitest/helpers.rb +51 -23
- data/lib/angelo/params_parser.rb +33 -17
- data/lib/angelo/responder.rb +10 -9
- data/lib/angelo/responder/eventsource.rb +21 -20
- data/lib/angelo/responder/websocket.rb +1 -2
- data/lib/angelo/stash.rb +5 -3
- data/lib/angelo/tilt/erb.rb +38 -11
- data/lib/angelo/version.rb +1 -1
- data/test/angelo/erb_spec.rb +65 -3
- data/test/angelo/mustermann_spec.rb +1 -8
- data/test/angelo/options_spec.rb +19 -0
- data/test/angelo/params_spec.rb +27 -2
- data/test/angelo_spec.rb +18 -0
- data/test/main/app.rb +38 -0
- data/test/main/main_spec.rb +101 -0
- data/test/spec_helper.rb +7 -39
- metadata +37 -6
- data/lib/angelo/mustermann.rb +0 -98
data/lib/angelo/main.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require "angelo"
|
2
|
+
|
3
|
+
module Angelo
|
4
|
+
class Base
|
5
|
+
@angelo_main = true
|
6
|
+
|
7
|
+
# Takes a block and/or modules that define methods that can be
|
8
|
+
# used from within request handlers. Methods can also be defined
|
9
|
+
# at the top level but they get defined as Object instance methods
|
10
|
+
# which isn't good.
|
11
|
+
|
12
|
+
module DSL
|
13
|
+
def helpers(*args, &block)
|
14
|
+
args.each do |mod|
|
15
|
+
include(mod)
|
16
|
+
end
|
17
|
+
block && class_exec(&block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# The intent here is to add the DSL to the top-level object "main" by
|
25
|
+
# creating an anonymous Angelo::Base subclass and forwarding the DSL
|
26
|
+
# methods to it from the top-level object. Then run the app at exit.
|
27
|
+
#
|
28
|
+
# If angelo/main is required from somewhere other than the top level,
|
29
|
+
# all bets are off.
|
30
|
+
|
31
|
+
if self.to_s == "main"
|
32
|
+
# We are probably at the top level.
|
33
|
+
|
34
|
+
require "forwardable"
|
35
|
+
self.extend Forwardable
|
36
|
+
@angelo_app = Class.new(Angelo::Base)
|
37
|
+
self.def_delegators :@angelo_app, *Angelo::Base::DSL.instance_methods
|
38
|
+
|
39
|
+
at_exit do
|
40
|
+
# Don't run @angelo_app on uncaught exceptions including exit
|
41
|
+
# being called which raises SystemExit. The rationale being that
|
42
|
+
# exit means exit, not "run the server".
|
43
|
+
if $!.nil?
|
44
|
+
@angelo_app.run!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -9,25 +9,25 @@ module Angelo
|
|
9
9
|
|
10
10
|
attr_reader :last_response
|
11
11
|
|
12
|
-
def define_app &block
|
13
|
-
|
12
|
+
def define_app app = nil, &block
|
14
13
|
before do
|
15
|
-
app
|
16
|
-
|
17
|
-
|
14
|
+
if app.nil? && block
|
15
|
+
app = Class.new Angelo::Base do
|
16
|
+
content_type :html # reset
|
17
|
+
class_eval &block
|
18
|
+
end
|
19
|
+
end
|
18
20
|
Celluloid.logger.level = ::Logger::ERROR # see spec_helper.rb:9
|
19
21
|
|
20
|
-
app.class_eval &block
|
21
22
|
@server = Angelo::Server.new app
|
22
23
|
app.server = @server
|
23
|
-
$reactor = Reactor.new
|
24
|
+
$reactor = Reactor.new if $reactor == nil || !$reactor.alive?
|
24
25
|
end
|
25
26
|
|
26
27
|
after do
|
27
28
|
sleep 0.1
|
28
29
|
@server.terminate if @server and @server.alive?
|
29
30
|
end
|
30
|
-
|
31
31
|
end
|
32
32
|
|
33
33
|
def hc
|
@@ -40,12 +40,8 @@ module Angelo
|
|
40
40
|
url
|
41
41
|
end
|
42
42
|
|
43
|
-
def hc_req method, path, params = {}, headers = {}
|
44
|
-
@last_response =
|
45
|
-
hc.__send__ method, url(path), params, headers, &Proc.new
|
46
|
-
else
|
47
|
-
hc.__send__ method, url(path), params, headers
|
48
|
-
end
|
43
|
+
def hc_req method, path, params = {}, headers = {}, &block
|
44
|
+
@last_response = hc.__send__ method, url(path), params, headers, &block
|
49
45
|
end
|
50
46
|
private :hc_req
|
51
47
|
|
@@ -70,15 +66,8 @@ module Angelo
|
|
70
66
|
private :http_req
|
71
67
|
|
72
68
|
[:get, :post, :put, :delete, :options, :head].each do |m|
|
73
|
-
define_method m do |path, params = {}, headers = {}|
|
74
|
-
|
75
|
-
# http_req m, path, params, headers
|
76
|
-
|
77
|
-
if block_given?
|
78
|
-
hc_req m, path, params, headers, &Proc.new
|
79
|
-
else
|
80
|
-
hc_req m, path, params, headers
|
81
|
-
end
|
69
|
+
define_method m do |path, params = {}, headers = {}, &block|
|
70
|
+
hc_req m, path, params, headers, &block
|
82
71
|
end
|
83
72
|
end
|
84
73
|
|
@@ -114,6 +103,44 @@ module Angelo
|
|
114
103
|
last_response.headers['Content-Type'].split(';').must_include JSON_TYPE
|
115
104
|
end
|
116
105
|
|
106
|
+
module Cellper
|
107
|
+
|
108
|
+
@@stop = false
|
109
|
+
@@testers = {}
|
110
|
+
|
111
|
+
def define_action sym, &block
|
112
|
+
define_method sym, &block
|
113
|
+
end
|
114
|
+
|
115
|
+
def remove_action sym
|
116
|
+
remove_method sym
|
117
|
+
end
|
118
|
+
|
119
|
+
def unstop!
|
120
|
+
@@stop = false
|
121
|
+
end
|
122
|
+
|
123
|
+
def stop!
|
124
|
+
@@stop = true
|
125
|
+
end
|
126
|
+
|
127
|
+
def stop?
|
128
|
+
@@stop
|
129
|
+
end
|
130
|
+
|
131
|
+
def testers; @@testers; end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Reactor
|
135
|
+
include Celluloid::IO
|
136
|
+
extend Cellper
|
137
|
+
end
|
138
|
+
|
139
|
+
class ActorPool
|
140
|
+
include Celluloid
|
141
|
+
extend Cellper
|
142
|
+
end
|
143
|
+
|
117
144
|
end
|
118
145
|
|
119
146
|
class WebsocketHelper
|
@@ -173,4 +200,5 @@ module Angelo
|
|
173
200
|
end
|
174
201
|
|
175
202
|
end
|
203
|
+
|
176
204
|
end
|
data/lib/angelo/params_parser.rb
CHANGED
@@ -9,7 +9,7 @@ module Angelo
|
|
9
9
|
EMPTY_JSON = '{}'
|
10
10
|
|
11
11
|
def parse_formencoded str
|
12
|
-
str.split(AMPERSAND).reduce(
|
12
|
+
str.split(AMPERSAND).reduce(SymHash.new) do |p, kv|
|
13
13
|
key, value = kv.split(EQUALS).map {|s| CGI.unescape s}
|
14
14
|
p[key] = value
|
15
15
|
p
|
@@ -22,28 +22,18 @@ module Angelo
|
|
22
22
|
|
23
23
|
def parse_post_body
|
24
24
|
body = request.body.to_s
|
25
|
-
qs = parse_query_string
|
26
25
|
case
|
27
26
|
when form_encoded?
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
body = EMPTY_JSON if body.empty?
|
32
|
-
body = JSON.parse body
|
33
|
-
recurse_symhash qs.merge! body
|
27
|
+
parse_formencoded body
|
28
|
+
when json? && !body.empty?
|
29
|
+
SymHash.new JSON.parse body
|
34
30
|
else
|
35
|
-
|
31
|
+
{}
|
36
32
|
end
|
37
33
|
end
|
38
34
|
|
39
|
-
def
|
40
|
-
|
41
|
-
if Hash === v
|
42
|
-
h[k] = Responder.symhash.merge! v
|
43
|
-
recurse_symhash h[k]
|
44
|
-
end
|
45
|
-
end
|
46
|
-
h
|
35
|
+
def parse_query_string_and_post_body
|
36
|
+
parse_query_string.merge! parse_post_body
|
47
37
|
end
|
48
38
|
|
49
39
|
def form_encoded?
|
@@ -64,4 +54,30 @@ module Angelo
|
|
64
54
|
|
65
55
|
end
|
66
56
|
|
57
|
+
class SymHash < Hash
|
58
|
+
|
59
|
+
# Returns a Hash that allows values to be fetched with String or
|
60
|
+
# Symbol keys.
|
61
|
+
def initialize h = nil
|
62
|
+
super(){|hash,key| hash[key.to_s] if Symbol === key}
|
63
|
+
unless h.nil?
|
64
|
+
merge! h
|
65
|
+
|
66
|
+
# Replace values that are Hashes with SymHashes, recursively.
|
67
|
+
each do |k,v|
|
68
|
+
self[k] = case v
|
69
|
+
when Hash
|
70
|
+
SymHash.new(v)
|
71
|
+
when Array
|
72
|
+
v.map {|e| Hash === e ? SymHash.new(e) : e}
|
73
|
+
else
|
74
|
+
v
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
67
83
|
end
|
data/lib/angelo/responder.rb
CHANGED
@@ -25,17 +25,13 @@ module Angelo
|
|
25
25
|
@default_headers
|
26
26
|
end
|
27
27
|
|
28
|
-
def symhash
|
29
|
-
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
30
|
-
end
|
31
|
-
|
32
28
|
end
|
33
29
|
|
34
|
-
attr_accessor :connection, :request
|
30
|
+
attr_accessor :connection, :mustermann, :request
|
35
31
|
attr_writer :base
|
36
32
|
|
37
33
|
def initialize &block
|
38
|
-
@response_handler =
|
34
|
+
@response_handler = block
|
39
35
|
end
|
40
36
|
|
41
37
|
def reset!
|
@@ -48,7 +44,7 @@ module Angelo
|
|
48
44
|
def handle_request
|
49
45
|
if @response_handler
|
50
46
|
@base.filter :before
|
51
|
-
@body = catch(:halt) { @
|
47
|
+
@body = catch(:halt) { @base.instance_exec(&@response_handler) || EMPTY_STRING }
|
52
48
|
|
53
49
|
# TODO any real reason not to run afters with SSE?
|
54
50
|
case @body
|
@@ -164,8 +160,13 @@ module Angelo
|
|
164
160
|
@body = EMPTY_STRING
|
165
161
|
|
166
162
|
else
|
167
|
-
|
168
|
-
|
163
|
+
if respond_with? :json and @body.respond_to? :to_json
|
164
|
+
@body = @body.to_json
|
165
|
+
raise "uhhh? #{@body}" unless String === @body
|
166
|
+
else
|
167
|
+
unless @chunked and @body.respond_to? :each
|
168
|
+
raise RequestError.new "what is this? #{@body}"
|
169
|
+
end
|
169
170
|
end
|
170
171
|
end
|
171
172
|
|
@@ -13,29 +13,30 @@ module Angelo
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def handle_request
|
16
|
-
|
17
|
-
if @response_handler
|
18
|
-
@base.filter :before
|
19
|
-
@body = catch(:halt) { @base.eventsource &@response_handler.bind(@base) }
|
20
|
-
if HALT_STRUCT === @body
|
21
|
-
raise RequestError.new 'unknown sse error' unless @body.body == :sse
|
22
|
-
end
|
23
|
-
|
24
|
-
# TODO any real reason not to run afters with SSE?
|
25
|
-
# @base.filter :after
|
26
|
-
|
27
|
-
respond
|
28
|
-
else
|
16
|
+
if !@response_handler
|
29
17
|
raise NotImplementedError
|
18
|
+
end
|
19
|
+
@base.filter :before
|
20
|
+
@body = catch(:halt) do
|
21
|
+
@base.eventsource do |socket|
|
22
|
+
@base.instance_exec(socket, &@response_handler)
|
30
23
|
end
|
31
|
-
rescue IOError => ioe
|
32
|
-
warn "#{ioe.class} - #{ioe.message}"
|
33
|
-
rescue RequestError => re
|
34
|
-
headers SSE_HEADER
|
35
|
-
handle_error re, re.type
|
36
|
-
rescue => e
|
37
|
-
handle_error e
|
38
24
|
end
|
25
|
+
if HALT_STRUCT === @body
|
26
|
+
raise RequestError.new 'unknown sse error' unless @body.body == :sse
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO any real reason not to run afters with SSE?
|
30
|
+
# @base.filter :after
|
31
|
+
|
32
|
+
respond
|
33
|
+
rescue IOError => ioe
|
34
|
+
warn "#{ioe.class} - #{ioe.message}"
|
35
|
+
rescue RequestError => re
|
36
|
+
headers SSE_HEADER
|
37
|
+
handle_error re, re.type
|
38
|
+
rescue => e
|
39
|
+
handle_error e
|
39
40
|
end
|
40
41
|
|
41
42
|
def respond
|
@@ -22,10 +22,9 @@ module Angelo
|
|
22
22
|
begin
|
23
23
|
if @response_handler
|
24
24
|
Angelo.log @connection, @request, @websocket, :switching_protocols
|
25
|
-
@bound_response_handler ||= @response_handler.bind @base
|
26
25
|
@websocket.on_pong &Responder::Websocket.on_pong
|
27
26
|
@base.filter :before
|
28
|
-
@
|
27
|
+
@base.instance_exec(@websocket, &@response_handler)
|
29
28
|
@base.filter :after
|
30
29
|
else
|
31
30
|
raise NotImplementedError
|
data/lib/angelo/stash.rb
CHANGED
@@ -127,9 +127,11 @@ module Angelo
|
|
127
127
|
extend Stash::ClassMethods
|
128
128
|
include Stash
|
129
129
|
|
130
|
-
def event
|
131
|
-
|
132
|
-
|
130
|
+
def event *args
|
131
|
+
name, data = args
|
132
|
+
raise ArgumentError if @context == :default and data.nil?
|
133
|
+
data, name = name, @context if data.nil?
|
134
|
+
each {|s| s.write Angelo::Base.sse_event(name, data)}
|
133
135
|
nil
|
134
136
|
end
|
135
137
|
|
data/lib/angelo/tilt/erb.rb
CHANGED
@@ -13,16 +13,13 @@ module Angelo
|
|
13
13
|
# hrm, sneaky
|
14
14
|
#
|
15
15
|
def self.included base
|
16
|
-
|
17
|
-
# TODO: remove at 0.4
|
18
|
-
warn "[DEPRECATED] Angelo::Tilt::ERB will be included by default in angelo >= 0.4"
|
19
|
-
raise "Angelo requires Tilt >= 2.0, you have #{::Tilt::VERSION}" unless ::Tilt::VERSION.to_i >= 2
|
20
|
-
|
21
16
|
base.extend ClassMethods
|
22
17
|
end
|
23
18
|
|
24
19
|
module ClassMethods
|
25
20
|
|
21
|
+
@reload_templates = false
|
22
|
+
|
26
23
|
def view_glob *glob
|
27
24
|
File.join views_dir, *glob
|
28
25
|
end
|
@@ -33,6 +30,7 @@ module Angelo
|
|
33
30
|
return h if (block_given? && yield(v))
|
34
31
|
sym.gsub! File::SEPARATOR, UNDERSCORE
|
35
32
|
sym.gsub! /\.\w+?\.erb$/, EMPTY_STRING
|
33
|
+
sym.gsub! /^#{LAYOUTS_DIR}#{UNDERSCORE}/, EMPTY_STRING
|
36
34
|
h[sym.to_sym] = ::Tilt::ERBTemplate.new v
|
37
35
|
h
|
38
36
|
end
|
@@ -40,22 +38,51 @@ module Angelo
|
|
40
38
|
|
41
39
|
def templates type = DEFAULT_TYPE
|
42
40
|
@templates ||= {}
|
43
|
-
|
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|
|
44
50
|
v =~ /^#{LAYOUTS_DIR}#{File::SEPARATOR}/
|
45
51
|
end
|
46
52
|
end
|
47
53
|
|
48
54
|
def layout_templates type = DEFAULT_TYPE
|
49
|
-
|
55
|
+
if reload_templates?
|
56
|
+
@layout_templates = load_layout_templates(type)
|
57
|
+
else
|
58
|
+
@layout_templates ||= load_layout_templates(type)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_layout_templates type = DEFAULT_TYPE
|
63
|
+
templatify LAYOUTS_DIR, "*.#{type}.erb"
|
50
64
|
end
|
51
65
|
|
52
66
|
def default_layout type = DEFAULT_TYPE
|
53
67
|
@default_layout ||= {}
|
54
|
-
if
|
55
|
-
|
56
|
-
|
68
|
+
if reload_templates?
|
69
|
+
@default_layout[type] = load_default_layout(type)
|
70
|
+
else
|
71
|
+
@default_layout[type] ||= load_default_layout(type)
|
57
72
|
end
|
58
|
-
|
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
|
59
86
|
end
|
60
87
|
|
61
88
|
end
|
data/lib/angelo/version.rb
CHANGED