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
@@ -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
|