hanami-controller 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +1180 -9
  5. data/hanami-controller.gemspec +19 -12
  6. data/lib/hanami-controller.rb +1 -0
  7. data/lib/hanami/action.rb +85 -0
  8. data/lib/hanami/action/cache.rb +174 -0
  9. data/lib/hanami/action/cache/cache_control.rb +70 -0
  10. data/lib/hanami/action/cache/conditional_get.rb +93 -0
  11. data/lib/hanami/action/cache/directives.rb +99 -0
  12. data/lib/hanami/action/cache/expires.rb +73 -0
  13. data/lib/hanami/action/callable.rb +94 -0
  14. data/lib/hanami/action/callbacks.rb +210 -0
  15. data/lib/hanami/action/configurable.rb +49 -0
  16. data/lib/hanami/action/cookie_jar.rb +181 -0
  17. data/lib/hanami/action/cookies.rb +85 -0
  18. data/lib/hanami/action/exposable.rb +115 -0
  19. data/lib/hanami/action/flash.rb +182 -0
  20. data/lib/hanami/action/glue.rb +66 -0
  21. data/lib/hanami/action/head.rb +122 -0
  22. data/lib/hanami/action/mime.rb +493 -0
  23. data/lib/hanami/action/params.rb +285 -0
  24. data/lib/hanami/action/rack.rb +270 -0
  25. data/lib/hanami/action/rack/callable.rb +47 -0
  26. data/lib/hanami/action/rack/file.rb +33 -0
  27. data/lib/hanami/action/redirect.rb +59 -0
  28. data/lib/hanami/action/request.rb +86 -0
  29. data/lib/hanami/action/session.rb +154 -0
  30. data/lib/hanami/action/throwable.rb +194 -0
  31. data/lib/hanami/action/validatable.rb +128 -0
  32. data/lib/hanami/controller.rb +250 -2
  33. data/lib/hanami/controller/configuration.rb +705 -0
  34. data/lib/hanami/controller/error.rb +7 -0
  35. data/lib/hanami/controller/version.rb +4 -1
  36. data/lib/hanami/http/status.rb +62 -0
  37. metadata +124 -16
  38. data/.gitignore +0 -9
  39. data/Gemfile +0 -4
  40. data/Rakefile +0 -2
  41. data/bin/console +0 -14
  42. data/bin/setup +0 -8
@@ -0,0 +1,47 @@
1
+
2
+ module Hanami
3
+ module Action
4
+ module Rack
5
+ module Callable
6
+ # Callable module for actions. With this module, actions with middlewares
7
+ # will be able to work with rack builder.
8
+ #
9
+ # @param env [Hash] the full Rack env or the params. This value may vary,
10
+ # see the examples below.
11
+ #
12
+ # @since 0.4.0
13
+ #
14
+ # @see Hanami::Action::Rack::ClassMethods#rack_builder
15
+ # @see Hanami::Action::Rack::ClassMethods#use
16
+ #
17
+ # @example
18
+ # require 'hanami/controller'
19
+ #
20
+ # class MyMiddleware
21
+ # def initialize(app)
22
+ # @app = app
23
+ # end
24
+ #
25
+ # def call(env)
26
+ # #...
27
+ # end
28
+ # end
29
+ #
30
+ # class Show
31
+ # include Hanami::Action
32
+ # use MyMiddleware
33
+ #
34
+ # def call(params)
35
+ # # ...
36
+ # puts params # => { id: 23 } extracted from Rack env
37
+ # end
38
+ # end
39
+ #
40
+ # Show.respond_to?(:call) # => true
41
+ def call(env)
42
+ rack_builder.call(env)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ require 'rack/file'
2
+
3
+ module Hanami
4
+ module Action
5
+ module Rack
6
+ # File to be sent
7
+ #
8
+ # @since 0.4.3
9
+ # @api private
10
+ #
11
+ # @see Hanami::Action::Rack#send_file
12
+ class File
13
+ # @param path [String,Pathname] file path
14
+ #
15
+ # @since 0.4.3
16
+ # @api private
17
+ def initialize(path)
18
+ @file = ::Rack::File.new(nil)
19
+ @path = path
20
+ end
21
+
22
+ # @since 0.4.3
23
+ # @api private
24
+ def call(env)
25
+ @file.path = @path.to_s
26
+ @file.serving(env)
27
+ rescue Errno::ENOENT
28
+ [404, {}, nil]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ module Hanami
2
+ module Action
3
+ # HTTP redirect API
4
+ #
5
+ # @since 0.1.0
6
+ module Redirect
7
+ # The HTTP header for redirects
8
+ #
9
+ # @since 0.2.0
10
+ # @api private
11
+ LOCATION = 'Location'.freeze
12
+
13
+ private
14
+
15
+ # Redirect to the given URL and halt the request
16
+ #
17
+ # @param url [String] the destination URL
18
+ # @param status [Fixnum] the http code
19
+ #
20
+ # @since 0.1.0
21
+ #
22
+ # @see Hanami::Action::Throwable#halt
23
+ #
24
+ # @example With default status code (302)
25
+ # require 'hanami/controller'
26
+ #
27
+ # class Create
28
+ # include Hanami::Action
29
+ #
30
+ # def call(params)
31
+ # # ...
32
+ # redirect_to 'http://example.com/articles/23'
33
+ # end
34
+ # end
35
+ #
36
+ # action = Create.new
37
+ # action.call({}) # => [302, {'Location' => '/articles/23'}, '']
38
+ #
39
+ # @example With custom status code
40
+ # require 'hanami/controller'
41
+ #
42
+ # class Create
43
+ # include Hanami::Action
44
+ #
45
+ # def call(params)
46
+ # # ...
47
+ # redirect_to 'http://example.com/articles/23', status: 301
48
+ # end
49
+ # end
50
+ #
51
+ # action = Create.new
52
+ # action.call({}) # => [301, {'Location' => '/articles/23'}, '']
53
+ def redirect_to(url, status: 302)
54
+ headers[LOCATION] = ::String.new(url)
55
+ halt(status)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,86 @@
1
+ require 'rack/request'
2
+
3
+ module Hanami
4
+ module Action
5
+ # An HTTP request based on top of Rack::Request.
6
+ # This guarantees backwards compatibility with with Rack.
7
+ #
8
+ # @since 0.3.1
9
+ #
10
+ # @see http://www.rubydoc.info/gems/rack/Rack/Request
11
+ class Request < ::Rack::Request
12
+
13
+ # @raise [NotImplementedError]
14
+ #
15
+ # @since 0.3.1
16
+ # @api private
17
+ def content_type
18
+ raise NotImplementedError, 'Please use Action#content_type'
19
+ end
20
+
21
+ # @raise [NotImplementedError]
22
+ #
23
+ # @since 0.3.1
24
+ # @api private
25
+ def session
26
+ raise NotImplementedError, 'Please include Action::Session and use Action#session'
27
+ end
28
+
29
+ # @raise [NotImplementedError]
30
+ #
31
+ # @since 0.3.1
32
+ # @api private
33
+ def cookies
34
+ raise NotImplementedError, 'Please include Action::Cookies and use Action#cookies'
35
+ end
36
+
37
+ # @raise [NotImplementedError]
38
+ #
39
+ # @since 0.3.1
40
+ # @api private
41
+ def params
42
+ raise NotImplementedError, 'Please use params passed to Action#call'
43
+ end
44
+
45
+ # @raise [NotImplementedError]
46
+ #
47
+ # @since 0.3.1
48
+ # @api private
49
+ def update_param(*)
50
+ raise NotImplementedError, 'Please use params passed to Action#call'
51
+ end
52
+
53
+ # @raise [NotImplementedError]
54
+ #
55
+ # @since 0.3.1
56
+ # @api private
57
+ def delete_param(*)
58
+ raise NotImplementedError, 'Please use params passed to Action#call'
59
+ end
60
+
61
+ # @raise [NotImplementedError]
62
+ #
63
+ # @since 0.3.1
64
+ # @api private
65
+ def [](*)
66
+ raise NotImplementedError, 'Please use params passed to Action#call'
67
+ end
68
+
69
+ # @raise [NotImplementedError]
70
+ #
71
+ # @since 0.3.1
72
+ # @api private
73
+ def []=(*)
74
+ raise NotImplementedError, 'Please use params passed to Action#call'
75
+ end
76
+
77
+ # @raise [NotImplementedError]
78
+ #
79
+ # @since 0.3.1
80
+ # @api private
81
+ def values_at(*)
82
+ raise NotImplementedError, 'Please use params passed to Action#call'
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,154 @@
1
+ require 'hanami/action/flash'
2
+
3
+ module Hanami
4
+ module Action
5
+ # Session API
6
+ #
7
+ # This module isn't included by default.
8
+ #
9
+ # @since 0.1.0
10
+ module Session
11
+ # The key that returns raw session from the Rack env
12
+ #
13
+ # @since 0.1.0
14
+ # @api private
15
+ SESSION_KEY = 'rack.session'.freeze
16
+
17
+ # The key that is used by flash to transport errors
18
+ #
19
+ # @since 0.3.0
20
+ # @api private
21
+ ERRORS_KEY = :__errors
22
+
23
+ # Add session to default exposures
24
+ #
25
+ # @since 0.4.4
26
+ # @api private
27
+ def self.included(action)
28
+ action.class_eval do
29
+ expose :session
30
+ end
31
+ end
32
+
33
+ # Gets the session from the request and expose it as an Hash.
34
+ #
35
+ # @return [Hash] the HTTP session from the request
36
+ #
37
+ # @since 0.1.0
38
+ #
39
+ # @example
40
+ # require 'hanami/controller'
41
+ # require 'hanami/action/session'
42
+ #
43
+ # class Show
44
+ # include Hanami::Action
45
+ # include Hanami::Action::Session
46
+ #
47
+ # def call(params)
48
+ # # ...
49
+ #
50
+ # # get a value
51
+ # session[:user_id] # => '23'
52
+ #
53
+ # # set a value
54
+ # session[:foo] = 'bar'
55
+ #
56
+ # # remove a value
57
+ # session[:bax] = nil
58
+ # end
59
+ # end
60
+ def session
61
+ @_env[SESSION_KEY] ||= {}
62
+ end
63
+
64
+ private
65
+
66
+ # Container useful to transport data with the HTTP session
67
+ #
68
+ # @return [Hanami::Action::Flash] a Flash instance
69
+ #
70
+ # @since 0.3.0
71
+ # @api private
72
+ #
73
+ # @see Hanami::Action::Flash
74
+ def flash
75
+ @flash ||= Flash.new(session, request_id)
76
+ end
77
+
78
+ # In case of validations errors, preserve those informations after a
79
+ # redirect.
80
+ #
81
+ # @return [void]
82
+ #
83
+ # @since 0.3.0
84
+ # @api private
85
+ #
86
+ # @see Hanami::Action::Redirect#redirect_to
87
+ #
88
+ # @example
89
+ # require 'hanami/controller'
90
+ #
91
+ # module Comments
92
+ # class Index
93
+ # include Hanami::Action
94
+ # include Hanami::Action::Session
95
+ #
96
+ # expose :comments
97
+ #
98
+ # def call(params)
99
+ # @comments = CommentRepository.all
100
+ # end
101
+ # end
102
+ #
103
+ # class Create
104
+ # include Hanami::Action
105
+ # include Hanami::Action::Session
106
+ #
107
+ # params do
108
+ # param :text, type: String, presence: true
109
+ # end
110
+ #
111
+ # def call(params)
112
+ # comment = Comment.new(params)
113
+ # CommentRepository.create(comment) if params.valid?
114
+ #
115
+ # redirect_to '/comments'
116
+ # end
117
+ # end
118
+ # end
119
+ #
120
+ # # The validation errors caused by Comments::Create are available
121
+ # # **after the redirect** in the context of Comments::Index.
122
+ def redirect_to(*args)
123
+ flash[ERRORS_KEY] = errors.to_a unless params.valid?
124
+ super
125
+ end
126
+
127
+ # Read errors from flash or delegate to the superclass
128
+ #
129
+ # @return [Hanami::Validations::Errors] A collection of validation errors
130
+ #
131
+ # @since 0.3.0
132
+ # @api private
133
+ #
134
+ # @see Hanami::Action::Validatable
135
+ # @see Hanami::Action::Session#flash
136
+ def errors
137
+ flash[ERRORS_KEY] || super
138
+ end
139
+
140
+ # Finalize the response
141
+ #
142
+ # @return [void]
143
+ #
144
+ # @since 0.3.0
145
+ # @api private
146
+ #
147
+ # @see Hanami::Action#finish
148
+ def finish
149
+ super
150
+ flash.clear
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,194 @@
1
+ require 'hanami/utils/class_attribute'
2
+ require 'hanami/http/status'
3
+
4
+ module Hanami
5
+ module Action
6
+ # Throw API
7
+ #
8
+ # @since 0.1.0
9
+ #
10
+ # @see Hanami::Action::Throwable::ClassMethods#handle_exception
11
+ # @see Hanami::Action::Throwable#halt
12
+ # @see Hanami::Action::Throwable#status
13
+ module Throwable
14
+ # @since 0.2.0
15
+ # @api private
16
+ RACK_ERRORS = 'rack.errors'.freeze
17
+
18
+ # This isn't part of Rack SPEC
19
+ #
20
+ # Exception notifiers use <tt>rack.exception</tt> instead of
21
+ # <tt>rack.errors</tt>, so we need to support it.
22
+ #
23
+ # @since 0.5.0
24
+ # @api private
25
+ #
26
+ # @see Hanami::Action::Throwable::RACK_ERRORS
27
+ # @see http://www.rubydoc.info/github/rack/rack/file/SPEC#The_Error_Stream
28
+ # @see https://github.com/hanami/controller/issues/133
29
+ RACK_EXCEPTION = 'rack.exception'.freeze
30
+
31
+ def self.included(base)
32
+ base.extend ClassMethods
33
+ end
34
+
35
+ # Throw API class methods
36
+ #
37
+ # @since 0.1.0
38
+ # @api private
39
+ module ClassMethods
40
+ private
41
+
42
+ # Handle the given exception with an HTTP status code.
43
+ #
44
+ # When the exception is raise during #call execution, it will be
45
+ # translated into the associated HTTP status.
46
+ #
47
+ # This is a fine grained control, for a global configuration see
48
+ # Hanami::Action.handled_exceptions
49
+ #
50
+ # @param exception [Hash] the exception class must be the key and the
51
+ # HTTP status the value of the hash
52
+ #
53
+ # @since 0.1.0
54
+ #
55
+ # @see Hanami::Action.handled_exceptions
56
+ #
57
+ # @example
58
+ # require 'hanami/controller'
59
+ #
60
+ # class Show
61
+ # include Hanami::Action
62
+ # handle_exception RecordNotFound => 404
63
+ #
64
+ # def call(params)
65
+ # # ...
66
+ # raise RecordNotFound.new
67
+ # end
68
+ # end
69
+ #
70
+ # Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
71
+ def handle_exception(exception)
72
+ configuration.handle_exception(exception)
73
+ end
74
+ end
75
+
76
+ protected
77
+
78
+ # Halt the action execution with the given HTTP status code and message.
79
+ #
80
+ # When used, the execution of a callback or of an action is interrupted
81
+ # and the control returns to the framework, that decides how to handle
82
+ # the event.
83
+ #
84
+ # If a message is provided, it sets the response body with the message.
85
+ # Otherwise, it sets the response body with the default message associated
86
+ # to the code (eg 404 will set `"Not Found"`).
87
+ #
88
+ # @param code [Fixnum] a valid HTTP status code
89
+ # @param message [String] the response body
90
+ #
91
+ # @since 0.2.0
92
+ #
93
+ # @see Hanami::Controller#handled_exceptions
94
+ # @see Hanami::Action::Throwable#handle_exception
95
+ # @see Hanami::Http::Status:ALL
96
+ #
97
+ # @example Basic usage
98
+ # require 'hanami/controller'
99
+ #
100
+ # class Show
101
+ # def call(params)
102
+ # halt 404
103
+ # end
104
+ # end
105
+ #
106
+ # # => [404, {}, ["Not Found"]]
107
+ #
108
+ # @example Custom message
109
+ # require 'hanami/controller'
110
+ #
111
+ # class Show
112
+ # def call(params)
113
+ # halt 404, "This is not the droid you're looking for."
114
+ # end
115
+ # end
116
+ #
117
+ # # => [404, {}, ["This is not the droid you're looking for."]]
118
+ def halt(code, message = nil)
119
+ message ||= Http::Status.message_for(code)
120
+ status(code, message)
121
+
122
+ throw :halt
123
+ end
124
+
125
+ # Sets the given code and message for the response
126
+ #
127
+ # @param code [Fixnum] a valid HTTP status code
128
+ # @param message [String] the response body
129
+ #
130
+ # @since 0.1.0
131
+ # @see Hanami::Http::Status:ALL
132
+ def status(code, message)
133
+ self.status = code
134
+ self.body = message
135
+ end
136
+
137
+ private
138
+ # @since 0.1.0
139
+ # @api private
140
+ def _rescue
141
+ catch :halt do
142
+ begin
143
+ yield
144
+ rescue => exception
145
+ _reference_in_rack_errors(exception)
146
+ _handle_exception(exception)
147
+ end
148
+ end
149
+ end
150
+
151
+ # @since 0.2.0
152
+ # @api private
153
+ def _reference_in_rack_errors(exception)
154
+ return if configuration.handled_exception?(exception)
155
+
156
+ @_env[RACK_EXCEPTION] = exception
157
+
158
+ if errors = @_env[RACK_ERRORS]
159
+ errors.write(_dump_exception(exception))
160
+ errors.flush
161
+ end
162
+ end
163
+
164
+ # @since 0.2.0
165
+ # @api private
166
+ def _dump_exception(exception)
167
+ [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
168
+ end
169
+
170
+ # @since 0.1.0
171
+ # @api private
172
+ def _handle_exception(exception)
173
+ raise unless configuration.handle_exceptions
174
+
175
+ instance_exec(
176
+ exception,
177
+ &_exception_handler(exception)
178
+ )
179
+ end
180
+
181
+ # @since 0.3.0
182
+ # @api private
183
+ def _exception_handler(exception)
184
+ handler = configuration.exception_handler(exception)
185
+
186
+ if respond_to?(handler.to_s, true)
187
+ method(handler)
188
+ else
189
+ ->(ex) { halt handler }
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end