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