kenji 0.5 → 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
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
- ````ruby
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
- ````ruby
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 rubygems and bundler.
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
+
@@ -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
@@ -1,5 +1,5 @@
1
1
 
2
2
  module Kenji
3
- VERSION = '0.5'
3
+ VERSION = '0.6.5'
4
4
  end
5
5
 
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
- def initialize(env, root)
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
- path = @env['PATH_INFO']
31
-
32
- # deal with static files
33
- static = "#{@root}public#{path}"
34
- return Rack::File.new("#{@root}public").call(@env) if File.file?(static)
35
-
36
-
37
- # new routing code
38
- segments = path.split('/')
39
- segments = segments.drop(1) if segments.first == '' # discard leading /'s empty segment
40
- segments.unshift('')
41
-
42
- acc = ''; out = 'null'
43
- while head = segments.shift
44
- acc = "#{acc}/#{head}"
45
- if controller = controller_for(acc) # if we have a valid controller
46
- begin
47
- out = controller.call(@env['REQUEST_METHOD'].downcase.to_sym, '/'+segments.join('/')).to_json
48
- rescue KenjiRespondControlFlowInterrupt => e
49
- out = e.response
50
- rescue Exception => e
51
- p e, e.backtrace # log exceptions
52
- raise e
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
- [@status, @headers, [out]]
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
- raise KenjiRespondControlFlowInterrupt.new(response.to_json)
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
 
@@ -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
@@ -0,0 +1,11 @@
1
+
2
+ class BeforeController < Kenji::Controller
3
+
4
+ before do
5
+ kenji.respond(302, 'redirect...')
6
+ end
7
+
8
+ get '/hello' do
9
+ {status: 200, hello: :world}
10
+ end
11
+ 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
- lambda do |env|
13
- Kenji::Kenji.new(env, File.dirname(__FILE__)+'/'+path).call
14
- end
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
- it 'should route a GET call to a defined get call' do
29
- get '/main/hello'
30
- expected_response = {status: 200, hello: :world}.to_json
31
- last_response.body.should == expected_response
32
- end
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
- [:post, :put, :delete].each do |method|
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 "should route a #{method.to_s.upcase} to a defined #{method.to_s} call" do
37
- send(method, '/main')
38
- expected_response = {status: 1337}.to_json
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
- end
94
+ context '2' do
95
+ def app; app_for('2'); end
49
96
 
50
- describe Kenji do
51
- include Rack::Test::Methods
52
- def app; app_for('2'); end
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
- it 'should pass routing down to child controllers' do
61
- get '/child/foo'
62
- expected_response = {status: 200, foo: :bar}.to_json
63
- last_response.body.should == expected_response
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: '0.5'
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: