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,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