angelo 0.4.1 → 0.5.0
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 -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
@@ -134,9 +134,12 @@ module Angelo
|
|
134
134
|
class Reactor
|
135
135
|
include Celluloid::IO
|
136
136
|
extend Cellper
|
137
|
+
def wait_for_stop
|
138
|
+
every(0.01){ terminate if Reactor.stop? }
|
139
|
+
end
|
137
140
|
end
|
138
141
|
|
139
|
-
class
|
142
|
+
class Actor
|
140
143
|
include Celluloid
|
141
144
|
extend Cellper
|
142
145
|
end
|
@@ -144,7 +147,7 @@ module Angelo
|
|
144
147
|
end
|
145
148
|
|
146
149
|
class WebsocketHelper
|
147
|
-
include Celluloid::Logger
|
150
|
+
include Celluloid::Internals::Logger
|
148
151
|
|
149
152
|
extend Forwardable
|
150
153
|
def_delegator :@socket, :write
|
@@ -192,8 +195,11 @@ module Angelo
|
|
192
195
|
|
193
196
|
def go
|
194
197
|
@driver.start
|
195
|
-
|
196
|
-
@
|
198
|
+
begin
|
199
|
+
while msg = @socket.readpartial(4096)
|
200
|
+
@driver.parse msg
|
201
|
+
end
|
202
|
+
rescue EOFError, SystemCallError => e
|
197
203
|
end
|
198
204
|
end
|
199
205
|
|
data/lib/angelo/params_parser.rb
CHANGED
@@ -21,19 +21,28 @@ module Angelo
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def parse_post_body
|
24
|
-
body = request.body.to_s
|
24
|
+
body = request_body rescue request.body.to_s
|
25
25
|
case
|
26
26
|
when form_encoded?
|
27
27
|
parse_formencoded body
|
28
28
|
when json? && !body.empty?
|
29
|
-
|
29
|
+
parsed_body = JSON.parse body
|
30
|
+
parsed_body = SymHash.new parsed_body if Hash === parsed_body
|
31
|
+
parsed_body
|
30
32
|
else
|
31
33
|
{}
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
37
|
def parse_query_string_and_post_body
|
36
|
-
|
38
|
+
parsed_body = parse_post_body
|
39
|
+
case parsed_body
|
40
|
+
when Hash
|
41
|
+
parse_query_string.merge! parse_post_body
|
42
|
+
when Array
|
43
|
+
self.request_body = parsed_body
|
44
|
+
parse_query_string
|
45
|
+
end
|
37
46
|
end
|
38
47
|
|
39
48
|
def form_encoded?
|
data/lib/angelo/responder.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Angelo
|
2
2
|
|
3
3
|
class Responder
|
4
|
-
include Celluloid::Logger
|
4
|
+
include Celluloid::Internals::Logger
|
5
5
|
|
6
6
|
class << self
|
7
7
|
|
@@ -30,8 +30,8 @@ module Angelo
|
|
30
30
|
attr_accessor :connection, :mustermann, :request
|
31
31
|
attr_writer :base
|
32
32
|
|
33
|
-
def initialize &block
|
34
|
-
@response_handler = block
|
33
|
+
def initialize method, &block
|
34
|
+
@method, @response_handler = method, block
|
35
35
|
end
|
36
36
|
|
37
37
|
def reset!
|
@@ -70,7 +70,7 @@ module Angelo
|
|
70
70
|
|
71
71
|
def handle_error _error, type = :internal_server_error, report = @base.report_errors?
|
72
72
|
err_msg = error_message _error
|
73
|
-
Angelo.log @connection, @request, nil, type, err_msg.size
|
73
|
+
Angelo.log @method, @connection, @request, nil, type, err_msg.size
|
74
74
|
@connection.respond type, headers, err_msg
|
75
75
|
@connection.close
|
76
76
|
if report
|
@@ -170,11 +170,11 @@ module Angelo
|
|
170
170
|
end
|
171
171
|
end
|
172
172
|
|
173
|
-
status ||= @redirect.nil? ? :ok :
|
174
|
-
headers LOCATION_HEADER_KEY => @redirect if @redirect
|
173
|
+
status ||= @redirect.nil? ? :ok : @redirect[1]
|
174
|
+
headers LOCATION_HEADER_KEY => @redirect[0] if @redirect
|
175
175
|
|
176
176
|
if @chunked
|
177
|
-
Angelo.log @connection, @request, nil, status
|
177
|
+
Angelo.log @method, @connection, @request, nil, status
|
178
178
|
@request.respond status, headers
|
179
179
|
err = nil
|
180
180
|
begin
|
@@ -190,7 +190,7 @@ module Angelo
|
|
190
190
|
end
|
191
191
|
else
|
192
192
|
size = @body.nil? ? 0 : @body.size
|
193
|
-
Angelo.log @connection, @request, nil, status, size
|
193
|
+
Angelo.log @method, @connection, @request, nil, status, size
|
194
194
|
@request.respond status, headers, @body
|
195
195
|
end
|
196
196
|
|
@@ -198,11 +198,15 @@ module Angelo
|
|
198
198
|
handle_error e, :internal_server_error
|
199
199
|
end
|
200
200
|
|
201
|
-
def redirect url
|
202
|
-
@redirect = url
|
201
|
+
def redirect url, permanent = false
|
202
|
+
@redirect = [url, permanent ? :moved_permanently : :found]
|
203
203
|
nil
|
204
204
|
end
|
205
205
|
|
206
|
+
def redirect! url
|
207
|
+
redirect url, true
|
208
|
+
end
|
209
|
+
|
206
210
|
def on_close= on_close
|
207
211
|
raise ArgumentError.new unless Proc === on_close
|
208
212
|
@on_close = on_close
|
@@ -4,7 +4,7 @@ module Angelo
|
|
4
4
|
|
5
5
|
def initialize _headers = nil, &block
|
6
6
|
headers _headers if _headers
|
7
|
-
super &block
|
7
|
+
super :get, &block
|
8
8
|
end
|
9
9
|
|
10
10
|
def request= request
|
@@ -40,7 +40,7 @@ module Angelo
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def respond
|
43
|
-
Angelo.log @connection, @request, nil, :ok
|
43
|
+
Angelo.log :sse, @connection, @request, nil, :ok
|
44
44
|
@request.respond 200, headers, nil
|
45
45
|
end
|
46
46
|
|
@@ -2,16 +2,6 @@ module Angelo
|
|
2
2
|
class Responder
|
3
3
|
class Websocket < Responder
|
4
4
|
|
5
|
-
class << self
|
6
|
-
|
7
|
-
attr_writer :on_pong
|
8
|
-
|
9
|
-
def on_pong
|
10
|
-
@on_pong ||= ->(e){}
|
11
|
-
end
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
5
|
def request= request
|
16
6
|
@params = nil
|
17
7
|
@request = request
|
@@ -21,8 +11,7 @@ module Angelo
|
|
21
11
|
def handle_request
|
22
12
|
begin
|
23
13
|
if @response_handler
|
24
|
-
Angelo.log @connection, @request, @websocket, :switching_protocols
|
25
|
-
@websocket.on_pong &Responder::Websocket.on_pong
|
14
|
+
Angelo.log :ws, @connection, @request, @websocket, :switching_protocols
|
26
15
|
@base.filter :before
|
27
16
|
@base.instance_exec(@websocket, &@response_handler)
|
28
17
|
@base.filter :after
|
@@ -45,6 +34,8 @@ module Angelo
|
|
45
34
|
|
46
35
|
def close_websocket
|
47
36
|
@websocket.close
|
37
|
+
rescue IOError
|
38
|
+
ensure
|
48
39
|
@base.websockets.remove_socket @websocket
|
49
40
|
end
|
50
41
|
|
data/lib/angelo/server.rb
CHANGED
@@ -5,7 +5,7 @@ module Angelo
|
|
5
5
|
|
6
6
|
class Server < Reel::Server::HTTP
|
7
7
|
extend Forwardable
|
8
|
-
include Celluloid::Logger
|
8
|
+
include Celluloid::Internals::Logger
|
9
9
|
|
10
10
|
def_delegators :@base, :websockets, :sses
|
11
11
|
|
@@ -51,11 +51,22 @@ module Angelo
|
|
51
51
|
route! meth, connection, request
|
52
52
|
end
|
53
53
|
rescue URI::InvalidURIError => e
|
54
|
-
Angelo.log connection, request, nil, :bad_request
|
54
|
+
Angelo.log meth, connection, request, nil, :bad_request
|
55
55
|
connection.respond :bad_request, DEFAULT_RESPONSE_HEADERS, e.message
|
56
56
|
end
|
57
57
|
|
58
|
+
def post_override! meth, request
|
59
|
+
if meth == :post and request.headers.has_key? POST_OVERRIDE_REQUEST_HEADER_KEY
|
60
|
+
new_meth = request.headers[POST_OVERRIDE_REQUEST_HEADER_KEY].downcase.to_sym
|
61
|
+
meth = new_meth if POST_OVERRIDABLE.include? new_meth
|
62
|
+
end
|
63
|
+
meth
|
64
|
+
rescue
|
65
|
+
meth
|
66
|
+
end
|
67
|
+
|
58
68
|
def route! meth, connection, request
|
69
|
+
meth = post_override! meth, request
|
59
70
|
if rs = @base.routes[meth][request.path]
|
60
71
|
responder = rs.dup
|
61
72
|
responder.reset!
|
@@ -65,7 +76,7 @@ module Angelo
|
|
65
76
|
responder.handle_request
|
66
77
|
responder
|
67
78
|
else
|
68
|
-
Angelo.log connection, request, nil, :not_found
|
79
|
+
Angelo.log meth, connection, request, nil, :not_found
|
69
80
|
connection.respond :not_found, DEFAULT_RESPONSE_HEADERS, NOT_FOUND
|
70
81
|
end
|
71
82
|
end
|
@@ -77,7 +88,7 @@ module Angelo
|
|
77
88
|
def static! meth, connection, request, local_path
|
78
89
|
etag = etag_for local_path
|
79
90
|
if request.headers[IF_NONE_MATCH_HEADER_KEY] == etag
|
80
|
-
Angelo.log connection, request, nil, :not_modified, 0
|
91
|
+
Angelo.log meth, connection, request, nil, :not_modified, 0
|
81
92
|
connection.respond :not_modified
|
82
93
|
else
|
83
94
|
headers = {
|
@@ -96,7 +107,7 @@ module Angelo
|
|
96
107
|
ETAG_HEADER_KEY => etag
|
97
108
|
|
98
109
|
}
|
99
|
-
Angelo.log connection, request, nil, :ok, headers[CONTENT_LENGTH_HEADER_KEY]
|
110
|
+
Angelo.log meth, connection, request, nil, :ok, headers[CONTENT_LENGTH_HEADER_KEY]
|
100
111
|
connection.respond :ok, headers, (meth == :head ? nil : File.read(local_path))
|
101
112
|
end
|
102
113
|
end
|
data/lib/angelo/stash.rb
CHANGED
@@ -3,7 +3,7 @@ module Angelo
|
|
3
3
|
# utility class for stashing connected websockets in arbitrary contexts
|
4
4
|
#
|
5
5
|
module Stash
|
6
|
-
include Celluloid::Logger
|
6
|
+
include Celluloid::Internals::Logger
|
7
7
|
|
8
8
|
module ClassMethods
|
9
9
|
|
@@ -29,6 +29,7 @@ module Angelo
|
|
29
29
|
def initialize server, context = :default
|
30
30
|
raise ArgumentError.new "symbol required" unless Symbol === context
|
31
31
|
@context, @server = context, server
|
32
|
+
@mutex = Mutex.new
|
32
33
|
stashes[@context] ||= []
|
33
34
|
end
|
34
35
|
|
@@ -51,7 +52,8 @@ module Angelo
|
|
51
52
|
# as needed
|
52
53
|
#
|
53
54
|
def each &block
|
54
|
-
stash.dup
|
55
|
+
stash_dup = @mutex.synchronize { stash.dup }
|
56
|
+
stash_dup.each do |s|
|
55
57
|
begin
|
56
58
|
yield s
|
57
59
|
rescue Reel::SocketError, IOError, SystemCallError => e
|
@@ -69,6 +71,7 @@ module Angelo
|
|
69
71
|
if stash.include? s
|
70
72
|
warn "removing socket from context ':#{@context}' (#{peeraddrs[s][2]})"
|
71
73
|
stash.delete s
|
74
|
+
|
72
75
|
peeraddrs.delete s
|
73
76
|
end
|
74
77
|
end
|
@@ -83,12 +86,14 @@ module Angelo
|
|
83
86
|
# ping_websockets task
|
84
87
|
#
|
85
88
|
def all_each
|
86
|
-
stashes.values.flatten
|
89
|
+
all_stashed = @mutex.synchronize { stashes.values.flatten }
|
90
|
+
all_stashed.each do |s|
|
87
91
|
begin
|
88
92
|
yield s
|
89
93
|
rescue Reel::SocketError, IOError, SystemCallError => e
|
90
94
|
debug "all - #{e.message}"
|
91
95
|
remove_socket s
|
96
|
+
stashes.values.each {|_s| _s.delete s}
|
92
97
|
end
|
93
98
|
end
|
94
99
|
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'tilt'
|
2
|
+
|
3
|
+
module Angelo
|
4
|
+
module Templates
|
5
|
+
|
6
|
+
# When this code refers to Tilt, use ::Tilt not Angelo::Tilt.
|
7
|
+
|
8
|
+
Tilt = ::Tilt
|
9
|
+
|
10
|
+
# Add instance methods to render views with each template engine
|
11
|
+
# I'm using. The methods are called "haml", "markdown", etc.
|
12
|
+
|
13
|
+
# They render files (Symbols) or Strings, with no frills other
|
14
|
+
# than locals. Files must be in the self.class.views_dir
|
15
|
+
# directory, with an extension that Tilt maps to the
|
16
|
+
# template_type.
|
17
|
+
|
18
|
+
# Angelo::Tilt::ERB includes an erb method built on top of _erb
|
19
|
+
# (below) which provides slightly differnet semantics. For
|
20
|
+
# compatibilty, we don't overwrite that method here and instead
|
21
|
+
# define _erb. At least for now.
|
22
|
+
|
23
|
+
[:haml, :markdown].each do |template_type|
|
24
|
+
define_method(template_type) do |view, opts = {}|
|
25
|
+
render(template_type, view, opts)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def _erb(view, opts = {})
|
30
|
+
render(:erb, view, opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def render(template_type, view, opts = {})
|
36
|
+
# Extract the options that belong to us. Any remaining options
|
37
|
+
# will be passed to the engine when the template is instantiated.
|
38
|
+
|
39
|
+
locals = opts.delete(:locals) || {}
|
40
|
+
layout_engine = opts.delete(:layout_engine) || template_type
|
41
|
+
layout =
|
42
|
+
if opts.has_key?(:layout)
|
43
|
+
layout = opts.delete(:layout)
|
44
|
+
case layout
|
45
|
+
when true
|
46
|
+
:layout
|
47
|
+
when nil
|
48
|
+
false
|
49
|
+
else
|
50
|
+
layout
|
51
|
+
end
|
52
|
+
else
|
53
|
+
# Use the default layout.
|
54
|
+
:layout
|
55
|
+
end
|
56
|
+
|
57
|
+
template = self.class.get_template(template_type, view, self.class.views_dir, opts)
|
58
|
+
if !template
|
59
|
+
raise ArgumentError, "Can't find template `#{view}' of type `#{template_type}'"
|
60
|
+
end
|
61
|
+
|
62
|
+
render = ->{ template.render(self, locals) }
|
63
|
+
if layout
|
64
|
+
layout_template = self.class.get_template(layout_engine, layout,
|
65
|
+
File.join(self.class.views_dir), opts)
|
66
|
+
end
|
67
|
+
if layout_template
|
68
|
+
layout_template.render(self, &render)
|
69
|
+
else
|
70
|
+
render.call
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.included(klass)
|
75
|
+
klass.extend TemplateCaching
|
76
|
+
end
|
77
|
+
|
78
|
+
module TemplateCaching
|
79
|
+
# A Cache which, unlike Tilt::Cache, will cache nil so when a
|
80
|
+
# template isn't found we'll remmeber that and won't try/fail to
|
81
|
+
# load it again. This happens most commonly when there is no
|
82
|
+
# default layout file.
|
83
|
+
|
84
|
+
# A pull request has been accepted for Tilt which makes
|
85
|
+
# Tilt::Cache work like our Cache so we can remove this when a
|
86
|
+
# new Tilt gem is released (currently 2.0.1), but Tilt doesn't
|
87
|
+
# seem to be getting much love these days.
|
88
|
+
|
89
|
+
class Cache
|
90
|
+
def initialize
|
91
|
+
@cache = {}
|
92
|
+
end
|
93
|
+
|
94
|
+
def fetch(*key)
|
95
|
+
@cache.fetch(key) do
|
96
|
+
@cache[key] = yield
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Create a template cache that all subclasses of Angelo::Base
|
102
|
+
# will share. And a non_cache object that will stand in for the cache
|
103
|
+
# when reloading templates, which is set per-class.
|
104
|
+
|
105
|
+
@@template_cache = Cache.new
|
106
|
+
@@non_cache = Object.new.tap do |o|
|
107
|
+
def o.fetch(*args)
|
108
|
+
yield
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_template(*args)
|
113
|
+
template_cache.fetch(*args) do
|
114
|
+
instantiate_template(*args)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def template_cache
|
121
|
+
@reload_templates ? @@non_cache : @@template_cache
|
122
|
+
end
|
123
|
+
|
124
|
+
def instantiate_template(template_type, view, views_dir, opts)
|
125
|
+
case view
|
126
|
+
when Symbol
|
127
|
+
instantiate_template_from_file(template_type, view.to_s, views_dir, opts)
|
128
|
+
when String
|
129
|
+
instantiate_template_from_string(template_type, view, opts)
|
130
|
+
else
|
131
|
+
raise ArgumentError, "view must be a Symbol or a String"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def instantiate_template_from_file(template_type, view, views_dir, opts)
|
136
|
+
# Find a file in the views directory with a correct extension
|
137
|
+
# for this template_type.
|
138
|
+
file = find_view_file_for_template_type(views_dir, view, template_type)
|
139
|
+
if file
|
140
|
+
# Use absolute filenames so backtraces have absolute filenames.
|
141
|
+
absolute_file = File.expand_path(file)
|
142
|
+
Tilt.new(absolute_file, opts)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def instantiate_template_from_string(template_type, string, opts)
|
147
|
+
# To make Tilt use a String as a template we pass Tilt.new a
|
148
|
+
# block that returns the String. Passing a block makes
|
149
|
+
# Tilt.new ignore the file argument. Except the file argument
|
150
|
+
# is still used to figure out which template engine to use.
|
151
|
+
# Fortunately we can just pass template_type, except the file
|
152
|
+
# argument needs to respond to to_str and symbols don't
|
153
|
+
# respond to that so we convert it to a String.
|
154
|
+
Tilt.new(template_type.to_s, opts) {string}
|
155
|
+
end
|
156
|
+
|
157
|
+
def find_view_file_for_template_type(views, name, template_type)
|
158
|
+
template_class = Tilt[template_type]
|
159
|
+
exts = Tilt.default_mapping.extensions_for(template_class).uniq
|
160
|
+
filenames = exts.map{|ext| File.join(views, "#{name}.#{ext}")}
|
161
|
+
filenames.find{|f| File.exists?(f)}
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|