hanami-controller 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +1180 -9
  5. data/hanami-controller.gemspec +19 -12
  6. data/lib/hanami-controller.rb +1 -0
  7. data/lib/hanami/action.rb +85 -0
  8. data/lib/hanami/action/cache.rb +174 -0
  9. data/lib/hanami/action/cache/cache_control.rb +70 -0
  10. data/lib/hanami/action/cache/conditional_get.rb +93 -0
  11. data/lib/hanami/action/cache/directives.rb +99 -0
  12. data/lib/hanami/action/cache/expires.rb +73 -0
  13. data/lib/hanami/action/callable.rb +94 -0
  14. data/lib/hanami/action/callbacks.rb +210 -0
  15. data/lib/hanami/action/configurable.rb +49 -0
  16. data/lib/hanami/action/cookie_jar.rb +181 -0
  17. data/lib/hanami/action/cookies.rb +85 -0
  18. data/lib/hanami/action/exposable.rb +115 -0
  19. data/lib/hanami/action/flash.rb +182 -0
  20. data/lib/hanami/action/glue.rb +66 -0
  21. data/lib/hanami/action/head.rb +122 -0
  22. data/lib/hanami/action/mime.rb +493 -0
  23. data/lib/hanami/action/params.rb +285 -0
  24. data/lib/hanami/action/rack.rb +270 -0
  25. data/lib/hanami/action/rack/callable.rb +47 -0
  26. data/lib/hanami/action/rack/file.rb +33 -0
  27. data/lib/hanami/action/redirect.rb +59 -0
  28. data/lib/hanami/action/request.rb +86 -0
  29. data/lib/hanami/action/session.rb +154 -0
  30. data/lib/hanami/action/throwable.rb +194 -0
  31. data/lib/hanami/action/validatable.rb +128 -0
  32. data/lib/hanami/controller.rb +250 -2
  33. data/lib/hanami/controller/configuration.rb +705 -0
  34. data/lib/hanami/controller/error.rb +7 -0
  35. data/lib/hanami/controller/version.rb +4 -1
  36. data/lib/hanami/http/status.rb +62 -0
  37. metadata +124 -16
  38. data/.gitignore +0 -9
  39. data/Gemfile +0 -4
  40. data/Rakefile +0 -2
  41. data/bin/console +0 -14
  42. data/bin/setup +0 -8
@@ -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 = "hanami-controller"
7
+ spec.name = 'hanami-controller'
8
8
  spec.version = Hanami::Controller::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
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.summary = %q{The web, with simplicity}
13
- spec.description = %q{Hanami is a web framework for Ruby}
14
- spec.homepage = "http://hanamirb.org"
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.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
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 "bundler", "~> 1.11"
22
- spec.add_development_dependency "rake", "~> 10.0"
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