cannon 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +5 -0
  6. data/README +3 -0
  7. data/Rakefile +20 -0
  8. data/bin/bundler +16 -0
  9. data/bin/coderay +16 -0
  10. data/bin/htmldiff +16 -0
  11. data/bin/ldiff +16 -0
  12. data/bin/mustache +16 -0
  13. data/bin/pry +16 -0
  14. data/bin/rspec +16 -0
  15. data/cannon.gemspec +29 -0
  16. data/lib/cannon.rb +16 -0
  17. data/lib/cannon/app.rb +196 -0
  18. data/lib/cannon/concerns/path_cache.rb +48 -0
  19. data/lib/cannon/concerns/signature.rb +14 -0
  20. data/lib/cannon/config.rb +58 -0
  21. data/lib/cannon/cookie_jar.rb +99 -0
  22. data/lib/cannon/handler.rb +25 -0
  23. data/lib/cannon/middleware.rb +47 -0
  24. data/lib/cannon/middleware/content_type.rb +15 -0
  25. data/lib/cannon/middleware/cookies.rb +18 -0
  26. data/lib/cannon/middleware/files.rb +33 -0
  27. data/lib/cannon/middleware/flush_and_benchmark.rb +20 -0
  28. data/lib/cannon/middleware/request_logger.rb +13 -0
  29. data/lib/cannon/middleware/router.rb +19 -0
  30. data/lib/cannon/request.rb +36 -0
  31. data/lib/cannon/response.rb +165 -0
  32. data/lib/cannon/route.rb +80 -0
  33. data/lib/cannon/route_action.rb +92 -0
  34. data/lib/cannon/version.rb +3 -0
  35. data/lib/cannon/views.rb +28 -0
  36. data/spec/app_spec.rb +20 -0
  37. data/spec/config_spec.rb +41 -0
  38. data/spec/environments_spec.rb +68 -0
  39. data/spec/features/action_types_spec.rb +154 -0
  40. data/spec/features/cookies_spec.rb +62 -0
  41. data/spec/features/files_spec.rb +17 -0
  42. data/spec/features/method_types_spec.rb +104 -0
  43. data/spec/features/requests_spec.rb +59 -0
  44. data/spec/features/views_spec.rb +31 -0
  45. data/spec/fixtures/public/background.jpg +0 -0
  46. data/spec/fixtures/views/render_test.html +1 -0
  47. data/spec/fixtures/views/test.html +1 -0
  48. data/spec/spec_helper.rb +98 -0
  49. data/spec/support/cannon_test.rb +108 -0
  50. metadata +219 -0
@@ -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
@@ -0,0 +1,3 @@
1
+ module Cannon
2
+ VERSION = '0.0.2'
3
+ end
@@ -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
@@ -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
@@ -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