lotus-controller 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +7 -14
- data/.travis.yml +5 -0
- data/.yardopts +4 -0
- data/Gemfile +13 -2
- data/README.md +627 -6
- data/Rakefile +17 -1
- data/lib/lotus-controller.rb +1 -0
- data/lib/lotus/action.rb +55 -0
- data/lib/lotus/action/callable.rb +93 -0
- data/lib/lotus/action/callbacks.rb +138 -0
- data/lib/lotus/action/cookie_jar.rb +97 -0
- data/lib/lotus/action/cookies.rb +60 -0
- data/lib/lotus/action/exposable.rb +81 -0
- data/lib/lotus/action/mime.rb +190 -0
- data/lib/lotus/action/params.rb +53 -0
- data/lib/lotus/action/rack.rb +97 -0
- data/lib/lotus/action/redirect.rb +34 -0
- data/lib/lotus/action/session.rb +48 -0
- data/lib/lotus/action/throwable.rb +130 -0
- data/lib/lotus/controller.rb +65 -2
- data/lib/lotus/controller/dsl.rb +54 -0
- data/lib/lotus/controller/version.rb +4 -1
- data/lib/lotus/http/status.rb +103 -0
- data/lib/rack-patch.rb +20 -0
- data/lotus-controller.gemspec +16 -11
- data/test/action/callbacks_test.rb +99 -0
- data/test/action/params_test.rb +29 -0
- data/test/action_test.rb +31 -0
- data/test/controller_test.rb +24 -0
- data/test/cookies_test.rb +36 -0
- data/test/fixtures.rb +501 -0
- data/test/integration/mime_type_test.rb +175 -0
- data/test/integration/routing_test.rb +141 -0
- data/test/integration/sessions_test.rb +63 -0
- data/test/redirect_test.rb +20 -0
- data/test/session_test.rb +19 -0
- data/test/test_helper.rb +24 -0
- data/test/throw_test.rb +93 -0
- data/test/version_test.rb +7 -0
- metadata +112 -9
@@ -0,0 +1,48 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Action
|
3
|
+
# Session API
|
4
|
+
#
|
5
|
+
# This module isn't included by default.
|
6
|
+
#
|
7
|
+
# @since 0.1.0
|
8
|
+
module Session
|
9
|
+
# The key that returns raw session from the Rack env
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
SESSION_KEY = 'rack.session'.freeze
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
# Gets the session from the request and expose it as an Hash.
|
17
|
+
#
|
18
|
+
# @return [Hash] the HTTP session from the request
|
19
|
+
#
|
20
|
+
# @since 0.1.0
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# require 'lotus/controller'
|
24
|
+
# require 'lotus/action/session'
|
25
|
+
#
|
26
|
+
# class Show
|
27
|
+
# include Lotus::Action
|
28
|
+
# include Lotus::Action::Session
|
29
|
+
#
|
30
|
+
# def call(params)
|
31
|
+
# # ...
|
32
|
+
#
|
33
|
+
# # get a value
|
34
|
+
# session[:user_id] # => '23'
|
35
|
+
#
|
36
|
+
# # set a value
|
37
|
+
# session[:foo] = 'bar'
|
38
|
+
#
|
39
|
+
# # remove a value
|
40
|
+
# session[:bax] = nil
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
def session
|
44
|
+
@_env[SESSION_KEY] ||= {}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'lotus/utils/class_attribute'
|
2
|
+
require 'lotus/http/status'
|
3
|
+
|
4
|
+
module Lotus
|
5
|
+
module Action
|
6
|
+
# Throw API
|
7
|
+
#
|
8
|
+
# @since 0.1.0
|
9
|
+
#
|
10
|
+
# @see Lotus::Action::Throwable::ClassMethods#handle_exception
|
11
|
+
# @see Lotus::Action::Throwable#throw
|
12
|
+
# @see Lotus::Action::Throwable#status
|
13
|
+
module Throwable
|
14
|
+
def self.included(base)
|
15
|
+
base.class_eval do
|
16
|
+
extend ClassMethods
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def self.extended(base)
|
22
|
+
base.class_eval do
|
23
|
+
include Utils::ClassAttribute
|
24
|
+
|
25
|
+
# Action handled exceptions.
|
26
|
+
#
|
27
|
+
# When an handled exception is raised during #call execution, it will be
|
28
|
+
# translated into the associated HTTP status.
|
29
|
+
#
|
30
|
+
# By default there aren't handled exceptions, all the errors are threaded
|
31
|
+
# as a Server Side Error (500).
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
# @since 0.1.0
|
35
|
+
#
|
36
|
+
# @see Lotus::Controller.handled_exceptions
|
37
|
+
# @see Lotus::Action::Throwable.handle_exception
|
38
|
+
class_attribute :handled_exceptions
|
39
|
+
self.handled_exceptions = Controller.handled_exceptions.dup
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
# Handle the given exception with an HTTP status code.
|
46
|
+
#
|
47
|
+
# When the exception is raise during #call execution, it will be
|
48
|
+
# translated into the associated HTTP status.
|
49
|
+
#
|
50
|
+
# This is a fine grained control, for a global configuration see
|
51
|
+
# Lotus::Action.handled_exceptions
|
52
|
+
#
|
53
|
+
# @param exception [Class] the exception class
|
54
|
+
# @param status [Fixmun] a valid HTTP status
|
55
|
+
#
|
56
|
+
# @since 0.1.0
|
57
|
+
#
|
58
|
+
# @see Lotus::Action.handled_exceptions
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# require 'lotus/controller'
|
62
|
+
#
|
63
|
+
# class Show
|
64
|
+
# include Lotus::Action
|
65
|
+
# handle_exception RecordNotFound, 404
|
66
|
+
#
|
67
|
+
# def call(params)
|
68
|
+
# # ...
|
69
|
+
# raise RecordNotFound.new
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
|
74
|
+
def handle_exception(exception, status)
|
75
|
+
self.handled_exceptions[exception] = status
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
# Throw the given HTTP status code.
|
82
|
+
#
|
83
|
+
# When used, the execution of a callback or of an action is interrupted
|
84
|
+
# and the control returns to the framework, that decides how to handle
|
85
|
+
# the event.
|
86
|
+
#
|
87
|
+
# It also sets the response body with the message associated to the code
|
88
|
+
# (eg 404 will set `"Not Found"`).
|
89
|
+
#
|
90
|
+
# @param code [Fixnum] a valid HTTP status code
|
91
|
+
#
|
92
|
+
# @since 0.1.0
|
93
|
+
#
|
94
|
+
# @see Lotus::Controller#handled_exceptions
|
95
|
+
# @see Lotus::Action::Throwable#handle_exception
|
96
|
+
# @see Lotus::Http::Status:ALL
|
97
|
+
def throw(code)
|
98
|
+
status(*Http::Status.for_code(code))
|
99
|
+
super :halt
|
100
|
+
end
|
101
|
+
|
102
|
+
# Sets the given code and message for the response
|
103
|
+
#
|
104
|
+
# @param code [Fixnum] a valid HTTP status code
|
105
|
+
# @param message [String] the response body
|
106
|
+
#
|
107
|
+
# @since 0.1.0
|
108
|
+
# @see Lotus::Http::Status:ALL
|
109
|
+
def status(code, message)
|
110
|
+
self.status = code
|
111
|
+
self.body = message
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
def _rescue
|
116
|
+
catch :halt do
|
117
|
+
begin
|
118
|
+
yield
|
119
|
+
rescue => exception
|
120
|
+
_handle_exception(exception)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def _handle_exception(exception)
|
126
|
+
throw self.class.handled_exceptions.fetch(exception.class, 500)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/lotus/controller.rb
CHANGED
@@ -1,7 +1,70 @@
|
|
1
|
-
require
|
1
|
+
require 'lotus/utils/class_attribute'
|
2
|
+
require 'lotus/action'
|
3
|
+
require 'lotus/controller/dsl'
|
4
|
+
require 'lotus/controller/version'
|
5
|
+
require 'rack-patch'
|
2
6
|
|
3
7
|
module Lotus
|
8
|
+
# A set of logically grouped actions
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
11
|
+
#
|
12
|
+
# @see Lotus::Action
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# require 'lotus/controller'
|
16
|
+
#
|
17
|
+
# class ArticlesController
|
18
|
+
# include Lotus::Controller
|
19
|
+
#
|
20
|
+
# action 'Index' do
|
21
|
+
# # ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# action 'Show' do
|
25
|
+
# # ...
|
26
|
+
# end
|
27
|
+
# end
|
4
28
|
module Controller
|
5
|
-
|
29
|
+
include Utils::ClassAttribute
|
30
|
+
|
31
|
+
# Global handled exceptions.
|
32
|
+
# When an handled exception is raised during #call execution, it will be
|
33
|
+
# translated into the associated HTTP status.
|
34
|
+
#
|
35
|
+
# By default there aren't handled exceptions, all the errors are threaded
|
36
|
+
# as a Server Side Error (500).
|
37
|
+
#
|
38
|
+
# **Important:** Be sure to set this configuration, **before** the actions
|
39
|
+
# and controllers of your application are loaded.
|
40
|
+
#
|
41
|
+
# @since 0.1.0
|
42
|
+
#
|
43
|
+
# @see Lotus::Action::Throwable
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# require 'lotus/controller'
|
47
|
+
#
|
48
|
+
# Lotus::Controller.handled_exceptions = { RecordNotFound => 404 }
|
49
|
+
#
|
50
|
+
# class Show
|
51
|
+
# include Lotus::Action
|
52
|
+
#
|
53
|
+
# def call(params)
|
54
|
+
# # ...
|
55
|
+
# raise RecordNotFound.new
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
|
60
|
+
class_attribute :handled_exceptions
|
61
|
+
self.handled_exceptions = {}
|
62
|
+
|
63
|
+
def self.included(base)
|
64
|
+
base.class_eval do
|
65
|
+
include Dsl
|
66
|
+
end
|
67
|
+
end
|
6
68
|
end
|
7
69
|
end
|
70
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Controller
|
3
|
+
# Public DSL
|
4
|
+
#
|
5
|
+
# @since 0.1.0
|
6
|
+
module Dsl
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
extend ClassMethods
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
# Define an action for the given name.
|
15
|
+
# It generates a concrete class for the action, for this reason the name
|
16
|
+
# MUST be a valid name for Ruby.
|
17
|
+
#
|
18
|
+
# @param name [String] the name of the action
|
19
|
+
# @param blk [Proc] the code of the action
|
20
|
+
#
|
21
|
+
# @raise TypeError when the name isn't a valid Ruby name
|
22
|
+
#
|
23
|
+
# @since 0.1.0
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# require 'lotus/controller'
|
27
|
+
#
|
28
|
+
# class ArticlesController
|
29
|
+
# include Lotus::Controller
|
30
|
+
#
|
31
|
+
# action 'Index' do
|
32
|
+
# def call(params)
|
33
|
+
# # ...
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# action 'Show' do
|
38
|
+
# def call(params)
|
39
|
+
# # ...
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
def action(name, &blk)
|
44
|
+
const_set(name, Class.new)
|
45
|
+
|
46
|
+
const_get(name).tap do |klass|
|
47
|
+
klass.class_eval { include ::Lotus::Action }
|
48
|
+
klass.class_eval(&blk)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Http
|
3
|
+
# An HTTP status
|
4
|
+
#
|
5
|
+
# @since 0.1.0
|
6
|
+
# @api private
|
7
|
+
class Status
|
8
|
+
# A set of standard codes and messages for HTTP statuses
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
11
|
+
# @api private
|
12
|
+
ALL = {
|
13
|
+
100 => 'Continue',
|
14
|
+
101 => 'Switching Protocols',
|
15
|
+
102 => 'Processing (WebDAV) (RFC 2518)',
|
16
|
+
103 => 'Checkpoint',
|
17
|
+
122 => 'Request-URI too long',
|
18
|
+
200 => 'OK',
|
19
|
+
201 => 'Created',
|
20
|
+
202 => 'Accepted',
|
21
|
+
203 => 'Non-Authoritative Information',
|
22
|
+
204 => 'No Content',
|
23
|
+
205 => 'Reset Content',
|
24
|
+
206 => 'Partial Content',
|
25
|
+
207 => 'Multi-Status (WebDAV) (RFC 4918)',
|
26
|
+
208 => 'Already Reported (WebDAV) (RFC 5842)',
|
27
|
+
226 => 'IM Used (RFC 3229)',
|
28
|
+
300 => 'Multiple Choices',
|
29
|
+
301 => 'Moved Permanently',
|
30
|
+
302 => 'Found',
|
31
|
+
303 => 'See Other',
|
32
|
+
304 => 'Not Modified',
|
33
|
+
305 => 'Use Proxy',
|
34
|
+
306 => 'Switch Proxy',
|
35
|
+
307 => 'Temporary Redirect',
|
36
|
+
308 => 'Resume Incomplete',
|
37
|
+
400 => 'Bad Request',
|
38
|
+
401 => 'Unauthorized',
|
39
|
+
402 => 'Payment Required',
|
40
|
+
403 => 'Forbidden',
|
41
|
+
404 => 'Not Found',
|
42
|
+
405 => 'Method Not Allowed',
|
43
|
+
406 => 'Not Acceptable',
|
44
|
+
407 => 'Proxy Authentication Required',
|
45
|
+
408 => 'Request Timeout',
|
46
|
+
409 => 'Conflict',
|
47
|
+
410 => 'Gone',
|
48
|
+
411 => 'Length Required',
|
49
|
+
412 => 'Precondition Failed',
|
50
|
+
413 => 'Request Entity Too Large',
|
51
|
+
414 => 'Request-URI Too Long',
|
52
|
+
415 => 'Unsupported Media Type',
|
53
|
+
416 => 'Requested Range Not Satisfiable',
|
54
|
+
417 => 'Expectation Failed',
|
55
|
+
418 => 'I\'m a teapot (RFC 2324)',
|
56
|
+
420 => 'Enhance Your Calm',
|
57
|
+
422 => 'Unprocessable Entity (WebDAV) (RFC 4918)',
|
58
|
+
423 => 'Locked (WebDAV) (RFC 4918)',
|
59
|
+
424 => 'Failed Dependency (WebDAV) (RFC 4918)',
|
60
|
+
426 => 'Upgrade Required (RFC 2817)',
|
61
|
+
428 => 'Precondition Required',
|
62
|
+
429 => 'Too Many Requests',
|
63
|
+
431 => 'Request Header Fields Too Large',
|
64
|
+
444 => 'No Response',
|
65
|
+
449 => 'Retry With',
|
66
|
+
450 => 'Blocked by Windows Parental Controls',
|
67
|
+
451 => 'Wrong Exchange server',
|
68
|
+
499 => 'Client Closed Request',
|
69
|
+
500 => 'Internal Server Error',
|
70
|
+
501 => 'Not Implemented',
|
71
|
+
502 => 'Bad Gateway',
|
72
|
+
503 => 'Service Unavailable',
|
73
|
+
504 => 'Gateway Timeout',
|
74
|
+
505 => 'HTTP Version Not Supported',
|
75
|
+
506 => 'Variant Also Negotiates (RFC 2295)',
|
76
|
+
507 => 'Insufficient Storage (WebDAV) (RFC 4918)',
|
77
|
+
508 => 'Loop Detected (WebDAV) (RFC 5842)',
|
78
|
+
509 => 'Bandwidth Limit Exceeded (Apache bw\/limited extension)',
|
79
|
+
510 => 'Not Extended (RFC 2774)',
|
80
|
+
511 => 'Network Authentication Required',
|
81
|
+
598 => 'Network read timeout error',
|
82
|
+
599 => 'Network connect timeout error'
|
83
|
+
}.freeze
|
84
|
+
|
85
|
+
# Return a status for the given code
|
86
|
+
#
|
87
|
+
# @param code [Fixnum] a valid HTTP code
|
88
|
+
#
|
89
|
+
# @return [Array] a pair of code and message for an HTTP status
|
90
|
+
#
|
91
|
+
# @since 0.1.0
|
92
|
+
# @api private
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# require 'lotus/http/status'
|
96
|
+
#
|
97
|
+
# Lotus::Http::Status.for_code(418) # => [418, "I'm a teapot (RFC 2324)"]
|
98
|
+
def self.for_code(code)
|
99
|
+
ALL.assoc(code)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/rack-patch.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# see https://github.com/rack/rack/pull/659
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
if Rack.release <= '1.5'
|
5
|
+
require 'rack/utils'
|
6
|
+
|
7
|
+
Rack::Utils.class_eval do
|
8
|
+
def self.best_q_match(q_value_header, available_mimes)
|
9
|
+
values = q_values(q_value_header)
|
10
|
+
|
11
|
+
values.map do |req_mime, quality|
|
12
|
+
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
|
13
|
+
next unless match
|
14
|
+
[match, quality]
|
15
|
+
end.compact.sort_by do |match, quality|
|
16
|
+
(match.split('/', 2).count('*') * -10) + quality
|
17
|
+
end.last.first
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lotus-controller.gemspec
CHANGED
@@ -4,20 +4,25 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'lotus/controller/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'lotus-controller'
|
8
8
|
spec.version = Lotus::Controller::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary = %q{Controller layer for Lotus}
|
9
|
+
spec.authors = ['Luca Guidi']
|
10
|
+
spec.email = ['me@lucaguidi.com']
|
12
11
|
spec.description = %q{Controller layer for Lotus}
|
13
|
-
spec.
|
14
|
-
spec.
|
12
|
+
spec.summary = %q{Controller layer for Lotus, compatible with Rack}
|
13
|
+
spec.homepage = 'http://lotusrb.org'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
17
|
-
spec.executables =
|
18
|
-
spec.test_files = spec.files.grep(%r{^(test
|
19
|
-
spec.require_paths = [
|
17
|
+
spec.executables = []
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.
|
22
|
-
spec.
|
21
|
+
spec.add_dependency 'rack', '~> 1.5'
|
22
|
+
spec.add_dependency 'lotus-utils', '~> 0.1'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
25
|
+
spec.add_development_dependency 'minitest', '~> 5'
|
26
|
+
spec.add_development_dependency 'rack-test', '~> 0.6'
|
27
|
+
spec.add_development_dependency 'rake', '~> 10'
|
23
28
|
end
|