lotus-controller 0.2.0 → 0.3.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 +65 -127
- data/README.md +289 -89
- data/lib/lotus/action.rb +12 -6
- data/lib/lotus/action/cache.rb +174 -0
- data/lib/lotus/action/cache/cache_control.rb +70 -0
- data/lib/lotus/action/cache/conditional_get.rb +93 -0
- data/lib/lotus/action/cache/directives.rb +99 -0
- data/lib/lotus/action/cache/expires.rb +73 -0
- data/lib/lotus/action/callable.rb +3 -2
- data/lib/lotus/action/callbacks.rb +8 -0
- data/lib/lotus/action/configurable.rb +3 -2
- data/lib/lotus/action/cookies.rb +13 -12
- data/lib/lotus/action/exposable.rb +31 -4
- data/lib/lotus/action/flash.rb +141 -0
- data/lib/lotus/action/glue.rb +35 -0
- data/lib/lotus/action/mime.rb +82 -4
- data/lib/lotus/action/params.rb +87 -3
- data/lib/lotus/action/rack.rb +66 -51
- data/lib/lotus/action/redirect.rb +21 -3
- data/lib/lotus/action/session.rb +97 -0
- data/lib/lotus/action/throwable.rb +24 -4
- data/lib/lotus/action/validatable.rb +128 -0
- data/lib/lotus/controller.rb +11 -25
- data/lib/lotus/controller/configuration.rb +117 -66
- data/lib/lotus/controller/version.rb +1 -1
- data/lib/lotus/http/status.rb +5 -1
- data/lib/rack-patch.rb +8 -2
- data/lotus-controller.gemspec +6 -4
- metadata +54 -21
- data/lib/lotus/controller/dsl.rb +0 -56
data/lib/lotus/action.rb
CHANGED
@@ -5,6 +5,7 @@ require 'lotus/action/redirect'
|
|
5
5
|
require 'lotus/action/exposable'
|
6
6
|
require 'lotus/action/throwable'
|
7
7
|
require 'lotus/action/callbacks'
|
8
|
+
require 'lotus/action/validatable'
|
8
9
|
require 'lotus/action/callable'
|
9
10
|
|
10
11
|
module Lotus
|
@@ -33,28 +34,30 @@ module Lotus
|
|
33
34
|
#
|
34
35
|
# @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
|
35
36
|
#
|
36
|
-
# @see Lotus::Action::Configurable
|
37
37
|
# @see Lotus::Action::Rack
|
38
38
|
# @see Lotus::Action::Mime
|
39
39
|
# @see Lotus::Action::Redirect
|
40
40
|
# @see Lotus::Action::Exposable
|
41
41
|
# @see Lotus::Action::Throwable
|
42
42
|
# @see Lotus::Action::Callbacks
|
43
|
+
# @see Lotus::Action::Validatable
|
44
|
+
# @see Lotus::Action::Configurable
|
43
45
|
# @see Lotus::Action::Callable
|
44
46
|
def self.included(base)
|
45
47
|
base.class_eval do
|
46
|
-
include Configurable
|
47
48
|
include Rack
|
48
49
|
include Mime
|
49
50
|
include Redirect
|
50
51
|
include Exposable
|
51
52
|
include Throwable
|
52
53
|
include Callbacks
|
54
|
+
include Validatable
|
55
|
+
include Configurable
|
53
56
|
prepend Callable
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
57
|
-
|
60
|
+
private
|
58
61
|
|
59
62
|
# Finalize the response
|
60
63
|
#
|
@@ -66,9 +69,12 @@ module Lotus
|
|
66
69
|
# @api private
|
67
70
|
# @abstract
|
68
71
|
#
|
69
|
-
# @see Lotus::Action::Mime
|
70
|
-
# @see Lotus::Action::
|
71
|
-
# @see Lotus::Action::Callable
|
72
|
+
# @see Lotus::Action::Mime#finish
|
73
|
+
# @see Lotus::Action::Exposable#finish
|
74
|
+
# @see Lotus::Action::Callable#finish
|
75
|
+
# @see Lotus::Action::Session#finish
|
76
|
+
# @see Lotus::Action::Cookies#finish
|
77
|
+
# @see Lotus::Action::Cache#finish
|
72
78
|
def finish
|
73
79
|
end
|
74
80
|
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'lotus/action/cache/cache_control'
|
2
|
+
require 'lotus/action/cache/expires'
|
3
|
+
require 'lotus/action/cache/conditional_get'
|
4
|
+
|
5
|
+
module Lotus
|
6
|
+
module Action
|
7
|
+
# Cache type API
|
8
|
+
#
|
9
|
+
# @since 0.3.0
|
10
|
+
#
|
11
|
+
# @see Lotus::Action::Cache::ClassMethods#cache_control
|
12
|
+
# @see Lotus::Action::Cache::ClassMethods#expires
|
13
|
+
# @see Lotus::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 'lotus/controller'
|
58
|
+
# require 'lotus/action/cache'
|
59
|
+
#
|
60
|
+
# class Show
|
61
|
+
# include Lotus::Action
|
62
|
+
# include Lotus::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 'lotus/controller'
|
98
|
+
# require 'lotus/action/cache'
|
99
|
+
#
|
100
|
+
# class Show
|
101
|
+
# include Lotus::Action
|
102
|
+
# include Lotus::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 'lotus/controller'
|
139
|
+
# require 'lotus/action/cache'
|
140
|
+
#
|
141
|
+
# class Show
|
142
|
+
# include Lotus::Action
|
143
|
+
# include Lotus::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 'lotus/action/cache/directives'
|
2
|
+
|
3
|
+
module Lotus
|
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 Lotus::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 = Lotus::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 Lotus
|
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
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Action
|
3
|
+
module Cache
|
4
|
+
|
5
|
+
# Cache-Control directives which have values
|
6
|
+
#
|
7
|
+
# @since 0.3.0
|
8
|
+
# @api private
|
9
|
+
VALUE_DIRECTIVES = %i(max_age s_maxage min_fresh max_stale).freeze
|
10
|
+
|
11
|
+
# Cache-Control directives which are implicitly true
|
12
|
+
#
|
13
|
+
# @since 0.3.0
|
14
|
+
# @api private
|
15
|
+
NON_VALUE_DIRECTIVES = %i(public private no_cache no_store no_transform must_revalidate proxy_revalidate).freeze
|
16
|
+
|
17
|
+
# Class representing value directives
|
18
|
+
#
|
19
|
+
# ex: max-age=600
|
20
|
+
#
|
21
|
+
# @since 0.3.0
|
22
|
+
# @api private
|
23
|
+
class ValueDirective
|
24
|
+
attr_reader :name
|
25
|
+
|
26
|
+
def initialize(name, value)
|
27
|
+
@name, @value = name, value
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_str
|
31
|
+
"#{@name.to_s.tr('_', '-')}=#{@value.to_i}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid?
|
35
|
+
VALUE_DIRECTIVES.include? @name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Class representing non value directives
|
40
|
+
#
|
41
|
+
# ex: no-cache
|
42
|
+
#
|
43
|
+
# @since 0.3.0
|
44
|
+
# @api private
|
45
|
+
class NonValueDirective
|
46
|
+
attr_reader :name
|
47
|
+
|
48
|
+
def initialize(name)
|
49
|
+
@name = name
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_str
|
53
|
+
@name.to_s.tr('_', '-')
|
54
|
+
end
|
55
|
+
|
56
|
+
def valid?
|
57
|
+
NON_VALUE_DIRECTIVES.include? @name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Collection of value and non value directives
|
62
|
+
#
|
63
|
+
# @since 0.3.0
|
64
|
+
# @api private
|
65
|
+
class Directives
|
66
|
+
include Enumerable
|
67
|
+
|
68
|
+
def initialize(*values)
|
69
|
+
@directives = []
|
70
|
+
values.each do |directive_key|
|
71
|
+
if directive_key.kind_of? Hash
|
72
|
+
directive_key.each { |name, value| self.<< ValueDirective.new(name, value) }
|
73
|
+
else
|
74
|
+
self.<< NonValueDirective.new(directive_key)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def each
|
80
|
+
@directives.each { |d| yield d }
|
81
|
+
end
|
82
|
+
|
83
|
+
def <<(directive)
|
84
|
+
@directives << directive if directive.valid?
|
85
|
+
end
|
86
|
+
|
87
|
+
def values
|
88
|
+
@directives.delete_if do |directive|
|
89
|
+
directive.name == :public && @directives.map(&:name).include?(:private)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def join(separator)
|
94
|
+
values.join(separator)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|