lotus-controller 0.0.0 → 0.1.0
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.
- 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
|