cannon 0.0.2
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 +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/README +3 -0
- data/Rakefile +20 -0
- data/bin/bundler +16 -0
- data/bin/coderay +16 -0
- data/bin/htmldiff +16 -0
- data/bin/ldiff +16 -0
- data/bin/mustache +16 -0
- data/bin/pry +16 -0
- data/bin/rspec +16 -0
- data/cannon.gemspec +29 -0
- data/lib/cannon.rb +16 -0
- data/lib/cannon/app.rb +196 -0
- data/lib/cannon/concerns/path_cache.rb +48 -0
- data/lib/cannon/concerns/signature.rb +14 -0
- data/lib/cannon/config.rb +58 -0
- data/lib/cannon/cookie_jar.rb +99 -0
- data/lib/cannon/handler.rb +25 -0
- data/lib/cannon/middleware.rb +47 -0
- data/lib/cannon/middleware/content_type.rb +15 -0
- data/lib/cannon/middleware/cookies.rb +18 -0
- data/lib/cannon/middleware/files.rb +33 -0
- data/lib/cannon/middleware/flush_and_benchmark.rb +20 -0
- data/lib/cannon/middleware/request_logger.rb +13 -0
- data/lib/cannon/middleware/router.rb +19 -0
- data/lib/cannon/request.rb +36 -0
- data/lib/cannon/response.rb +165 -0
- data/lib/cannon/route.rb +80 -0
- data/lib/cannon/route_action.rb +92 -0
- data/lib/cannon/version.rb +3 -0
- data/lib/cannon/views.rb +28 -0
- data/spec/app_spec.rb +20 -0
- data/spec/config_spec.rb +41 -0
- data/spec/environments_spec.rb +68 -0
- data/spec/features/action_types_spec.rb +154 -0
- data/spec/features/cookies_spec.rb +62 -0
- data/spec/features/files_spec.rb +17 -0
- data/spec/features/method_types_spec.rb +104 -0
- data/spec/features/requests_spec.rb +59 -0
- data/spec/features/views_spec.rb +31 -0
- data/spec/fixtures/public/background.jpg +0 -0
- data/spec/fixtures/views/render_test.html +1 -0
- data/spec/fixtures/views/test.html +1 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/cannon_test.rb +108 -0
- metadata +219 -0
data/lib/cannon/route.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Cannon
|
2
|
+
class Route
|
3
|
+
attr_reader :path, :actions, :redirect, :method
|
4
|
+
|
5
|
+
def initialize(app, method:, path:, actions: nil, redirect: nil)
|
6
|
+
@path = build_path(path)
|
7
|
+
@method, @app, @redirect = method.to_s.upcase, app, redirect
|
8
|
+
@actions = actions || []
|
9
|
+
@route_action = build_route_action(@actions.dup)
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_route_action(action)
|
13
|
+
@route_action.last_action.callback = RouteAction.new(@app, action: action, callback: nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(request)
|
17
|
+
return false unless method == 'ALL' || request.method == method
|
18
|
+
|
19
|
+
matches = self.path.match(request.path)
|
20
|
+
if matches.nil?
|
21
|
+
false
|
22
|
+
else
|
23
|
+
@params.each_with_index { |key, index| request.params[key.to_sym] = matches.captures[index] }
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def handle(request, response, finish_proc)
|
29
|
+
if redirect
|
30
|
+
response.permanent_redirect(redirect)
|
31
|
+
elsif @route_action
|
32
|
+
begin
|
33
|
+
@route_action.run(request, response, finish_proc)
|
34
|
+
rescue => error
|
35
|
+
Cannon.logger.error error.message
|
36
|
+
Cannon.logger.error error.backtrace.join("\n")
|
37
|
+
response.internal_server_error(title: error.message, content: error.backtrace.join('<br/>'))
|
38
|
+
finish_proc.call
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
"Route: #{path}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def build_path(path)
|
50
|
+
path = '/' + path unless path =~ /^\// # ensure path begins with '/'
|
51
|
+
@params = []
|
52
|
+
|
53
|
+
if path.include? ':'
|
54
|
+
param_path_to_regexp(path)
|
55
|
+
else
|
56
|
+
/^#{path.gsub('/', '\/')}$/
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def param_path_to_regexp(path)
|
61
|
+
/^#{path.split('/').map { |part| normalize_path_part(part) }.join('\/')}$/
|
62
|
+
end
|
63
|
+
|
64
|
+
def normalize_path_part(part)
|
65
|
+
if part =~ /^:(.+)/
|
66
|
+
@params << $1
|
67
|
+
'([^\/]+)'
|
68
|
+
else
|
69
|
+
part
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_route_action(actions, callback: nil)
|
74
|
+
return callback if actions.size < 1
|
75
|
+
|
76
|
+
route_action = RouteAction.new(@app, action: actions.pop, callback: callback)
|
77
|
+
build_route_action(actions, callback: route_action)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class RouteAction
|
2
|
+
include EventMachine::Deferrable
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def controller(name, app)
|
6
|
+
controllers[name] ||= Object.const_get(name).new(app)
|
7
|
+
end
|
8
|
+
|
9
|
+
def controllers
|
10
|
+
@controllers ||= {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_writer :callback
|
15
|
+
|
16
|
+
def initialize(app, action:, callback:)
|
17
|
+
@app, @action, @callback = app, action, callback
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_action
|
21
|
+
@callback.nil? ? self : @callback.last_action
|
22
|
+
end
|
23
|
+
|
24
|
+
def run(request, response, finish_proc)
|
25
|
+
next_proc = -> do
|
26
|
+
if response.flushed?
|
27
|
+
fail
|
28
|
+
finish_proc.call
|
29
|
+
else
|
30
|
+
setup_callback
|
31
|
+
succeed(request, response, finish_proc)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
if @action.is_a? Proc
|
36
|
+
run_inline_action(request, response, next_proc)
|
37
|
+
elsif @action.include? '#'
|
38
|
+
run_controller_action(request, response, next_proc)
|
39
|
+
else
|
40
|
+
run_bound_action(request, response, next_proc)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def run_inline_action(request, response, next_proc)
|
47
|
+
Cannon.logger.debug 'Action: Inline'
|
48
|
+
|
49
|
+
if @action.arity == 2
|
50
|
+
@action.call(request, response)
|
51
|
+
next_proc.call
|
52
|
+
else
|
53
|
+
@action.call(request, response, next_proc)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def run_controller_action(request, response, next_proc)
|
58
|
+
controller, action = @action.split('#')
|
59
|
+
|
60
|
+
Cannon.logger.debug "Controller: #{controller}, Action: #{action}"
|
61
|
+
|
62
|
+
controller_instance = RouteAction.controller(controller, @app)
|
63
|
+
if controller_instance.method(action).arity == 2
|
64
|
+
controller_instance.send(action, request, response)
|
65
|
+
next_proc.call
|
66
|
+
else
|
67
|
+
controller_instance.send(action, request, response, next_proc)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_bound_action(request, response, next_proc)
|
72
|
+
Cannon.logger.debug "Action: #{@action}"
|
73
|
+
|
74
|
+
if @app.app_binding.method(@action).arity == 2
|
75
|
+
@app.app_binding.send(@action, request, response)
|
76
|
+
next_proc.call
|
77
|
+
else
|
78
|
+
@app.app_binding.send(@action, request, response, next_proc)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup_callback
|
83
|
+
set_deferred_status nil
|
84
|
+
callback do |request, response, finish_proc|
|
85
|
+
if @callback.nil?
|
86
|
+
finish_proc.call
|
87
|
+
else
|
88
|
+
@callback.run(request, response, finish_proc)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/cannon/views.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'mustache'
|
2
|
+
|
3
|
+
module Cannon
|
4
|
+
module Views
|
5
|
+
include PathCache
|
6
|
+
|
7
|
+
def view(filename, options = {})
|
8
|
+
reload_cache if outdated_cache?
|
9
|
+
|
10
|
+
file, content_type = *file_and_content_type("#{base_path}/#{filename}")
|
11
|
+
header('Content-Type', content_type)
|
12
|
+
send(Mustache.render(file, options), status: status)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def initialize_views
|
18
|
+
self.cache = :views
|
19
|
+
self.base_path = build_view_path
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def build_view_path
|
25
|
+
@app.config.view_path =~ /^\// ? @app.config.view_path : "#{Cannon.root}/#{@app.config.view_path}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/app_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Cannon::App, :cannon_app do
|
4
|
+
context 'app is listening asynchronously' do
|
5
|
+
before(:each) { cannon_app.listen(port: 3030, async: true) }
|
6
|
+
after(:each) { cannon_app.stop }
|
7
|
+
|
8
|
+
it 'raises an error if trying to listen again before stopping' do
|
9
|
+
expect(-> { cannon_app.listen }).to raise_error(Cannon::AlreadyListening)
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'app has been stopped' do
|
13
|
+
before(:each) { cannon_app.stop }
|
14
|
+
|
15
|
+
it 'does not raise an error' do
|
16
|
+
expect(-> { cannon_app.listen(async: true) }).to_not raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Configuration' do
|
4
|
+
describe 'view_path' do
|
5
|
+
it 'can be relative', :cannon_app do
|
6
|
+
cannon_app.config.view_path = '../fixtures/views'
|
7
|
+
cannon_app.get('/') do |request, response|
|
8
|
+
response.view('test.html')
|
9
|
+
end
|
10
|
+
cannon_app.listen(async: true)
|
11
|
+
get '/'
|
12
|
+
expect(response.body).to eq('Test view content')
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can be absolute', :cannon_app do
|
16
|
+
cannon_app.config.view_path = Cannon.root + '/../fixtures/views'
|
17
|
+
cannon_app.get('/') do |request, response|
|
18
|
+
response.view('test.html')
|
19
|
+
end
|
20
|
+
cannon_app.listen(async: true)
|
21
|
+
get '/'
|
22
|
+
expect(response.body).to eq('Test view content')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'public_path' do
|
27
|
+
it 'can be relative', :cannon_app do
|
28
|
+
cannon_app.config.public_path = '../fixtures/public'
|
29
|
+
cannon_app.listen(async: true)
|
30
|
+
get '/background.jpg'
|
31
|
+
expect(response.code).to be(200)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'can be absolute', :cannon_app do
|
35
|
+
cannon_app.config.public_path = Cannon.root + '/../fixtures/public'
|
36
|
+
cannon_app.listen(async: true)
|
37
|
+
get '/background.jpg'
|
38
|
+
expect(response.code).to be(200)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Cannon environment', :cannon_app do
|
4
|
+
context 'no environment specified' do
|
5
|
+
before(:each) { ENV['CANNON_ENV'] = nil }
|
6
|
+
|
7
|
+
it 'sets the environment to development' do
|
8
|
+
expect(cannon_app.env).to eq('development')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'environment specified' do
|
13
|
+
before(:each) { ENV['CANNON_ENV'] = 'strange' }
|
14
|
+
|
15
|
+
it 'sets the environment to the environment specified' do
|
16
|
+
expect(cannon_app.env).to eq('strange')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'environment helper methods' do
|
21
|
+
before(:each) do
|
22
|
+
ENV['CANNON_ENV'] = 'carrots'
|
23
|
+
|
24
|
+
cannon_app.configure(:potatoes) {}
|
25
|
+
Cannon.configure(:celeries) {}
|
26
|
+
cannon_app.configure(:onions, :lettuce, :pickles)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'creates a helper method for current environment' do
|
30
|
+
expect(cannon_app.env.carrots?).to be(true)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'creates helper methods for single configured environments' do
|
34
|
+
expect(cannon_app.env.potatoes?).to be(false)
|
35
|
+
expect(cannon_app.env.celeries?).to be(false)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'creates helper methods for multi configured environments' do
|
39
|
+
expect(cannon_app.env.onions?).to be(false)
|
40
|
+
expect(cannon_app.env.lettuce?).to be(false)
|
41
|
+
expect(cannon_app.env.pickles?).to be(false)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'environment specific config blocks' do
|
46
|
+
before(:each) do
|
47
|
+
ENV['CANNON_ENV'] = 'carrots'
|
48
|
+
|
49
|
+
cannon_app.configure(:potatoes) do |config|
|
50
|
+
config.view_path = 'potatoes_view_path'
|
51
|
+
end
|
52
|
+
cannon_app.configure(:carrots) do |config|
|
53
|
+
config.view_path = 'carrots_view_path'
|
54
|
+
end
|
55
|
+
Cannon.configure(:pickles) do |config|
|
56
|
+
config.public_path = 'pickles_view_path'
|
57
|
+
end
|
58
|
+
cannon_app.configure(:onions, :carrots) do |config|
|
59
|
+
config.public_path = 'shared_view_path'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'runs the configuration for the given environment' do
|
64
|
+
expect(Cannon.config.view_path).to eq('carrots_view_path')
|
65
|
+
expect(Cannon.config.public_path).to eq('shared_view_path')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
def hi(request, response)
|
4
|
+
response.send('hi')
|
5
|
+
end
|
6
|
+
|
7
|
+
def how(request, response)
|
8
|
+
response.send(' how ')
|
9
|
+
end
|
10
|
+
|
11
|
+
def are_you(request, response)
|
12
|
+
response.send('are you?')
|
13
|
+
end
|
14
|
+
|
15
|
+
def first(request, response, next_proc)
|
16
|
+
EM.defer(
|
17
|
+
-> { sleep 0.1 },
|
18
|
+
->(result) do
|
19
|
+
response.send('first')
|
20
|
+
next_proc.call
|
21
|
+
end
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def second(request, response)
|
26
|
+
response.send(' second')
|
27
|
+
end
|
28
|
+
|
29
|
+
class World
|
30
|
+
def initialize(app)
|
31
|
+
@count = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def hello(request, response)
|
35
|
+
response.send('Hello World!')
|
36
|
+
end
|
37
|
+
|
38
|
+
def count(request, response)
|
39
|
+
response.send("count = #{@count}")
|
40
|
+
@count += 1
|
41
|
+
end
|
42
|
+
|
43
|
+
def first(request, response, next_proc)
|
44
|
+
EM.defer(
|
45
|
+
-> { sleep 0.1 },
|
46
|
+
->(result) do
|
47
|
+
response.send('first')
|
48
|
+
next_proc.call
|
49
|
+
end
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def second(request, response)
|
54
|
+
response.send(' second')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
RSpec.describe 'Cannon app', :cannon_app do
|
59
|
+
before(:all) do
|
60
|
+
cannon_app.get('/1-2-simple', actions: ['first', 'second'])
|
61
|
+
cannon_app.get('/hi', action: 'hi')
|
62
|
+
cannon_app.get('/how', actions: ['hi', 'how', 'are_you'])
|
63
|
+
|
64
|
+
cannon_app.get('/hello', action: 'World#hello')
|
65
|
+
cannon_app.get('/count', action: 'World#count')
|
66
|
+
cannon_app.get('/1-2-controller', actions: ['World#first', 'World#second'])
|
67
|
+
|
68
|
+
cannon_app.get('/inline') do |request, response|
|
69
|
+
response.send('inline action')
|
70
|
+
end
|
71
|
+
|
72
|
+
cannon_app.get('/inline_chained') do |request, response|
|
73
|
+
response.send('first')
|
74
|
+
end.handle do |request, response|
|
75
|
+
response.send(' second')
|
76
|
+
end.handle do |request, response|
|
77
|
+
response.send(' third')
|
78
|
+
end
|
79
|
+
|
80
|
+
cannon_app.get('/1-2-inline') do |request, response, next_proc|
|
81
|
+
EM.defer(
|
82
|
+
-> { sleep 0.1 },
|
83
|
+
->(result) do
|
84
|
+
response.send('first')
|
85
|
+
next_proc.call
|
86
|
+
end
|
87
|
+
)
|
88
|
+
end.handle do |request, response|
|
89
|
+
response.send(' second')
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
cannon_app.listen(async: true)
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'simple method' do
|
97
|
+
it 'handles single actions' do
|
98
|
+
get '/hi'
|
99
|
+
expect(response.code).to be(200)
|
100
|
+
expect(response.body).to eq('hi')
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'handles an actions chain' do
|
104
|
+
get '/how'
|
105
|
+
expect(response.code).to be(200)
|
106
|
+
expect(response.body).to eq('hi how are you?')
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'handles next_proc calls for deferred processing' do
|
110
|
+
get '/1-2-simple'
|
111
|
+
expect(response.body).to eq('first second')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe 'inline' do
|
116
|
+
it 'handles single actions' do
|
117
|
+
get '/inline'
|
118
|
+
expect(response.code).to be(200)
|
119
|
+
expect(response.body).to eq('inline action')
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'handles an actions chain' do
|
123
|
+
get '/inline_chained'
|
124
|
+
expect(response.body).to eq('first second third')
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'handles next_proc calls for deferred processing' do
|
128
|
+
get '/1-2-inline'
|
129
|
+
expect(response.body).to eq('first second')
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe 'controller based' do
|
134
|
+
it 'handles simple actions' do
|
135
|
+
get '/hello'
|
136
|
+
expect(response.body).to eq('Hello World!')
|
137
|
+
expect(response.code).to eq(200)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'handles next_proc calls for deferred processing' do
|
141
|
+
get '/1-2-controller'
|
142
|
+
expect(response.body).to eq('first second')
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'keeps the instantiation of the controller' do
|
146
|
+
get '/count'
|
147
|
+
expect(response.body).to eq('count = 0')
|
148
|
+
get '/count'
|
149
|
+
expect(response.body).to eq('count = 1')
|
150
|
+
get '/count'
|
151
|
+
expect(response.body).to eq('count = 2')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|