kenji 0.5 → 0.6.5
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.
- data/README.md +27 -8
- data/lib/kenji/controller.rb +26 -1
- data/lib/kenji/version.rb +1 -1
- data/lib/kenji.rb +81 -34
- data/spec/1/controllers/main.rb +9 -0
- data/spec/3/controllers/before.rb +11 -0
- data/spec/kenji_spec.rb +93 -35
- metadata +3 -1
data/README.md
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
*Project is still actively in development.*
|
2
|
-
|
3
|
-
|
4
1
|
# Kenji
|
5
2
|
|
6
3
|
Kenji is a lightweight backend framework for Ruby.
|
@@ -26,17 +23,17 @@ Kenji wants you to organize your code into logical units of code, aka. controlle
|
|
26
23
|
|
27
24
|
The canonical Hello World example for the URL `/hello/world` in Kenji would look like this, in `controller/hello.rb`:
|
28
25
|
|
29
|
-
|
26
|
+
```ruby
|
30
27
|
class HelloController < Kenji::Controller
|
31
28
|
get '/world' do
|
32
29
|
{hello: :world}
|
33
30
|
end
|
34
31
|
end
|
35
|
-
|
32
|
+
```
|
36
33
|
|
37
34
|
A more representative example might be:
|
38
35
|
|
39
|
-
|
36
|
+
```ruby
|
40
37
|
class UserController < Kenji::Controller
|
41
38
|
|
42
39
|
# ...
|
@@ -53,7 +50,7 @@ class UserController < Kenji::Controller
|
|
53
50
|
# delete connection from user id to friend_id
|
54
51
|
end
|
55
52
|
end
|
56
|
-
|
53
|
+
```
|
57
54
|
|
58
55
|
|
59
56
|
### Data Transport
|
@@ -80,5 +77,27 @@ And already, your app is ready to go:
|
|
80
77
|
|
81
78
|
## Requirements & Assumptions
|
82
79
|
|
83
|
-
- Requires
|
80
|
+
- Requires RubyGems and Bundler.
|
81
|
+
- Requires Rack
|
84
82
|
- Requires Ruby 1.9.
|
83
|
+
|
84
|
+
|
85
|
+
## Changelog
|
86
|
+
|
87
|
+
#### 0.6.5
|
88
|
+
|
89
|
+
- Automatically handle CORS / Access-Control.
|
90
|
+
- Use throw / catch instead of raise / rescue for control flow.
|
91
|
+
|
92
|
+
#### Before TODO: figure out when
|
93
|
+
|
94
|
+
- `before` command.
|
95
|
+
- specs
|
96
|
+
- passing
|
97
|
+
|
98
|
+
## Still to do
|
99
|
+
|
100
|
+
- The auto-generated project template should be updated.
|
101
|
+
- The controller naming convention should not contain a 'Controller' suffix.
|
102
|
+
- Route multiple URLs for the same route?
|
103
|
+
|
data/lib/kenji/controller.rb
CHANGED
@@ -86,11 +86,30 @@ module Kenji
|
|
86
86
|
node[:@controller] = controller
|
87
87
|
end
|
88
88
|
|
89
|
+
|
90
|
+
# This lets you define before blocks.
|
91
|
+
#
|
92
|
+
# class MyController < Kenji::Controller
|
93
|
+
# before do
|
94
|
+
# # eg. ensure authentication, you can use kenji.respond in here.
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
def self.before(&block)
|
99
|
+
define_method(:_tmp_before_action, &block)
|
100
|
+
block = instance_method(:_tmp_before_action)
|
101
|
+
remove_method(:_tmp_before_action)
|
102
|
+
(@befores ||= []) << block
|
103
|
+
end
|
104
|
+
|
89
105
|
|
90
106
|
# Most likely only used by Kenji itself.
|
91
107
|
# Override to implement your own routing, if you'd like.
|
92
108
|
#
|
93
109
|
def call(method, path)
|
110
|
+
|
111
|
+
self.class.befores.each {|b| b.bind(self).call }
|
112
|
+
|
94
113
|
segments = path.split('/')
|
95
114
|
segments = segments.drop(1) if segments.first == '' # discard leading /'s empty segment
|
96
115
|
|
@@ -104,11 +123,12 @@ module Kenji
|
|
104
123
|
end
|
105
124
|
if node[:@controller]
|
106
125
|
instance = node[:@controller].new
|
126
|
+
instance.kenji = kenji if instance.respond_to?(:kenji=)
|
107
127
|
return instance.call(method, remaining_segments.join('/'))
|
108
128
|
end
|
109
129
|
|
110
130
|
# regular routing
|
111
|
-
node = self.class.routes[method]
|
131
|
+
node = self.class.routes[method] || {}
|
112
132
|
variables = []
|
113
133
|
searching = true
|
114
134
|
segments.each do |segment| # traverse tree to find
|
@@ -140,6 +160,8 @@ module Kenji
|
|
140
160
|
else
|
141
161
|
return fallback
|
142
162
|
end
|
163
|
+
else
|
164
|
+
kenji.respond(404, 'Not found!')
|
143
165
|
end
|
144
166
|
end
|
145
167
|
|
@@ -151,6 +173,9 @@ module Kenji
|
|
151
173
|
def self.passes
|
152
174
|
@passes || {}
|
153
175
|
end
|
176
|
+
def self.befores
|
177
|
+
@befores || []
|
178
|
+
end
|
154
179
|
end
|
155
180
|
end
|
156
181
|
|
data/lib/kenji/version.rb
CHANGED
data/lib/kenji.rb
CHANGED
@@ -10,52 +10,89 @@ module Kenji
|
|
10
10
|
|
11
11
|
# Setting `kenji.status = 203` will affect the status code of the response.
|
12
12
|
attr_accessor :status
|
13
|
+
# Exceptions will be printed here, and controllers are expected to log to
|
14
|
+
# this IO buffer:
|
15
|
+
attr_accessor :stderr
|
13
16
|
|
14
17
|
# Methods for rack!
|
15
18
|
|
16
19
|
# Constructor...
|
17
20
|
#
|
18
|
-
|
21
|
+
# `env` should be the environment hash provided by Rack.
|
22
|
+
#
|
23
|
+
# `root` is the root directory (as output by File.expand_path) of the Kenji
|
24
|
+
# directory structure.
|
25
|
+
#
|
26
|
+
# `options` is an options hash that accepts the following keys:
|
27
|
+
#
|
28
|
+
# - :auto_cors => true | false # automatically deal with
|
29
|
+
# CORS / Access-Control
|
30
|
+
#
|
31
|
+
def initialize(env, root, options = {})
|
19
32
|
@headers = {
|
20
33
|
'Content-Type' => 'application/json'
|
21
34
|
}
|
22
35
|
@status = 200
|
23
36
|
@root = File.expand_path(root) + '/'
|
37
|
+
@stderr = $stderr
|
24
38
|
@env = env
|
39
|
+
|
40
|
+
@options = {
|
41
|
+
auto_cors: true
|
42
|
+
}.merge(options)
|
25
43
|
end
|
26
44
|
|
27
45
|
# This method does all the work!
|
28
46
|
#
|
29
47
|
def call
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
|
49
|
+
auto_cors if @options[:auto_cors]
|
50
|
+
|
51
|
+
catch(:KenjiRespondControlFlowInterrupt) do
|
52
|
+
path = @env['PATH_INFO']
|
53
|
+
|
54
|
+
# deal with static files
|
55
|
+
static = "#{@root}public#{path}"
|
56
|
+
return Rack::File.new("#{@root}public").call(@env) if File.file?(static)
|
57
|
+
|
58
|
+
|
59
|
+
# new routing code
|
60
|
+
segments = path.split('/')
|
61
|
+
segments = segments.drop(1) if segments.first == '' # discard leading /'s empty segment
|
62
|
+
segments.unshift('')
|
63
|
+
|
64
|
+
acc = ''; out = '', success = false
|
65
|
+
while head = segments.shift
|
66
|
+
acc = "#{acc}/#{head}"
|
67
|
+
if controller = controller_for(acc) # if we have a valid controller
|
68
|
+
begin
|
69
|
+
method = @env['REQUEST_METHOD'].downcase.to_sym
|
70
|
+
subpath = '/'+segments.join('/')
|
71
|
+
out = controller.call(method, subpath).to_json
|
72
|
+
end
|
73
|
+
success = true
|
74
|
+
break
|
53
75
|
end
|
54
|
-
break
|
55
76
|
end
|
77
|
+
|
78
|
+
return response_404 unless success
|
79
|
+
|
80
|
+
[@status, @headers, [out]]
|
56
81
|
end
|
82
|
+
rescue Exception => e
|
83
|
+
@stderr.puts e.inspect # log exceptions
|
84
|
+
e.backtrace.each {|b| @stderr.puts " #{b}" }
|
85
|
+
response_500
|
86
|
+
end
|
87
|
+
|
88
|
+
# 500 error
|
89
|
+
def response_500
|
90
|
+
[500, @headers, [{status: 500, message: 'Something went wrong...'}.to_json]]
|
91
|
+
end
|
57
92
|
|
58
|
-
|
93
|
+
# 404 error
|
94
|
+
def response_404
|
95
|
+
[404, @headers, [{status: 404, message: 'Not found!'}.to_json]]
|
59
96
|
end
|
60
97
|
|
61
98
|
|
@@ -96,7 +133,7 @@ module Kenji
|
|
96
133
|
:message => message
|
97
134
|
}
|
98
135
|
hash.each { |k,v| response[k]=v }
|
99
|
-
|
136
|
+
throw(:KenjiRespondControlFlowInterrupt, [@status, @headers, [response.to_json]])
|
100
137
|
end
|
101
138
|
|
102
139
|
|
@@ -104,6 +141,23 @@ module Kenji
|
|
104
141
|
# Private methods
|
105
142
|
private
|
106
143
|
|
144
|
+
# Deals with silly HTTP CORS Access-Control restrictions by automatically
|
145
|
+
# allowing all requests.
|
146
|
+
#
|
147
|
+
def auto_cors
|
148
|
+
origin = env['HTTP_ORIGIN']
|
149
|
+
header 'Access-Control-Allow-Origin' => origin if origin
|
150
|
+
|
151
|
+
if env['REQUEST_METHOD'] == 'OPTIONS'
|
152
|
+
header 'Access-Control-Allow-Methods' => 'OPTIONS, GET, POST, PUT, DELETE'
|
153
|
+
|
154
|
+
if requested_headers = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
|
155
|
+
header 'Access-Control-Allow-Headers' => requested_headers
|
156
|
+
end
|
157
|
+
respond(200, 'CORS is allowed.')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
107
161
|
# Will attempt to fetch the controller, and verify that it is a implements call
|
108
162
|
#
|
109
163
|
def controller_for(subpath)
|
@@ -122,12 +176,5 @@ module Kenji
|
|
122
176
|
|
123
177
|
end
|
124
178
|
|
125
|
-
|
126
|
-
class KenjiRespondControlFlowInterrupt < StandardError
|
127
|
-
attr_accessor :response
|
128
|
-
def initialize(response)
|
129
|
-
@response = response
|
130
|
-
end
|
131
|
-
end # early exit containing a response
|
132
179
|
end
|
133
180
|
|
data/spec/1/controllers/main.rb
CHANGED
@@ -5,6 +5,10 @@ class MainController < Kenji::Controller
|
|
5
5
|
{status: 200, hello: :world}
|
6
6
|
end
|
7
7
|
|
8
|
+
get '/crasher' do
|
9
|
+
raise
|
10
|
+
end
|
11
|
+
|
8
12
|
post '/' do
|
9
13
|
{status:1337}
|
10
14
|
end
|
@@ -16,4 +20,9 @@ class MainController < Kenji::Controller
|
|
16
20
|
delete '/' do
|
17
21
|
{status:1337}
|
18
22
|
end
|
23
|
+
|
24
|
+
get '/respond' do
|
25
|
+
kenji.respond(123, 'hello')
|
26
|
+
raise # never called
|
27
|
+
end
|
19
28
|
end
|
data/spec/kenji_spec.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require 'rack'
|
3
3
|
require 'rack/test'
|
4
4
|
require 'rspec'
|
5
|
+
require 'rspec/mocks'
|
5
6
|
|
6
7
|
require 'kenji'
|
7
8
|
|
@@ -9,57 +10,114 @@ require 'kenji'
|
|
9
10
|
# NOTE: these tests make use of the controllers defined in test/controllers.
|
10
11
|
|
11
12
|
def app_for(path)
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
lambda do |env|
|
14
|
+
kenji = Kenji::Kenji.new(env, File.dirname(__FILE__)+'/'+path)
|
15
|
+
kenji.stderr = double(puts: nil)
|
16
|
+
kenji.call
|
17
|
+
end
|
15
18
|
end
|
16
19
|
|
17
|
-
describe Kenji do
|
18
|
-
|
20
|
+
describe Kenji::Kenji, 'expected reponses' do
|
19
21
|
include Rack::Test::Methods
|
20
|
-
def app; app_for('1'); end
|
21
22
|
|
23
|
+
context '1' do
|
24
|
+
def app; app_for('1'); end
|
22
25
|
|
23
|
-
it 'should return "null" for unknown routes' do
|
24
|
-
get '/sdlkjhb'
|
25
|
-
last_response.body.should == 'null'
|
26
|
-
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
it 'should return 404 for unknown routes (no controller)' do
|
28
|
+
get '/sdlkjhb'
|
29
|
+
expected_response = {status: 404, message: 'Not found!'}.to_json
|
30
|
+
last_response.body.should == expected_response
|
31
|
+
last_response.status.should == 404
|
32
|
+
end
|
33
33
|
|
34
|
-
|
34
|
+
it 'should return 404 for unknown routes (no route on valid controller)' do
|
35
|
+
get '/main/sdlkjhb'
|
36
|
+
expected_response = {status: 404, message: 'Not found!'}.to_json
|
37
|
+
last_response.body.should == expected_response
|
38
|
+
last_response.status.should == 404
|
39
|
+
end
|
35
40
|
|
36
|
-
it
|
37
|
-
|
38
|
-
expected_response = {status:
|
41
|
+
it 'should return 500 for exceptions' do
|
42
|
+
get '/main/crasher'
|
43
|
+
expected_response = {status: 500, message: 'Something went wrong...'}.to_json
|
39
44
|
last_response.body.should == expected_response
|
45
|
+
last_response.status.should == 500
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should route a GET call to a defined get call' do
|
49
|
+
get '/main/hello'
|
50
|
+
expected_response = {status: 200, hello: :world}.to_json
|
51
|
+
last_response.body.should == expected_response
|
52
|
+
end
|
53
|
+
|
54
|
+
[:post, :put, :delete].each do |method|
|
55
|
+
|
56
|
+
it "should route a #{method.to_s.upcase} to a defined #{method.to_s} call" do
|
57
|
+
send(method, '/main')
|
58
|
+
expected_response = {status: 1337}.to_json
|
59
|
+
last_response.body.should == expected_response
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should return "null" for unsupported methods' do
|
64
|
+
post '/main/hello'
|
65
|
+
expected_response = {status: 404, message: 'Not found!'}.to_json
|
66
|
+
last_response.body.should == expected_response
|
67
|
+
last_response.status.should == 404
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should use throw / catch to respond immediately with kenji.respond' do
|
71
|
+
get '/main/respond'
|
72
|
+
expected_response = {status: 123, message: 'hello'}.to_json
|
73
|
+
last_response.body.should == expected_response
|
74
|
+
last_response.status.should == 123
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should automatically allow CORS for simple requests' do
|
78
|
+
header 'Origin', 'foo'
|
79
|
+
get '/main/hello'
|
80
|
+
last_response.header['Access-Control-Allow-Origin'].should == 'foo'
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should automatically allow CORS for complex requests' do
|
84
|
+
header 'Origin', 'foo'
|
85
|
+
header 'Access-Control-Request-Headers', 'Bar'
|
86
|
+
options '/main/hello'
|
87
|
+
last_response.header['Access-Control-Allow-Origin'].should == 'foo'
|
88
|
+
last_response.header['Access-Control-Allow-Methods'].should == 'OPTIONS, GET, POST, PUT, DELETE'
|
89
|
+
last_response.header['Access-Control-Allow-Headers'].should == 'Bar'
|
40
90
|
end
|
41
|
-
end
|
42
91
|
|
43
|
-
it 'should return "null" for unsupported methods' do
|
44
|
-
post '/main/hello'
|
45
|
-
last_response.body.should == 'null'
|
46
92
|
end
|
47
93
|
|
48
|
-
|
94
|
+
context '2' do
|
95
|
+
def app; app_for('2'); end
|
49
96
|
|
50
|
-
|
51
|
-
|
52
|
-
|
97
|
+
it 'should use root controller' do
|
98
|
+
get '/'
|
99
|
+
expected_response = {status: 200, controller_used: :root}.to_json
|
100
|
+
last_response.body.should == expected_response
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should pass routing down to child controllers' do
|
104
|
+
get '/child/foo'
|
105
|
+
expected_response = {status: 200, foo: :bar}.to_json
|
106
|
+
last_response.body.should == expected_response
|
107
|
+
end
|
53
108
|
|
54
|
-
it 'should use root controller' do
|
55
|
-
get '/'
|
56
|
-
expected_response = {status: 200, controller_used: :root}.to_json
|
57
|
-
last_response.body.should == expected_response
|
58
109
|
end
|
59
110
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
111
|
+
context '3' do
|
112
|
+
def app; app_for('3'); end
|
113
|
+
|
114
|
+
it 'should call before block' do
|
115
|
+
get '/before/hello'
|
116
|
+
expected_response = {status: 302, message: 'redirect...'}.to_json
|
117
|
+
last_response.body.should == expected_response
|
118
|
+
last_response.status.should == 302
|
119
|
+
end
|
120
|
+
|
64
121
|
end
|
122
|
+
|
65
123
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kenji
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.6.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -79,6 +79,7 @@ files:
|
|
79
79
|
- spec/1/controllers/main.rb
|
80
80
|
- spec/2/controllers/_.rb
|
81
81
|
- spec/2/controllers/child.rb
|
82
|
+
- spec/3/controllers/before.rb
|
82
83
|
- spec/kenji_spec.rb
|
83
84
|
homepage: https://github.com/kballenegger/kenji
|
84
85
|
licenses: []
|
@@ -108,5 +109,6 @@ test_files:
|
|
108
109
|
- spec/1/controllers/main.rb
|
109
110
|
- spec/2/controllers/_.rb
|
110
111
|
- spec/2/controllers/child.rb
|
112
|
+
- spec/3/controllers/before.rb
|
111
113
|
- spec/kenji_spec.rb
|
112
114
|
has_rdoc:
|