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,49 @@
1
+ require 'hanami/utils/class_attribute'
2
+
3
+ module Hanami
4
+ module Action
5
+ # Configuration API
6
+ #
7
+ # @since 0.2.0
8
+ #
9
+ # @see Hanami::Controller::Configuration
10
+ module Configurable
11
+ # Override Ruby's hook for modules.
12
+ # It includes configuration logic
13
+ #
14
+ # @param base [Class] the target action
15
+ #
16
+ # @since 0.2.0
17
+ # @api private
18
+ #
19
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
20
+ #
21
+ # @example
22
+ # require 'hanami/controller'
23
+ #
24
+ # class Show
25
+ # include Hanami::Action
26
+ # end
27
+ #
28
+ # Show.configuration
29
+ def self.included(base)
30
+ config = Hanami::Controller::Configuration.for(base)
31
+
32
+ base.class_eval do
33
+ include Utils::ClassAttribute
34
+
35
+ class_attribute :configuration
36
+ self.configuration = config
37
+ end
38
+
39
+ config.copy!(base)
40
+ end
41
+
42
+ private
43
+
44
+ def configuration
45
+ self.class.configuration
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,181 @@
1
+ require 'hanami/utils/hash'
2
+
3
+ module Hanami
4
+ module Action
5
+ # A set of HTTP Cookies
6
+ #
7
+ # It acts as an Hash
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @see Hanami::Action::Cookies#cookies
12
+ class CookieJar
13
+ # The key that returns raw cookies from the Rack env
14
+ #
15
+ # @since 0.1.0
16
+ # @api private
17
+ HTTP_HEADER = 'HTTP_COOKIE'.freeze
18
+
19
+ # The key used by Rack to set the session cookie
20
+ #
21
+ # We let CookieJar to NOT take care of this cookie, but it leaves the
22
+ # responsibility to the Rack middleware that handle sessions.
23
+ #
24
+ # This prevents <tt>Set-Cookie</tt> to be sent twice.
25
+ #
26
+ # @since 0.5.1
27
+ # @api private
28
+ #
29
+ # @see https://github.com/hanami/controller/issues/138
30
+ RACK_SESSION_KEY = :'rack.session'
31
+
32
+ # The key used by Rack to set the cookies as an Hash in the env
33
+ #
34
+ # @since 0.1.0
35
+ # @api private
36
+ COOKIE_HASH_KEY = 'rack.request.cookie_hash'.freeze
37
+
38
+ # The key used by Rack to set the cookies as a String in the env
39
+ #
40
+ # @since 0.1.0
41
+ # @api private
42
+ COOKIE_STRING_KEY = 'rack.request.cookie_string'.freeze
43
+
44
+ # @since 0.4.5
45
+ # @api private
46
+ COOKIE_SEPARATOR = ';,'.freeze
47
+
48
+ # Initialize the CookieJar
49
+ #
50
+ # @param env [Hash] a raw Rack env
51
+ # @param headers [Hash] the response headers
52
+ #
53
+ # @return [CookieJar]
54
+ #
55
+ # @since 0.1.0
56
+ def initialize(env, headers, default_options)
57
+ @_headers = headers
58
+ @cookies = Utils::Hash.new(extract(env)).symbolize!
59
+ @default_options = default_options
60
+ end
61
+
62
+ # Finalize itself, by setting the proper headers to add and remove
63
+ # cookies, before the response is returned to the webserver.
64
+ #
65
+ # @return [void]
66
+ #
67
+ # @since 0.1.0
68
+ #
69
+ # @see Hanami::Action::Cookies#finish
70
+ def finish
71
+ @cookies.delete(RACK_SESSION_KEY)
72
+ @cookies.each { |k,v| v.nil? ? delete_cookie(k) : set_cookie(k, _merge_default_values(v)) }
73
+ end
74
+
75
+ # Returns the object associated with the given key
76
+ #
77
+ # @param key [Symbol] the key
78
+ #
79
+ # @return [Object,nil] return the associated object, if found
80
+ #
81
+ # @since 0.2.0
82
+ def [](key)
83
+ @cookies[key]
84
+ end
85
+
86
+ # Associate the given value with the given key and store them
87
+ #
88
+ # @param key [Symbol] the key
89
+ # @param value [#to_s,Hash] value that can be serialized as a string or
90
+ # expressed as a Hash
91
+ # @option value [String] :domain - The domain
92
+ # @option value [String] :path - The path
93
+ # @option value [Integer] :max_age - Duration expressed in seconds
94
+ # @option value [Time] :expires - Expiration time
95
+ # @option value [TrueClass,FalseClass] :secure - Restrict cookie to secure
96
+ # connections
97
+ # @option value [TrueClass,FalseClass] :httponly - Restrict JavaScript
98
+ # access
99
+ #
100
+ # @return [void]
101
+ #
102
+ # @since 0.2.0
103
+ #
104
+ # @see http://en.wikipedia.org/wiki/HTTP_cookie
105
+ def []=(key, value)
106
+ @cookies[key] = value
107
+ end
108
+
109
+ private
110
+
111
+ # Merge default cookies options with values provided by user
112
+ #
113
+ # Cookies values provided by user are respected
114
+ #
115
+ # @since 0.4.0
116
+ # @api private
117
+ def _merge_default_values(value)
118
+ cookies_options = if value.is_a? Hash
119
+ value.merge! _add_expires_option(value)
120
+ else
121
+ { value: value }
122
+ end
123
+ @default_options.merge cookies_options
124
+ end
125
+
126
+ # Add expires option to cookies if :max_age presents
127
+ #
128
+ # @since 0.4.3
129
+ # @api private
130
+ def _add_expires_option(value)
131
+ if value.has_key?(:max_age) && !value.has_key?(:expires)
132
+ { expires: (Time.now + value[:max_age]) }
133
+ else
134
+ {}
135
+ end
136
+ end
137
+
138
+ # Extract the cookies from the raw Rack env.
139
+ #
140
+ # This implementation is borrowed from Rack::Request#cookies.
141
+ #
142
+ # @since 0.1.0
143
+ # @api private
144
+ def extract(env)
145
+ hash = env[COOKIE_HASH_KEY] ||= {}
146
+ string = env[HTTP_HEADER]
147
+
148
+ return hash if string == env[COOKIE_STRING_KEY]
149
+ # TODO Next Rack 1.7.x ?? version will have ::Rack::Utils.parse_cookies
150
+ # We can then replace the following lines.
151
+ hash.clear
152
+
153
+ # According to RFC 2109:
154
+ # If multiple cookies satisfy the criteria above, they are ordered in
155
+ # the Cookie header such that those with more specific Path attributes
156
+ # precede those with less specific. Ordering with respect to other
157
+ # attributes (e.g., Domain) is unspecified.
158
+ cookies = ::Rack::Utils.parse_query(string, COOKIE_SEPARATOR) { |s| ::Rack::Utils.unescape(s) rescue s }
159
+ cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
160
+ env[COOKIE_STRING_KEY] = string
161
+ hash
162
+ end
163
+
164
+ # Set a cookie in the headers
165
+ #
166
+ # @since 0.1.0
167
+ # @api private
168
+ def set_cookie(key, value)
169
+ ::Rack::Utils.set_cookie_header!(@_headers, key, value)
170
+ end
171
+
172
+ # Remove a cookie from the headers
173
+ #
174
+ # @since 0.1.0
175
+ # @api private
176
+ def delete_cookie(key)
177
+ ::Rack::Utils.delete_cookie_header!(@_headers, key, {})
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,85 @@
1
+ require 'hanami/action/cookie_jar'
2
+
3
+ module Hanami
4
+ module Action
5
+ # Cookies API
6
+ #
7
+ # This module isn't included by default.
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @see Hanami::Action::Cookies#cookies
12
+ module Cookies
13
+ protected
14
+
15
+ # Gets the cookies from the request and expose them as an Hash
16
+ #
17
+ # It automatically sets options from global configuration, but it allows to
18
+ # override values case by case.
19
+ #
20
+ # For a list of options please have a look at <tt>Hanami::Controller::Configuration</tt>,
21
+ # and <tt>Hanami::Action::CookieJar</tt>.
22
+ #
23
+ # @return [Hanami::Action::CookieJar] cookies
24
+ #
25
+ # @since 0.1.0
26
+ # @api public
27
+ #
28
+ # @see Hanami::Controller::Configuration#cookies
29
+ # @see Hanami::Action::CookieJar#[]=
30
+ #
31
+ # @example Basic Usage
32
+ # require 'hanami/controller'
33
+ # require 'hanami/action/cookies'
34
+ #
35
+ # class Show
36
+ # include Hanami::Action
37
+ # include Hanami::Action::Cookies
38
+ #
39
+ # def call(params)
40
+ # # ...
41
+ #
42
+ # # get a value
43
+ # cookies[:user_id] # => '23'
44
+ #
45
+ # # set a value
46
+ # cookies[:foo] = 'bar'
47
+ #
48
+ # # remove a value
49
+ # cookies[:bax] = nil
50
+ # end
51
+ # end
52
+ #
53
+ # @example Cookies Options
54
+ # require 'hanami/controller'
55
+ # require 'hanami/action/cookies'
56
+ #
57
+ # class Show
58
+ # include Hanami::Action
59
+ # include Hanami::Action::Cookies
60
+ #
61
+ # def call(params)
62
+ # # ...
63
+ # # set a value
64
+ # cookies[:foo] = { value: 'bar', max_age: 300, path: '/dashboard' }
65
+ # end
66
+ # end
67
+ def cookies
68
+ @cookies ||= CookieJar.new(@_env.dup, headers, configuration.cookies)
69
+ end
70
+
71
+ private
72
+
73
+ # Finalize the response by flushing cookies into the response
74
+ #
75
+ # @since 0.1.0
76
+ # @api private
77
+ #
78
+ # @see Hanami::Action#finish
79
+ def finish
80
+ super
81
+ cookies.finish
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,115 @@
1
+ module Hanami
2
+ module Action
3
+ # Exposures API
4
+ #
5
+ # @since 0.1.0
6
+ #
7
+ # @see Hanami::Action::Exposable::ClassMethods#expose
8
+ module Exposable
9
+ # Override Ruby's hook for modules.
10
+ # It includes exposures logic
11
+ #
12
+ # @param base [Class] the target action
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ #
17
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
18
+ def self.included(base)
19
+ base.extend ClassMethods
20
+ end
21
+
22
+ # Exposures API class methods
23
+ #
24
+ # @since 0.1.0
25
+ # @api private
26
+ module ClassMethods
27
+ # Expose the given attributes on the outside of the object with
28
+ # a getter and a special method called #exposures.
29
+ #
30
+ # @param names [Array<Symbol>] the name(s) of the attribute(s) to be
31
+ # exposed
32
+ #
33
+ # @return [void]
34
+ #
35
+ # @since 0.1.0
36
+ #
37
+ # @example
38
+ # require 'hanami/controller'
39
+ #
40
+ # class Show
41
+ # include Hanami::Action
42
+ #
43
+ # expose :article, :tags
44
+ #
45
+ # def call(params)
46
+ # @article = Article.find params[:id]
47
+ # @tags = Tag.for(article)
48
+ # end
49
+ # end
50
+ #
51
+ # action = Show.new
52
+ # action.call({id: 23})
53
+ #
54
+ # action.article # => #<Article ...>
55
+ # action.tags # => [#<Tag ...>, #<Tag ...>]
56
+ #
57
+ # action.exposures # => { :article => #<Article ...>, :tags => [ ... ] }
58
+ def expose(*names)
59
+ class_eval do
60
+ names.each do |name|
61
+ attr_reader(name) unless attr_reader?(name)
62
+ end
63
+
64
+ exposures.push(*names)
65
+ end
66
+ end
67
+
68
+ # Set of exposures attribute names
69
+ #
70
+ # @return [Array] the exposures attribute names
71
+ #
72
+ # @since 0.1.0
73
+ # @api private
74
+ def exposures
75
+ @exposures ||= []
76
+ end
77
+
78
+ private
79
+ # Check if the attr_reader is already defined
80
+ #
81
+ # @since 0.3.0
82
+ # @api private
83
+ def attr_reader?(name)
84
+ (instance_methods | private_instance_methods).include?(name)
85
+ end
86
+ end
87
+
88
+ # Set of exposures
89
+ #
90
+ # @return [Hash] the exposures
91
+ #
92
+ # @since 0.1.0
93
+ #
94
+ # @see Hanami::Action::Exposable::ClassMethods.expose
95
+ def exposures
96
+ @exposures ||= {}.tap do |result|
97
+ self.class.exposures.each do |name|
98
+ result[name] = send(name)
99
+ end
100
+ end
101
+ end
102
+
103
+ # Finalize the response
104
+ #
105
+ # @since 0.3.0
106
+ # @api private
107
+ #
108
+ # @see Hanami::Action#finish
109
+ def finish
110
+ super
111
+ exposures
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,182 @@
1
+ module Hanami
2
+ module Action
3
+ # Container useful to transport data with the HTTP session
4
+ # It has a life span of one HTTP request or redirect.
5
+ #
6
+ # @since 0.3.0
7
+ # @api private
8
+ class Flash
9
+ # Session key where the data is stored
10
+ #
11
+ # @since 0.3.0
12
+ # @api private
13
+ SESSION_KEY = :__flash
14
+
15
+ # Session key where the last request_id is stored
16
+ #
17
+ # @since 0.4.0
18
+ # @api private
19
+ LAST_REQUEST_KEY = :__last_request_id
20
+
21
+ # Initialize a new Flash instance
22
+ #
23
+ # @param session [Rack::Session::Abstract::SessionHash] the session
24
+ # @param request_id [String] the HTTP Request ID
25
+ #
26
+ # @return [Hanami::Action::Flash] the flash
27
+ #
28
+ # @see http://www.rubydoc.info/gems/rack/Rack/Session/Abstract/SessionHash
29
+ # @see Hanami::Action::Rack#session_id
30
+ def initialize(session, request_id)
31
+ @session = session
32
+ @request_id = request_id
33
+ @last_request_id = session[LAST_REQUEST_KEY]
34
+
35
+ session[SESSION_KEY] ||= {}
36
+ session[SESSION_KEY][request_id] ||= {}
37
+ end
38
+
39
+ # Set the given value for the given key
40
+ #
41
+ # @param key [#to_s] the key
42
+ # @param value [Object] the value
43
+ #
44
+ # @since 0.3.0
45
+ # @api private
46
+ def []=(key, value)
47
+ data[key] = value
48
+ end
49
+
50
+ # Get the value associated to the given key, if any
51
+ #
52
+ # @return [Object,NilClass] the value
53
+ #
54
+ # @since 0.3.0
55
+ # @api private
56
+ def [](key)
57
+ last_request_flash.merge(data).fetch(key) do
58
+ _values.find {|data| !data[key].nil? }
59
+ end
60
+ end
61
+
62
+ # Removes entirely the flash from the session if it has stale contents
63
+ # or if empty.
64
+ #
65
+ # @return [void]
66
+ #
67
+ # @since 0.3.0
68
+ # @api private
69
+ def clear
70
+ # FIXME we're just before a release and I can't find a proper way to reproduce
71
+ # this bug that I've found via a browser.
72
+ #
73
+ # It may happen that `#flash` is nil, and those two methods will fail
74
+ unless flash.nil?
75
+ expire_stale!
76
+ set_last_request_id!
77
+ remove!
78
+ end
79
+ end
80
+
81
+ # Check if there are contents stored in the flash from the current or the
82
+ # previous request.
83
+ #
84
+ # @return [TrueClass,FalseClass] the result of the check
85
+ #
86
+ # @since 0.3.0
87
+ # @api private
88
+ def empty?
89
+ _values.all?(&:empty?)
90
+ end
91
+
92
+ private
93
+
94
+ # The flash registry that holds the data for **all** the recent requests
95
+ #
96
+ # @return [Hash] the flash
97
+ #
98
+ # @since 0.3.0
99
+ # @api private
100
+ def flash
101
+ @session[SESSION_KEY] || {}
102
+ end
103
+
104
+ # The flash registry that holds the data **only for** the current request
105
+ #
106
+ # @return [Hash] the flash for the current request
107
+ #
108
+ # @since 0.3.0
109
+ # @api private
110
+ def data
111
+ flash[@request_id] || {}
112
+ end
113
+
114
+ # Expire the stale data from the previous request.
115
+ #
116
+ # @return [void]
117
+ #
118
+ # @since 0.3.0
119
+ # @api private
120
+ def expire_stale!
121
+ flash.each do |request_id, _|
122
+ flash.delete(request_id) if delete?(request_id)
123
+ end
124
+ end
125
+
126
+ # Remove the flash entirely from the session if empty.
127
+ #
128
+ # @return [void]
129
+ #
130
+ # @since 0.3.0
131
+ # @api private
132
+ #
133
+ # @see Hanami::Action::Flash#empty?
134
+ def remove!
135
+ @session.delete(SESSION_KEY) if empty?
136
+ end
137
+
138
+ # Values from all the stored requests
139
+ #
140
+ # @return [Array]
141
+ #
142
+ # @since 0.3.0
143
+ # @api private
144
+ def _values
145
+ flash.values
146
+ end
147
+
148
+ # Determine if delete data from flash for the given Request ID
149
+ #
150
+ # @return [TrueClass,FalseClass] the result of the check
151
+ #
152
+ # @since 0.4.0
153
+ # @api private
154
+ #
155
+ # @see Hanami::Action::Flash#expire_stale!
156
+ def delete?(request_id)
157
+ ![@request_id, @session[LAST_REQUEST_KEY]].include?(request_id)
158
+ end
159
+
160
+ # Get the last request session flash
161
+ #
162
+ # @return [Hash] the flash of last request
163
+ #
164
+ # @since 0.4.0
165
+ # @api private
166
+ def last_request_flash
167
+ flash[@last_request_id] || {}
168
+ end
169
+
170
+ # Store the last request_id to create the next flash with its values
171
+ # is current flash is not empty.
172
+ #
173
+ # @return [void]
174
+ # @since 0.4.0
175
+ # @api private
176
+ def set_last_request_id!
177
+ @session[LAST_REQUEST_KEY] = @request_id if !empty?
178
+ end
179
+
180
+ end
181
+ end
182
+ end