hanami-action 3.0.0.rc1
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 +7 -0
- data/CHANGELOG.md +985 -0
- data/LICENSE +20 -0
- data/README.md +873 -0
- data/hanami-action.gemspec +39 -0
- data/lib/hanami/action/body_parser/json.rb +20 -0
- data/lib/hanami/action/body_parser/multipart_form.rb +22 -0
- data/lib/hanami/action/body_parser.rb +109 -0
- data/lib/hanami/action/cache/cache_control.rb +84 -0
- data/lib/hanami/action/cache/conditional_get.rb +101 -0
- data/lib/hanami/action/cache/directives.rb +126 -0
- data/lib/hanami/action/cache/expires.rb +84 -0
- data/lib/hanami/action/cache.rb +29 -0
- data/lib/hanami/action/config/formats.rb +256 -0
- data/lib/hanami/action/config.rb +172 -0
- data/lib/hanami/action/constants.rb +283 -0
- data/lib/hanami/action/cookie_jar.rb +214 -0
- data/lib/hanami/action/cookies.rb +27 -0
- data/lib/hanami/action/csrf_protection.rb +217 -0
- data/lib/hanami/action/errors.rb +109 -0
- data/lib/hanami/action/flash.rb +176 -0
- data/lib/hanami/action/halt.rb +18 -0
- data/lib/hanami/action/mime/request_mime_weight.rb +66 -0
- data/lib/hanami/action/mime.rb +438 -0
- data/lib/hanami/action/params.rb +342 -0
- data/lib/hanami/action/rack/file.rb +41 -0
- data/lib/hanami/action/rack_utils.rb +11 -0
- data/lib/hanami/action/request/session.rb +68 -0
- data/lib/hanami/action/request.rb +141 -0
- data/lib/hanami/action/response.rb +481 -0
- data/lib/hanami/action/session.rb +47 -0
- data/lib/hanami/action/validatable.rb +166 -0
- data/lib/hanami/action/version.rb +13 -0
- data/lib/hanami/action/view_name_inferrer.rb +56 -0
- data/lib/hanami/action.rb +672 -0
- data/lib/hanami/http/status.rb +149 -0
- data/lib/hanami-action.rb +3 -0
- metadata +153 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is synced from hanakai-rb/repo-sync. To update it, edit repo-sync.yml.
|
|
4
|
+
|
|
5
|
+
lib = File.expand_path("lib", __dir__)
|
|
6
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
7
|
+
require "hanami/action/version"
|
|
8
|
+
|
|
9
|
+
Gem::Specification.new do |spec|
|
|
10
|
+
spec.name = "hanami-action"
|
|
11
|
+
spec.authors = ["Hanakai team"]
|
|
12
|
+
spec.email = ["info@hanakai.org"]
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.version = Hanami::Action::VERSION.dup
|
|
15
|
+
|
|
16
|
+
spec.summary = "Complete, fast and testable actions for Rack and Hanami"
|
|
17
|
+
spec.description = spec.summary
|
|
18
|
+
spec.homepage = "https://hanamirb.org"
|
|
19
|
+
spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "hanami-action.gemspec", "lib/**/*"]
|
|
20
|
+
spec.bindir = "exe"
|
|
21
|
+
spec.executables = Dir["exe/*"].map { |f| File.basename(f) }
|
|
22
|
+
spec.require_paths = ["lib"]
|
|
23
|
+
|
|
24
|
+
spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE"]
|
|
25
|
+
|
|
26
|
+
spec.metadata["changelog_uri"] = "https://github.com/hanami/hanami-action/blob/main/CHANGELOG.md"
|
|
27
|
+
spec.metadata["source_code_uri"] = "https://github.com/hanami/hanami-action"
|
|
28
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/hanami/hanami-action/issues"
|
|
29
|
+
spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami"
|
|
30
|
+
|
|
31
|
+
spec.required_ruby_version = ">= 3.3"
|
|
32
|
+
|
|
33
|
+
spec.add_runtime_dependency "rack", ">= 2.2.16"
|
|
34
|
+
spec.add_runtime_dependency "hanami-utils", "~> 3.0.0.rc"
|
|
35
|
+
spec.add_runtime_dependency "dry-configurable", "~> 1.4"
|
|
36
|
+
spec.add_runtime_dependency "dry-core", "~> 1.0"
|
|
37
|
+
spec.add_runtime_dependency "zeitwerk", "~> 2.6"
|
|
38
|
+
end
|
|
39
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
class Action
|
|
7
|
+
module BodyParser
|
|
8
|
+
# Body parser for JSON request bodies.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module JSON
|
|
12
|
+
def self.call(body, _env)
|
|
13
|
+
::JSON.parse(body)
|
|
14
|
+
rescue ::JSON::ParserError => exception
|
|
15
|
+
raise BodyParsingError, exception.message
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/multipart"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
class Action
|
|
7
|
+
module BodyParser
|
|
8
|
+
# Body parser for multipart form data (file uploads).
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module MultipartForm
|
|
12
|
+
def self.call(_body, env)
|
|
13
|
+
# Rack's `parse_multipart` reads the input from the env. We've already rewound this input
|
|
14
|
+
# in BodyParser, before this parser is called.
|
|
15
|
+
::Rack::Multipart.parse_multipart(env)
|
|
16
|
+
rescue StandardError => exception
|
|
17
|
+
raise BodyParsingError, exception.message
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "hanami/utils/hash"
|
|
5
|
+
|
|
6
|
+
module Hanami
|
|
7
|
+
class Action
|
|
8
|
+
# Parses request bodies based on the action's accepted formats.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module BodyParser
|
|
12
|
+
FALLBACK_KEY = :_
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
16
|
+
|
|
17
|
+
# Parses the request body if applicable
|
|
18
|
+
#
|
|
19
|
+
# @param env [Hash] Rack environment
|
|
20
|
+
# @param config [Hanami::Action::Config] action configuration
|
|
21
|
+
#
|
|
22
|
+
# @return [void]
|
|
23
|
+
def parse(env, config)
|
|
24
|
+
# If the router has already parsed the body, derive our own keys from it.
|
|
25
|
+
if env.key?(ROUTER_PARSED_BODY)
|
|
26
|
+
parsed = env[ROUTER_PARSED_BODY]
|
|
27
|
+
env[ACTION_PARSED_BODY] = parsed
|
|
28
|
+
env[ACTION_BODY_PARAMS] = symbolize_body(parsed)
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
return if env.key?(ACTION_PARSED_BODY)
|
|
33
|
+
|
|
34
|
+
input = env[::Rack::RACK_INPUT]
|
|
35
|
+
return unless input
|
|
36
|
+
|
|
37
|
+
media_type = Mime.extract_media_type(env[CONTENT_TYPE])
|
|
38
|
+
return unless media_type
|
|
39
|
+
|
|
40
|
+
if config.formats.empty?
|
|
41
|
+
# When no format is explicity configured, parse multipart/form-data bodies as a sensible
|
|
42
|
+
# default. These kinds of form submissions are a standard part of standard web behavior,
|
|
43
|
+
# and users expect them to work out of the box.
|
|
44
|
+
return unless media_type == "multipart/form-data"
|
|
45
|
+
else
|
|
46
|
+
return unless Mime.accepted_content_type?(media_type, config)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
parser = config.formats.body_parser_for(media_type)
|
|
50
|
+
return unless parser
|
|
51
|
+
|
|
52
|
+
input = ensure_rewindable_input(env)
|
|
53
|
+
body = read_body(input)
|
|
54
|
+
return if body.nil? || body.empty?
|
|
55
|
+
|
|
56
|
+
# Pass both the body string and the Rack env to the parser. Most parsers should only need
|
|
57
|
+
# the body, but the env is there in case access to headers or calling Rack APIs is
|
|
58
|
+
# required.
|
|
59
|
+
parsed = parser.call(body, env)
|
|
60
|
+
|
|
61
|
+
env[ACTION_PARSED_BODY] = parsed
|
|
62
|
+
env[ACTION_BODY_PARAMS] = symbolize_body(parsed)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Ensures the input in the Rack env is rewindable (for Rack 3 compatibility).
|
|
70
|
+
def ensure_rewindable_input(env)
|
|
71
|
+
input = env[::Rack::RACK_INPUT]
|
|
72
|
+
return input if input.respond_to?(:rewind)
|
|
73
|
+
|
|
74
|
+
env[::Rack::RACK_INPUT] = ::Rack::RewindableInput.new(input)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Reads and rewinds the body.
|
|
78
|
+
def read_body(input)
|
|
79
|
+
input.rewind
|
|
80
|
+
body = input.read
|
|
81
|
+
input.rewind
|
|
82
|
+
|
|
83
|
+
body
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Symbolizes the parsed body, wrapping non-hash values in a fallback key.
|
|
87
|
+
def symbolize_body(parsed)
|
|
88
|
+
if parsed.is_a?(::Hash)
|
|
89
|
+
deep_symbolize(parsed)
|
|
90
|
+
else
|
|
91
|
+
{FALLBACK_KEY => deep_symbolize(parsed)}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Recursively symbolizes hash keys within any structure (arrays or hashes).
|
|
96
|
+
def deep_symbolize(value)
|
|
97
|
+
case value
|
|
98
|
+
when ::Hash
|
|
99
|
+
Utils::Hash.deep_symbolize(value)
|
|
100
|
+
when ::Array
|
|
101
|
+
value.map { deep_symbolize(_1) }
|
|
102
|
+
else
|
|
103
|
+
value
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
class Action
|
|
5
|
+
module Cache
|
|
6
|
+
# Module with Cache-Control logic
|
|
7
|
+
#
|
|
8
|
+
# @since 0.3.0
|
|
9
|
+
# @api private
|
|
10
|
+
module CacheControl
|
|
11
|
+
# @since 0.3.0
|
|
12
|
+
# @api private
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.class_eval do
|
|
15
|
+
extend ClassMethods
|
|
16
|
+
|
|
17
|
+
@cache_control_directives = nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @since 0.3.0
|
|
22
|
+
# @api private
|
|
23
|
+
module ClassMethods
|
|
24
|
+
# @since 0.3.0
|
|
25
|
+
# @api private
|
|
26
|
+
def cache_control(*values)
|
|
27
|
+
@cache_control_directives ||= Directives.new(*values)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @since 0.3.0
|
|
31
|
+
# @api private
|
|
32
|
+
def cache_control_directives
|
|
33
|
+
@cache_control_directives || Object.new.tap do |null_object|
|
|
34
|
+
def null_object.headers
|
|
35
|
+
{}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Finalize the response including default cache headers into the response
|
|
42
|
+
#
|
|
43
|
+
# @since 0.3.0
|
|
44
|
+
# @api private
|
|
45
|
+
#
|
|
46
|
+
# @see Hanami::Action#finish
|
|
47
|
+
def finish(_, res, _)
|
|
48
|
+
unless res.headers.include?(Action::CACHE_CONTROL)
|
|
49
|
+
res.headers.merge!(self.class.cache_control_directives.headers)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Class which stores CacheControl values
|
|
56
|
+
#
|
|
57
|
+
# @since 0.3.0
|
|
58
|
+
# @api private
|
|
59
|
+
class Directives
|
|
60
|
+
# @since 2.0.0
|
|
61
|
+
# @api private
|
|
62
|
+
SEPARATOR = ", "
|
|
63
|
+
private_constant :SEPARATOR
|
|
64
|
+
|
|
65
|
+
# @since 0.3.0
|
|
66
|
+
# @api private
|
|
67
|
+
def initialize(*values)
|
|
68
|
+
@directives = Hanami::Action::Cache::Directives.new(*values)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @since 0.3.0
|
|
72
|
+
# @api private
|
|
73
|
+
def headers
|
|
74
|
+
if @directives.any?
|
|
75
|
+
{Action::CACHE_CONTROL => @directives.join(SEPARATOR)}
|
|
76
|
+
else
|
|
77
|
+
{}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hanami/utils/blank"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
class Action
|
|
7
|
+
module Cache
|
|
8
|
+
# ETag value object
|
|
9
|
+
#
|
|
10
|
+
# @since 0.3.0
|
|
11
|
+
# @api private
|
|
12
|
+
class ETag
|
|
13
|
+
# @since 0.3.0
|
|
14
|
+
# @api private
|
|
15
|
+
def initialize(env, value)
|
|
16
|
+
@env, @value = env, value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @since 0.3.0
|
|
20
|
+
# @api private
|
|
21
|
+
def fresh?
|
|
22
|
+
none_match && @value == none_match
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @since 0.3.0
|
|
26
|
+
# @api private
|
|
27
|
+
def header
|
|
28
|
+
{Action::ETAG => @value} if @value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @since 0.3.0
|
|
34
|
+
# @api private
|
|
35
|
+
def none_match
|
|
36
|
+
@env[Action::IF_NONE_MATCH]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# LastModified value object
|
|
41
|
+
#
|
|
42
|
+
# @since 0.3.0
|
|
43
|
+
# @api private
|
|
44
|
+
class LastModified
|
|
45
|
+
# @since 0.3.0
|
|
46
|
+
# @api private
|
|
47
|
+
def initialize(env, value)
|
|
48
|
+
@env, @value = env, value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @since 0.3.0
|
|
52
|
+
# @api private
|
|
53
|
+
def fresh?
|
|
54
|
+
return false if Hanami::Utils::Blank.blank?(modified_since)
|
|
55
|
+
return false if Hanami::Utils::Blank.blank?(@value)
|
|
56
|
+
|
|
57
|
+
Time.httpdate(modified_since).to_i >= @value.to_time.to_i
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @since 0.3.0
|
|
61
|
+
# @api private
|
|
62
|
+
def header
|
|
63
|
+
{Action::LAST_MODIFIED => @value.httpdate} if @value.respond_to?(:httpdate)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# @since 0.3.0
|
|
69
|
+
# @api private
|
|
70
|
+
def modified_since
|
|
71
|
+
@env[Action::IF_MODIFIED_SINCE]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Class responsible to determine if a given request is fresh
|
|
76
|
+
# based on IF_NONE_MATCH and IF_MODIFIED_SINCE headers
|
|
77
|
+
#
|
|
78
|
+
# @since 0.3.0
|
|
79
|
+
# @api private
|
|
80
|
+
class ConditionalGet
|
|
81
|
+
# @since 0.3.0
|
|
82
|
+
# @api private
|
|
83
|
+
def initialize(env, options)
|
|
84
|
+
@validations = [ETag.new(env, options[:etag]), LastModified.new(env, options[:last_modified])]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @since 0.3.0
|
|
88
|
+
# @api private
|
|
89
|
+
def fresh?
|
|
90
|
+
yield if @validations.any?(&:fresh?)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @since 0.3.0
|
|
94
|
+
# @api private
|
|
95
|
+
def headers
|
|
96
|
+
@validations.map(&:header).compact.reduce({}, :merge)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
class Action
|
|
5
|
+
module Cache
|
|
6
|
+
# Cache-Control directives which have values
|
|
7
|
+
#
|
|
8
|
+
# @since 0.3.0
|
|
9
|
+
# @api private
|
|
10
|
+
VALUE_DIRECTIVES = %i[max_age s_maxage min_fresh max_stale].freeze
|
|
11
|
+
|
|
12
|
+
# Cache-Control directives which are implicitly true
|
|
13
|
+
#
|
|
14
|
+
# @since 0.3.0
|
|
15
|
+
# @api private
|
|
16
|
+
NON_VALUE_DIRECTIVES = %i[public private no_cache no_store no_transform must_revalidate proxy_revalidate].freeze
|
|
17
|
+
|
|
18
|
+
# Class representing value directives
|
|
19
|
+
#
|
|
20
|
+
# ex: max-age=600
|
|
21
|
+
#
|
|
22
|
+
# @since 0.3.0
|
|
23
|
+
# @api private
|
|
24
|
+
class ValueDirective
|
|
25
|
+
# @since 0.3.0
|
|
26
|
+
# @api private
|
|
27
|
+
attr_reader :name
|
|
28
|
+
|
|
29
|
+
# @since 0.3.0
|
|
30
|
+
# @api private
|
|
31
|
+
def initialize(name, value)
|
|
32
|
+
@name, @value = name, value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @since 0.3.0
|
|
36
|
+
# @api private
|
|
37
|
+
def to_str
|
|
38
|
+
"#{@name.to_s.tr('_', '-')}=#{@value.to_i}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @since 0.3.0
|
|
42
|
+
# @api private
|
|
43
|
+
def valid?
|
|
44
|
+
VALUE_DIRECTIVES.include? @name
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Class representing non value directives
|
|
49
|
+
#
|
|
50
|
+
# ex: no-cache
|
|
51
|
+
#
|
|
52
|
+
# @since 0.3.0
|
|
53
|
+
# @api private
|
|
54
|
+
class NonValueDirective
|
|
55
|
+
# @since 0.3.0
|
|
56
|
+
# @api private
|
|
57
|
+
attr_reader :name
|
|
58
|
+
|
|
59
|
+
# @since 0.3.0
|
|
60
|
+
# @api private
|
|
61
|
+
def initialize(name)
|
|
62
|
+
@name = name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @since 0.3.0
|
|
66
|
+
# @api private
|
|
67
|
+
def to_str
|
|
68
|
+
@name.to_s.tr("_", "-")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @since 0.3.0
|
|
72
|
+
# @api private
|
|
73
|
+
def valid?
|
|
74
|
+
NON_VALUE_DIRECTIVES.include? @name
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Collection of value and non value directives
|
|
79
|
+
#
|
|
80
|
+
# @since 0.3.0
|
|
81
|
+
# @api private
|
|
82
|
+
class Directives
|
|
83
|
+
include Enumerable
|
|
84
|
+
|
|
85
|
+
# @since 0.3.0
|
|
86
|
+
# @api private
|
|
87
|
+
def initialize(*values)
|
|
88
|
+
@directives = []
|
|
89
|
+
values.each do |directive_key|
|
|
90
|
+
if directive_key.is_a? Hash
|
|
91
|
+
directive_key.each { |name, value| self << ValueDirective.new(name, value) }
|
|
92
|
+
else
|
|
93
|
+
self << NonValueDirective.new(directive_key)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @since 0.3.0
|
|
99
|
+
# @api private
|
|
100
|
+
def each(&block)
|
|
101
|
+
@directives.each(&block)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @since 0.3.0
|
|
105
|
+
# @api private
|
|
106
|
+
def <<(directive)
|
|
107
|
+
@directives << directive if directive.valid?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @since 0.3.0
|
|
111
|
+
# @api private
|
|
112
|
+
def values
|
|
113
|
+
@directives.delete_if do |directive|
|
|
114
|
+
directive.name == :public && @directives.map(&:name).include?(:private)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @since 0.3.0
|
|
119
|
+
# @api private
|
|
120
|
+
def join(separator)
|
|
121
|
+
values.join(separator)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
class Action
|
|
5
|
+
module Cache
|
|
6
|
+
# Module with Expires logic
|
|
7
|
+
#
|
|
8
|
+
# @since 0.3.0
|
|
9
|
+
# @api private
|
|
10
|
+
module Expires
|
|
11
|
+
# @since 0.3.0
|
|
12
|
+
# @api private
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.class_eval do
|
|
15
|
+
extend ClassMethods
|
|
16
|
+
|
|
17
|
+
@expires_directives = nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @since 0.3.0
|
|
22
|
+
# @api private
|
|
23
|
+
module ClassMethods
|
|
24
|
+
# @since 0.3.0
|
|
25
|
+
# @api private
|
|
26
|
+
def expires(amount, *values)
|
|
27
|
+
@expires_directives ||= Directives.new(amount, *values)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @since 0.3.0
|
|
31
|
+
# @api private
|
|
32
|
+
def expires_directives
|
|
33
|
+
@expires_directives || Object.new.tap do |null_object|
|
|
34
|
+
def null_object.headers
|
|
35
|
+
{}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Finalize the response including default cache headers into the response
|
|
42
|
+
#
|
|
43
|
+
# @since 0.3.0
|
|
44
|
+
# @api private
|
|
45
|
+
#
|
|
46
|
+
# @see Hanami::Action#finish
|
|
47
|
+
def finish(_, res, _)
|
|
48
|
+
unless res.headers.include?(Action::EXPIRES)
|
|
49
|
+
res.headers.merge!(self.class.expires_directives.headers)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Class which stores Expires directives
|
|
56
|
+
#
|
|
57
|
+
# @since 0.3.0
|
|
58
|
+
# @api private
|
|
59
|
+
class Directives
|
|
60
|
+
# @since 0.3.0
|
|
61
|
+
# @api private
|
|
62
|
+
def initialize(amount, *values)
|
|
63
|
+
@amount = amount
|
|
64
|
+
@cache_control = Hanami::Action::Cache::CacheControl::Directives.new(*(values << {max_age: amount}))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @since 0.3.0
|
|
68
|
+
# @api private
|
|
69
|
+
def headers
|
|
70
|
+
{Action::EXPIRES => time.httpdate}.merge(@cache_control.headers)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# @since 0.3.0
|
|
76
|
+
# @api private
|
|
77
|
+
def time
|
|
78
|
+
Time.now + @amount.to_i
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
class Action
|
|
5
|
+
# Cache type API
|
|
6
|
+
#
|
|
7
|
+
# @since 0.3.0
|
|
8
|
+
#
|
|
9
|
+
# @see Hanami::Action::Cache::ClassMethods#cache_control
|
|
10
|
+
# @see Hanami::Action::Cache::ClassMethods#expires
|
|
11
|
+
# @see Hanami::Action::Cache::ClassMethods#fresh
|
|
12
|
+
module Cache
|
|
13
|
+
# Override Ruby's hook for modules.
|
|
14
|
+
# It includes exposures logic
|
|
15
|
+
#
|
|
16
|
+
# @param base [Class] the target action
|
|
17
|
+
#
|
|
18
|
+
# @since 0.3.0
|
|
19
|
+
# @api private
|
|
20
|
+
#
|
|
21
|
+
# @see http://www.ruby-doc.org/core/Module.html#method-i-included
|
|
22
|
+
def self.included(base)
|
|
23
|
+
base.class_eval do
|
|
24
|
+
include CacheControl, Expires
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|