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.
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
- protected
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::Cookies
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