hanami-controller 0.0.0 → 0.6.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.
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