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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/LICENSE.md +22 -0
- data/README.md +1180 -9
- data/hanami-controller.gemspec +19 -12
- data/lib/hanami-controller.rb +1 -0
- data/lib/hanami/action.rb +85 -0
- data/lib/hanami/action/cache.rb +174 -0
- data/lib/hanami/action/cache/cache_control.rb +70 -0
- data/lib/hanami/action/cache/conditional_get.rb +93 -0
- data/lib/hanami/action/cache/directives.rb +99 -0
- data/lib/hanami/action/cache/expires.rb +73 -0
- data/lib/hanami/action/callable.rb +94 -0
- data/lib/hanami/action/callbacks.rb +210 -0
- data/lib/hanami/action/configurable.rb +49 -0
- data/lib/hanami/action/cookie_jar.rb +181 -0
- data/lib/hanami/action/cookies.rb +85 -0
- data/lib/hanami/action/exposable.rb +115 -0
- data/lib/hanami/action/flash.rb +182 -0
- data/lib/hanami/action/glue.rb +66 -0
- data/lib/hanami/action/head.rb +122 -0
- data/lib/hanami/action/mime.rb +493 -0
- data/lib/hanami/action/params.rb +285 -0
- data/lib/hanami/action/rack.rb +270 -0
- data/lib/hanami/action/rack/callable.rb +47 -0
- data/lib/hanami/action/rack/file.rb +33 -0
- data/lib/hanami/action/redirect.rb +59 -0
- data/lib/hanami/action/request.rb +86 -0
- data/lib/hanami/action/session.rb +154 -0
- data/lib/hanami/action/throwable.rb +194 -0
- data/lib/hanami/action/validatable.rb +128 -0
- data/lib/hanami/controller.rb +250 -2
- data/lib/hanami/controller/configuration.rb +705 -0
- data/lib/hanami/controller/error.rb +7 -0
- data/lib/hanami/controller/version.rb +4 -1
- data/lib/hanami/http/status.rb +62 -0
- metadata +124 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
data/hanami-controller.gemspec
CHANGED
@@ -4,20 +4,27 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'hanami/controller/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'hanami-controller'
|
8
8
|
spec.version = Hanami::Controller::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Luca Guidi', 'Trung Lê', 'Alfonso Uceda']
|
10
|
+
spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com', 'uceda73@gmail.com']
|
11
|
+
spec.description = %q{Complete, fast and testable actions for Rack}
|
12
|
+
spec.summary = %q{Complete, fast and testable actions for Rack and Hanami}
|
13
|
+
spec.homepage = 'http://hanamirb.org'
|
14
|
+
spec.license = 'MIT'
|
11
15
|
|
12
|
-
spec.
|
13
|
-
spec.
|
14
|
-
spec.
|
16
|
+
spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-controller.gemspec`.split($/)
|
17
|
+
spec.executables = []
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
spec.required_ruby_version = '>= 2.0.0'
|
15
21
|
|
16
|
-
spec.
|
17
|
-
spec.
|
18
|
-
spec.
|
19
|
-
spec.require_paths = ["lib"]
|
22
|
+
spec.add_dependency 'rack', '~> 1.6', '>= 1.6.2'
|
23
|
+
spec.add_dependency 'hanami-utils', '~> 0.7'
|
24
|
+
spec.add_dependency 'hanami-validations', '~> 0.5'
|
20
25
|
|
21
|
-
spec.add_development_dependency
|
22
|
-
spec.add_development_dependency
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
27
|
+
spec.add_development_dependency 'minitest', '~> 5'
|
28
|
+
spec.add_development_dependency 'rack-test', '~> 0.6'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10'
|
23
30
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'hanami/controller'
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'hanami/action/configurable'
|
2
|
+
require 'hanami/action/rack'
|
3
|
+
require 'hanami/action/mime'
|
4
|
+
require 'hanami/action/redirect'
|
5
|
+
require 'hanami/action/exposable'
|
6
|
+
require 'hanami/action/throwable'
|
7
|
+
require 'hanami/action/callbacks'
|
8
|
+
require 'hanami/action/validatable'
|
9
|
+
require 'hanami/action/head'
|
10
|
+
require 'hanami/action/callable'
|
11
|
+
|
12
|
+
module Hanami
|
13
|
+
# An HTTP endpoint
|
14
|
+
#
|
15
|
+
# @since 0.1.0
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# require 'hanami/controller'
|
19
|
+
#
|
20
|
+
# class Show
|
21
|
+
# include Hanami::Action
|
22
|
+
#
|
23
|
+
# def call(params)
|
24
|
+
# # ...
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
module Action
|
28
|
+
# Override Ruby's hook for modules.
|
29
|
+
# It includes basic Hanami::Action modules to the given class.
|
30
|
+
#
|
31
|
+
# @param base [Class] the target action
|
32
|
+
#
|
33
|
+
# @since 0.1.0
|
34
|
+
# @api private
|
35
|
+
#
|
36
|
+
# @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
|
37
|
+
#
|
38
|
+
# @see Hanami::Action::Rack
|
39
|
+
# @see Hanami::Action::Mime
|
40
|
+
# @see Hanami::Action::Http
|
41
|
+
# @see Hanami::Action::Redirect
|
42
|
+
# @see Hanami::Action::Exposable
|
43
|
+
# @see Hanami::Action::Throwable
|
44
|
+
# @see Hanami::Action::Callbacks
|
45
|
+
# @see Hanami::Action::Validatable
|
46
|
+
# @see Hanami::Action::Configurable
|
47
|
+
# @see Hanami::Action::Callable
|
48
|
+
def self.included(base)
|
49
|
+
base.class_eval do
|
50
|
+
include Rack
|
51
|
+
include Mime
|
52
|
+
include Redirect
|
53
|
+
include Exposable
|
54
|
+
include Throwable
|
55
|
+
include Callbacks
|
56
|
+
include Validatable
|
57
|
+
include Configurable
|
58
|
+
include Head
|
59
|
+
prepend Callable
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Finalize the response
|
66
|
+
#
|
67
|
+
# This method is abstract and COULD be implemented by included modules in
|
68
|
+
# order to prepare their data before the reponse will be returned to the
|
69
|
+
# webserver.
|
70
|
+
#
|
71
|
+
# @since 0.1.0
|
72
|
+
# @api private
|
73
|
+
# @abstract
|
74
|
+
#
|
75
|
+
# @see Hanami::Action::Mime#finish
|
76
|
+
# @see Hanami::Action::Exposable#finish
|
77
|
+
# @see Hanami::Action::Callable#finish
|
78
|
+
# @see Hanami::Action::Session#finish
|
79
|
+
# @see Hanami::Action::Cookies#finish
|
80
|
+
# @see Hanami::Action::Cache#finish
|
81
|
+
# @see Hanami::Action::Head#finish
|
82
|
+
def finish
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'hanami/action/cache/cache_control'
|
2
|
+
require 'hanami/action/cache/expires'
|
3
|
+
require 'hanami/action/cache/conditional_get'
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Action
|
7
|
+
# Cache type API
|
8
|
+
#
|
9
|
+
# @since 0.3.0
|
10
|
+
#
|
11
|
+
# @see Hanami::Action::Cache::ClassMethods#cache_control
|
12
|
+
# @see Hanami::Action::Cache::ClassMethods#expires
|
13
|
+
# @see Hanami::Action::Cache::ClassMethods#fresh
|
14
|
+
module Cache
|
15
|
+
# Override Ruby's hook for modules.
|
16
|
+
# It includes exposures logic
|
17
|
+
#
|
18
|
+
# @param base [Class] the target action
|
19
|
+
#
|
20
|
+
# @since 0.3.0
|
21
|
+
# @api private
|
22
|
+
#
|
23
|
+
# @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
|
24
|
+
def self.included(base)
|
25
|
+
base.class_eval do
|
26
|
+
include CacheControl, Expires
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
# Specify response freshness policy for HTTP caches (Cache-Control header).
|
33
|
+
# Any number of non-value directives (:public, :private, :no_cache,
|
34
|
+
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
|
35
|
+
# a Hash of value directives (:max_age, :min_stale, :s_max_age).
|
36
|
+
#
|
37
|
+
# See RFC 2616 / 14.9 for more on standard cache control directives:
|
38
|
+
# http://tools.ietf.org/html/rfc2616#section-14.9.1
|
39
|
+
#
|
40
|
+
# @param values [Array<Symbols, Hash>] mapped to cache_control directives
|
41
|
+
# @option values [Symbol] :public
|
42
|
+
# @option values [Symbol] :private
|
43
|
+
# @option values [Symbol] :no_cache
|
44
|
+
# @option values [Symbol] :no_store
|
45
|
+
# @option values [Symbol] :must_validate
|
46
|
+
# @option values [Symbol] :proxy_revalidate
|
47
|
+
# @option values [Hash] :max_age
|
48
|
+
# @option values [Hash] :min_stale
|
49
|
+
# @option values [Hash] :s_max_age
|
50
|
+
#
|
51
|
+
# @return void
|
52
|
+
#
|
53
|
+
# @since 0.3.0
|
54
|
+
# @api public
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# require 'hanami/controller'
|
58
|
+
# require 'hanami/action/cache'
|
59
|
+
#
|
60
|
+
# class Show
|
61
|
+
# include Hanami::Action
|
62
|
+
# include Hanami::Action::Cache
|
63
|
+
#
|
64
|
+
# def call(params)
|
65
|
+
# # ...
|
66
|
+
#
|
67
|
+
# # set Cache-Control directives
|
68
|
+
# cache_control :public, max_age: 900, s_maxage: 86400
|
69
|
+
#
|
70
|
+
# # overwrite previous Cache-Control directives
|
71
|
+
# cache_control :private, :no_cache, :no_store
|
72
|
+
#
|
73
|
+
# => Cache-Control: private, no-store, max-age=900
|
74
|
+
#
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
def cache_control(*values)
|
79
|
+
cache_control = CacheControl::Directives.new(*values)
|
80
|
+
headers.merge!(cache_control.headers)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Set the Expires header and Cache-Control/max-age directive. Amount
|
84
|
+
# can be an integer number of seconds in the future or a Time object
|
85
|
+
# indicating when the response should be considered "stale". The remaining
|
86
|
+
# "values" arguments are passed to the #cache_control helper:
|
87
|
+
#
|
88
|
+
# @param amount [Integer,Time] number of seconds or point in time
|
89
|
+
# @param values [Array<Symbols>] mapped to cache_control directives
|
90
|
+
#
|
91
|
+
# @return void
|
92
|
+
#
|
93
|
+
# @since 0.3.0
|
94
|
+
# @api public
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# require 'hanami/controller'
|
98
|
+
# require 'hanami/action/cache'
|
99
|
+
#
|
100
|
+
# class Show
|
101
|
+
# include Hanami::Action
|
102
|
+
# include Hanami::Action::Cache
|
103
|
+
#
|
104
|
+
# def call(params)
|
105
|
+
# # ...
|
106
|
+
#
|
107
|
+
# # set Cache-Control directives and Expires
|
108
|
+
# expires 900, :public
|
109
|
+
#
|
110
|
+
# # overwrite Cache-Control directives and Expires
|
111
|
+
# expires 300, :private, :no_cache, :no_store
|
112
|
+
#
|
113
|
+
# => Expires: Thu, 26 Jun 2014 12:00:00 GMT
|
114
|
+
# => Cache-Control: private, no-cache, no-store max-age=300
|
115
|
+
#
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
def expires(amount, *values)
|
120
|
+
expires = Expires::Directives.new(amount, *values)
|
121
|
+
headers.merge!(expires.headers)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Set the etag, last_modified, or both headers on the response
|
125
|
+
# and halts a 304 Not Modified if the request is still fresh
|
126
|
+
# respecting IfNoneMatch and IfModifiedSince request headers
|
127
|
+
#
|
128
|
+
# @param options [Hash]
|
129
|
+
# @option options [Integer] :etag for testing IfNoneMatch conditions
|
130
|
+
# @option options [Date] :last_modified for testing IfModifiedSince conditions
|
131
|
+
#
|
132
|
+
# @return void
|
133
|
+
#
|
134
|
+
# @since 0.3.0
|
135
|
+
# @api public
|
136
|
+
#
|
137
|
+
# @example
|
138
|
+
# require 'hanami/controller'
|
139
|
+
# require 'hanami/action/cache'
|
140
|
+
#
|
141
|
+
# class Show
|
142
|
+
# include Hanami::Action
|
143
|
+
# include Hanami::Action::Cache
|
144
|
+
#
|
145
|
+
# def call(params)
|
146
|
+
# # ...
|
147
|
+
#
|
148
|
+
# # set etag response header and halt 304
|
149
|
+
# # if request matches IF_NONE_MATCH header
|
150
|
+
# fresh etag: @resource.updated_at.to_i
|
151
|
+
#
|
152
|
+
# # set last_modified response header and halt 304
|
153
|
+
# # if request matches IF_MODIFIED_SINCE
|
154
|
+
# fresh last_modified: @resource.updated_at
|
155
|
+
#
|
156
|
+
# # set etag and last_modified response header,
|
157
|
+
# # halt 304 if request matches IF_MODIFIED_SINCE
|
158
|
+
# # and IF_NONE_MATCH
|
159
|
+
# fresh last_modified: @resource.updated_at
|
160
|
+
#
|
161
|
+
# end
|
162
|
+
# end
|
163
|
+
def fresh(options)
|
164
|
+
conditional_get = ConditionalGet.new(@_env, options)
|
165
|
+
|
166
|
+
headers.merge!(conditional_get.headers)
|
167
|
+
|
168
|
+
conditional_get.fresh? do
|
169
|
+
halt 304
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'hanami/action/cache/directives'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Action
|
5
|
+
module Cache
|
6
|
+
|
7
|
+
# Module with Cache-Control logic
|
8
|
+
#
|
9
|
+
# @since 0.3.0
|
10
|
+
# @api private
|
11
|
+
module CacheControl
|
12
|
+
|
13
|
+
# The HTTP header for Cache-Control
|
14
|
+
#
|
15
|
+
# @since 0.3.0
|
16
|
+
# @api private
|
17
|
+
HEADER = 'Cache-Control'.freeze
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
base.extend ClassMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def cache_control(*values)
|
25
|
+
@cache_control_directives ||= Directives.new(*values)
|
26
|
+
end
|
27
|
+
|
28
|
+
def cache_control_directives
|
29
|
+
@cache_control_directives || Object.new.tap do |null_object|
|
30
|
+
def null_object.headers
|
31
|
+
Hash.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Finalize the response including default cache headers into the response
|
38
|
+
#
|
39
|
+
# @since 0.3.0
|
40
|
+
# @api private
|
41
|
+
#
|
42
|
+
# @see Hanami::Action#finish
|
43
|
+
def finish
|
44
|
+
super
|
45
|
+
headers.merge!(self.class.cache_control_directives.headers) unless headers.include? HEADER
|
46
|
+
end
|
47
|
+
|
48
|
+
# Class which stores CacheControl values
|
49
|
+
#
|
50
|
+
# @since 0.3.0
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
#
|
54
|
+
class Directives
|
55
|
+
def initialize(*values)
|
56
|
+
@directives = Hanami::Action::Cache::Directives.new(*values)
|
57
|
+
end
|
58
|
+
|
59
|
+
def headers
|
60
|
+
if @directives.any?
|
61
|
+
{ HEADER => @directives.join(', ') }
|
62
|
+
else
|
63
|
+
{}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Action
|
3
|
+
module Cache
|
4
|
+
# @since 0.3.0
|
5
|
+
# @api private
|
6
|
+
IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
|
7
|
+
|
8
|
+
# The HTTP header for ETag
|
9
|
+
#
|
10
|
+
# @since 0.3.0
|
11
|
+
# @api private
|
12
|
+
ETAG = 'ETag'.freeze
|
13
|
+
|
14
|
+
# @since 0.3.0
|
15
|
+
# @api private
|
16
|
+
IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
|
17
|
+
|
18
|
+
# The HTTP header for Last-Modified
|
19
|
+
#
|
20
|
+
# @since 0.3.0
|
21
|
+
# @api private
|
22
|
+
LAST_MODIFIED = 'Last-Modified'.freeze
|
23
|
+
|
24
|
+
# ETag value object
|
25
|
+
#
|
26
|
+
# @since 0.3.0
|
27
|
+
# @api private
|
28
|
+
#
|
29
|
+
class ETag
|
30
|
+
def initialize(env, value)
|
31
|
+
@env, @value = env, value
|
32
|
+
end
|
33
|
+
|
34
|
+
def fresh?
|
35
|
+
none_match && @value == none_match
|
36
|
+
end
|
37
|
+
|
38
|
+
def header
|
39
|
+
{ ETAG => @value } if none_match
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def none_match
|
45
|
+
@env[IF_NONE_MATCH]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# LastModified value object
|
50
|
+
#
|
51
|
+
# @since 0.3.0
|
52
|
+
# @api private
|
53
|
+
class LastModified
|
54
|
+
def initialize(env, value)
|
55
|
+
@env, @value = env, value
|
56
|
+
end
|
57
|
+
|
58
|
+
def fresh?
|
59
|
+
modified_since && Time.httpdate(modified_since).to_i >= @value.to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
def header
|
63
|
+
{ LAST_MODIFIED => @value.httpdate } if modified_since
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def modified_since
|
69
|
+
@env[IF_MODIFIED_SINCE]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Class responsible to determine if a given request is fresh
|
74
|
+
# based on IF_NONE_MATCH and IF_MODIFIED_SINCE headers
|
75
|
+
#
|
76
|
+
# @since 0.3.0
|
77
|
+
# @api private
|
78
|
+
class ConditionalGet
|
79
|
+
def initialize(env, options)
|
80
|
+
@validations = [ ETag.new(env, options[:etag]), LastModified.new(env, options[:last_modified]) ]
|
81
|
+
end
|
82
|
+
|
83
|
+
def fresh?
|
84
|
+
yield if @validations.any?(&:fresh?)
|
85
|
+
end
|
86
|
+
|
87
|
+
def headers
|
88
|
+
@validations.map(&:header).compact.reduce Hash.new, :merge
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|