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