lotus-controller 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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