utopia 1.3.1 → 1.3.2
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/lib/utopia/controller.rb +3 -5
- data/lib/utopia/controller/base.rb +1 -1
- data/lib/utopia/controller/respond.rb +35 -34
- data/lib/utopia/controller/variables.rb +20 -11
- data/lib/utopia/exception_handler.rb +36 -21
- data/lib/utopia/http.rb +1 -1
- data/lib/utopia/locale.rb +2 -2
- data/lib/utopia/middleware.rb +0 -2
- data/lib/utopia/redirector.rb +18 -16
- data/lib/utopia/static.rb +7 -5
- data/lib/utopia/version.rb +1 -1
- data/spec/utopia/controller/respond_spec.rb +21 -3
- data/spec/utopia/controller/respond_spec/api/controller.rb +2 -1
- data/spec/utopia/controller/respond_spec/errors/controller.rb +5 -1
- data/spec/utopia/controller/sequence_spec.rb +1 -1
- data/spec/utopia/controller/variables_spec.rb +58 -0
- data/spec/utopia/exception_handler_spec.rb +5 -2
- data/spec/utopia/exception_handler_spec/controller.rb +1 -0
- data/spec/utopia/http/status_spec.rb +42 -0
- data/spec/utopia/locale_spec.rb +29 -21
- data/spec/utopia/rack_helper.rb +27 -0
- data/spec/utopia/redirector_spec.rb +53 -0
- data/spec/utopia/redirector_spec.ru +26 -0
- data/utopia.gemspec +1 -1
- metadata +14 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27fc0800c0d44763978e981de97541dbf2d0d2ef
|
4
|
+
data.tar.gz: 346b6f43e6e056667c8ab6c30b8b185bfb1cdd58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9eedc27a561f991f9d3e637371d51770f31eeeb42d4a46a85f4461a2211b8985b5f86af4f3e5da8115ce45f6b13bfdd12fdc2eafc4b0fa9083dac44d1b6f0af
|
7
|
+
data.tar.gz: d9feff14bb7a57d3d0df656537751abe5e1fb9b1608f49e870a8a4e2f17650e80e41be6601b8411395b60b33faa9b980f75d3b3862455e9c77850d2a07886a4c
|
data/lib/utopia/controller.rb
CHANGED
@@ -47,17 +47,15 @@ module Utopia
|
|
47
47
|
class Controller
|
48
48
|
CONTROLLER_RB = 'controller.rb'.freeze
|
49
49
|
|
50
|
-
def initialize(app,
|
50
|
+
def initialize(app, root: nil, cache_controllers: false)
|
51
51
|
@app = app
|
52
|
-
@root =
|
52
|
+
@root = root || Utopia::default_root
|
53
53
|
|
54
|
-
if
|
54
|
+
if cache_controllers
|
55
55
|
@controller_cache = Concurrent::Map.new
|
56
56
|
else
|
57
57
|
@controller_cache = nil
|
58
58
|
end
|
59
|
-
|
60
|
-
self.freeze
|
61
59
|
end
|
62
60
|
|
63
61
|
attr :app
|
@@ -100,7 +100,7 @@ module Utopia
|
|
100
100
|
# Copy the instance variables from the previous controller to the next controller (usually only a few). This allows controllers to share effectively the same instance variables while still being separate classes/instances.
|
101
101
|
def copy_instance_variables(from)
|
102
102
|
from.instance_variables.each do |name|
|
103
|
-
instance_variable_set(name, from.instance_variable_get(name))
|
103
|
+
self.instance_variable_set(name, from.instance_variable_get(name))
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
@@ -23,7 +23,7 @@ require_relative '../path/matcher'
|
|
23
23
|
|
24
24
|
module Utopia
|
25
25
|
class Controller
|
26
|
-
# This controller layer provides a convenient way to respond to different requested content types.
|
26
|
+
# This controller layer provides a convenient way to respond to different requested content types. The order in which you add converters matters, as it determins how the incoming Accept: header is mapped, e.g. the first converter is also defined as matching the media range */*.
|
27
27
|
module Respond
|
28
28
|
def self.prepended(base)
|
29
29
|
base.extend(ClassMethods)
|
@@ -42,11 +42,15 @@ module Utopia
|
|
42
42
|
return [status, headers, body]
|
43
43
|
end
|
44
44
|
|
45
|
-
|
45
|
+
Callback = Struct.new(:content_type, :block) do
|
46
46
|
def headers
|
47
47
|
{HTTP::CONTENT_TYPE => self.content_type}
|
48
48
|
end
|
49
49
|
|
50
|
+
def split(*args)
|
51
|
+
self.content_type.split(*args)
|
52
|
+
end
|
53
|
+
|
50
54
|
def call(context, response, media_range)
|
51
55
|
Converter.update_response(response, headers) do |content|
|
52
56
|
context.instance_exec(content, media_range, &block)
|
@@ -67,6 +71,10 @@ module Utopia
|
|
67
71
|
APPLICATION_JSON
|
68
72
|
end
|
69
73
|
|
74
|
+
def self.split(*args)
|
75
|
+
self.content_type.split(*args)
|
76
|
+
end
|
77
|
+
|
70
78
|
def self.serialize(content, media_range)
|
71
79
|
options = {}
|
72
80
|
|
@@ -85,67 +93,58 @@ module Utopia
|
|
85
93
|
end
|
86
94
|
end
|
87
95
|
|
96
|
+
module Passthrough
|
97
|
+
WILDCARD = HTTP::Accept::MediaTypes::MediaRange.new('*/*').freeze
|
98
|
+
|
99
|
+
def self.split(*args)
|
100
|
+
self.media_range.split(*args)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.media_range
|
104
|
+
WILDCARD
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.call(context, response, media_range)
|
108
|
+
return nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
88
112
|
class Responder
|
89
113
|
HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
|
90
114
|
NOT_ACCEPTABLE_RESPONSE = [406, {}, []].freeze
|
91
115
|
|
92
116
|
def initialize
|
93
117
|
@converters = HTTP::Accept::MediaTypes::Map.new
|
94
|
-
@otherwise = nil
|
95
118
|
end
|
96
119
|
|
97
120
|
def freeze
|
98
121
|
@converters.freeze
|
99
|
-
@otherwise.freeze
|
100
122
|
|
101
123
|
super
|
102
124
|
end
|
103
125
|
|
104
|
-
# Parse the list of browser preferred content types and return ordered by priority.
|
105
|
-
def browser_preferred_media_types(env)
|
106
|
-
if accept_content_types = env[HTTP_ACCEPT]
|
107
|
-
HTTP::Accept::MediaTypes.parse(accept_content_types)
|
108
|
-
else
|
109
|
-
return []
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
126
|
# Add a converter for the specified content type. Call the block with the response content if the request accepts the specified content_type.
|
114
127
|
def with(content_type, &block)
|
115
128
|
@converters << Converter::Callback.new(content_type, block)
|
116
129
|
end
|
117
130
|
|
131
|
+
def with_passthrough
|
132
|
+
@converters << Passthrough
|
133
|
+
end
|
134
|
+
|
118
135
|
# Add a converter for JSON when requests accept 'application/json'
|
119
136
|
def with_json
|
120
137
|
@converters << Converter::ToJSON
|
121
138
|
end
|
122
139
|
|
123
|
-
# If the content type could not be matched, invoke the provided block and use it's result as the response.
|
124
|
-
def otherwise(&block)
|
125
|
-
@otherwise = block
|
126
|
-
end
|
127
|
-
|
128
|
-
# If the content type could not be matched, ignore it and don't use the result of the controller layer.
|
129
|
-
def otherwise_passthrough
|
130
|
-
@otherwise = proc { nil }
|
131
|
-
end
|
132
|
-
|
133
140
|
def call(context, request, path, response)
|
134
|
-
|
141
|
+
# Parse the list of browser preferred content types and return ordered by priority:
|
142
|
+
media_types = HTTP::Accept::MediaTypes.browser_preferred_media_types(request.env)
|
135
143
|
|
136
144
|
converter, media_range = @converters.for(media_types)
|
137
145
|
|
138
146
|
if converter
|
139
147
|
converter.call(context, response, media_range)
|
140
|
-
else
|
141
|
-
not_acceptable_response(context, response)
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
# Generate a not acceptable response which unless customised with `otherwise`, will result in a generic 406 Not Acceptable response.
|
146
|
-
def not_acceptable_response(context, response)
|
147
|
-
if @otherwise
|
148
|
-
context.instance_exec(response, &@otherwise)
|
149
148
|
else
|
150
149
|
NOT_ACCEPTABLE_RESPONSE
|
151
150
|
end
|
@@ -169,7 +168,9 @@ module Utopia
|
|
169
168
|
# Rewrite the path before processing the request if possible.
|
170
169
|
def passthrough(request, path)
|
171
170
|
if response = super
|
172
|
-
self.class.response_for(self, request, path, response)
|
171
|
+
response = self.class.response_for(self, request, path, response)
|
172
|
+
|
173
|
+
response
|
173
174
|
end
|
174
175
|
end
|
175
176
|
end
|
@@ -24,25 +24,34 @@ module Utopia
|
|
24
24
|
def initialize
|
25
25
|
@controllers = []
|
26
26
|
end
|
27
|
+
|
28
|
+
def top
|
29
|
+
@controllers.last
|
30
|
+
end
|
27
31
|
|
28
32
|
def << controller
|
29
|
-
top =
|
33
|
+
if top = self.top
|
34
|
+
# This ensures that most variables will be at the top and controllers can naturally interactive with instance variables:
|
35
|
+
controller.copy_instance_variables(top)
|
36
|
+
end
|
30
37
|
|
31
38
|
@controllers << controller
|
32
39
|
|
33
|
-
|
34
|
-
controller.copy_instance_variables(top) if top
|
40
|
+
return self
|
35
41
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
42
|
+
|
43
|
+
# We use self as a seninel
|
44
|
+
def fetch(key, default=self)
|
45
|
+
if controller = self.top
|
39
46
|
if controller.instance_variables.include?(key)
|
40
47
|
return controller.instance_variable_get(key)
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
44
51
|
if block_given?
|
45
|
-
yield
|
52
|
+
yield(key)
|
53
|
+
elsif !default.equal?(self)
|
54
|
+
return default
|
46
55
|
else
|
47
56
|
raise KeyError.new(key)
|
48
57
|
end
|
@@ -50,20 +59,20 @@ module Utopia
|
|
50
59
|
|
51
60
|
def to_hash
|
52
61
|
attributes = {}
|
53
|
-
|
54
|
-
|
62
|
+
|
63
|
+
if controller = self.top
|
55
64
|
controller.instance_variables.each do |name|
|
56
65
|
key = name[1..-1]
|
57
66
|
|
58
67
|
attributes[key] = controller.instance_variable_get(name)
|
59
68
|
end
|
60
69
|
end
|
61
|
-
|
70
|
+
|
62
71
|
return attributes
|
63
72
|
end
|
64
73
|
|
65
74
|
def [] key
|
66
|
-
fetch("@#{key}".to_sym
|
75
|
+
fetch("@#{key}".to_sym, nil)
|
67
76
|
end
|
68
77
|
end
|
69
78
|
end
|
@@ -37,40 +37,55 @@ module Utopia
|
|
37
37
|
|
38
38
|
super
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
|
+
private def write_exception_to_stream(stream, env, exception, include_backtrace = false)
|
42
|
+
buffer = []
|
43
|
+
|
44
|
+
buffer << "While requesting resource #{env[Rack::PATH_INFO].inspect}, a fatal error occurred:"
|
45
|
+
|
46
|
+
while exception != nil
|
47
|
+
buffer << "\t#{exception.class.name}: #{exception.to_s}"
|
48
|
+
|
49
|
+
if include_backtrace
|
50
|
+
exception.backtrace.each do |line|
|
51
|
+
buffer << "\t\t#{line}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
exception = exception.cause
|
56
|
+
end
|
57
|
+
|
58
|
+
# We do this in one go so that lines don't get mixed up.
|
59
|
+
stream.puts buffer.join("\n")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Generate a very simple fatal error response. This function should be unlikely to fail. Additionally, it generates a lowest common denominator response which should be suitable as a response to any kind of request. Ideally, this response is also not good or useful for any kind of higher level browser or API client, as this is not a normal error path but one that represents broken behaviour.
|
41
63
|
def fatal_error(env, exception)
|
42
64
|
body = StringIO.new
|
43
|
-
|
44
|
-
body
|
45
|
-
body.puts "<h1>Fatal Error</h1>"
|
46
|
-
body.puts "<p>While requesting resource #{Trenni::Strings::to_html env[Rack::PATH_INFO]}, a fatal error occurred.</p>"
|
47
|
-
body.puts "<blockquote><strong>#{Trenni::Strings::to_html exception.class.name}</strong>: #{Trenni::Strings::to_html exception.to_s}</blockquote>"
|
48
|
-
body.puts "<p>There is nothing more we can do to fix the problem at this point.</p>"
|
49
|
-
body.puts "<p>We apologize for the inconvenience.</p>"
|
50
|
-
body.puts "</body></html>"
|
65
|
+
|
66
|
+
write_exception_to_stream(body, env, exception)
|
51
67
|
body.rewind
|
52
|
-
|
53
|
-
return [
|
68
|
+
|
69
|
+
return [500, {HTTP::CONTENT_TYPE => "text/plain"}, body]
|
54
70
|
end
|
55
|
-
|
71
|
+
|
72
|
+
def log_exception(env, exception)
|
73
|
+
# An error has occurred, log it:
|
74
|
+
output = env['rack.errors'] || $stderr
|
75
|
+
write_exception_to_stream(output, env, exception, true)
|
76
|
+
end
|
77
|
+
|
56
78
|
def redirect(env, exception)
|
57
79
|
response = @app.call(env.merge(Rack::PATH_INFO => @location, Rack::REQUEST_METHOD => Rack::GET))
|
58
80
|
|
59
81
|
return [500, response[1], response[2]]
|
60
82
|
end
|
61
|
-
|
83
|
+
|
62
84
|
def call(env)
|
63
85
|
begin
|
64
86
|
return @app.call(env)
|
65
87
|
rescue Exception => exception
|
66
|
-
|
67
|
-
log = ::Logger.new(env['rack.errors'] || $stderr)
|
68
|
-
|
69
|
-
log.error "Exception #{exception.to_s.dump}!"
|
70
|
-
|
71
|
-
exception.backtrace.each do |line|
|
72
|
-
log.error line
|
73
|
-
end
|
88
|
+
log_exception(env, exception)
|
74
89
|
|
75
90
|
# If the error occurred while accessing the error handler, we finish with a fatal error:
|
76
91
|
if env[Rack::PATH_INFO] == @location
|
data/lib/utopia/http.rb
CHANGED
@@ -86,7 +86,7 @@ module Utopia
|
|
86
86
|
|
87
87
|
CONTENT_TYPE = 'Content-Type'.freeze
|
88
88
|
LOCATION = 'Location'.freeze
|
89
|
-
|
89
|
+
CACHE_CONTROL = 'Cache-Control'.freeze
|
90
90
|
|
91
91
|
# A small HTTP status wrapper that verifies the status code within a given range.
|
92
92
|
class Status
|
data/lib/utopia/locale.rb
CHANGED
@@ -19,7 +19,7 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
module Utopia
|
22
|
-
|
22
|
+
Locale = Struct.new(:language, :country, :variant) do
|
23
23
|
def to_s
|
24
24
|
to_a.compact.join('-')
|
25
25
|
end
|
@@ -36,7 +36,7 @@ module Utopia
|
|
36
36
|
elsif instance.is_a? Array
|
37
37
|
return self.new(*instance)
|
38
38
|
elsif instance.is_a? self
|
39
|
-
return instance
|
39
|
+
return instance.frozen? ? instance : instance.dup
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
data/lib/utopia/middleware.rb
CHANGED
data/lib/utopia/redirector.rb
CHANGED
@@ -40,20 +40,12 @@ module Utopia
|
|
40
40
|
DIRECTORY_INDEX = [/^(.*)\/$/, lambda{|prefix| [307, {HTTP::LOCATION => "#{prefix}index"}, []]}].freeze
|
41
41
|
|
42
42
|
# Redirects a whole source tree to a destination tree, given by the roots.
|
43
|
-
def self.moved(source_root, destination_root)
|
44
|
-
return [
|
45
|
-
/^#{Regexp.escape(source_root)}(.*)$/,
|
46
|
-
lambda do |match|
|
47
|
-
[301, {HTTP::LOCATION => (destination_root + match[1]).to_s}, []]
|
48
|
-
end
|
49
|
-
]
|
43
|
+
def self.moved(source_root, destination_root, max_age = 3600*24)
|
44
|
+
return [/^#{Regexp.escape(source_root)}(.*)$/, lambda{|match| destination_root + match[1]}]
|
50
45
|
end
|
51
46
|
|
52
47
|
def self.starts_with(source_root, destination_uri)
|
53
|
-
return [
|
54
|
-
/^#{Regexp.escape(source_root)}/,
|
55
|
-
destination_uri
|
56
|
-
]
|
48
|
+
return [/^#{Regexp.escape(source_root)}/, destination_uri]
|
57
49
|
end
|
58
50
|
|
59
51
|
private
|
@@ -117,13 +109,23 @@ module Utopia
|
|
117
109
|
|
118
110
|
super
|
119
111
|
end
|
120
|
-
|
121
|
-
def
|
112
|
+
|
113
|
+
def cache_control(max_age)
|
114
|
+
# http://jacquesmattheij.com/301-redirects-a-dangerous-one-way-street
|
115
|
+
"max-age=#{max_age}"
|
116
|
+
end
|
117
|
+
|
118
|
+
# We cache 301 redirects for 24 hours.
|
119
|
+
DEFAULT_MAX_AGE = 3600*24
|
120
|
+
|
121
|
+
def redirect(uri, match_data, status: 301, max_age: DEFAULT_MAX_AGE)
|
122
|
+
cache_control = self.cache_control(max_age)
|
123
|
+
|
122
124
|
if uri.respond_to? :call
|
123
|
-
|
124
|
-
else
|
125
|
-
return [301, {HTTP::LOCATION => uri.to_s}, []]
|
125
|
+
uri = uri.call(match_data)
|
126
126
|
end
|
127
|
+
|
128
|
+
return [status, {HTTP::LOCATION => uri.to_s, HTTP::CACHE_CONTROL => cache_control}, []]
|
127
129
|
end
|
128
130
|
|
129
131
|
def call(env)
|
data/lib/utopia/static.rb
CHANGED
@@ -84,6 +84,9 @@ module Utopia
|
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
|
+
class ExpansionError < ArgumentError
|
88
|
+
end
|
89
|
+
|
87
90
|
def expand(types)
|
88
91
|
types.each do |type|
|
89
92
|
current_count = @extensions.size
|
@@ -102,12 +105,11 @@ module Utopia
|
|
102
105
|
self.extract_extensions.call([type])
|
103
106
|
end
|
104
107
|
rescue
|
105
|
-
|
106
|
-
raise $!
|
108
|
+
raise ExpansionError.new("#{self.class.name}: Error while processing #{type.inspect}!")
|
107
109
|
end
|
108
110
|
|
109
111
|
if @extensions.size == current_count
|
110
|
-
|
112
|
+
raise ExpansionError.new("#{self.class.name}: Could not find any mime type for #{type.inspect}")
|
111
113
|
end
|
112
114
|
end
|
113
115
|
end
|
@@ -178,7 +180,7 @@ module Utopia
|
|
178
180
|
ranges = Rack::Utils.byte_ranges(env, size)
|
179
181
|
response = [200, response_headers, self]
|
180
182
|
|
181
|
-
#
|
183
|
+
# puts "Requesting ranges: #{ranges.inspect} (#{size})"
|
182
184
|
|
183
185
|
if ranges == nil or ranges.size != 1
|
184
186
|
# No ranges, or multiple ranges (which we don't support).
|
@@ -196,7 +198,7 @@ module Utopia
|
|
196
198
|
response[1]["Content-Range"] = "bytes #{@range.min}-#{@range.max}/#{size}"
|
197
199
|
end
|
198
200
|
|
199
|
-
#
|
201
|
+
# puts "Serving file #{full_path.inspect}, range #{@range.inspect}"
|
200
202
|
|
201
203
|
return response
|
202
204
|
end
|
data/lib/utopia/version.rb
CHANGED
@@ -89,6 +89,9 @@ module Utopia::Controller::RespondSpec
|
|
89
89
|
let(:app) {Rack::Builder.parse_file(File.expand_path('respond_spec.ru', __dir__)).first}
|
90
90
|
|
91
91
|
it "should get html error page" do
|
92
|
+
# Standard web browser header:
|
93
|
+
header 'Accept', 'text/html, text/*, */*'
|
94
|
+
|
92
95
|
get '/errors/file-not-found'
|
93
96
|
|
94
97
|
expect(last_response.status).to be == 200
|
@@ -97,7 +100,9 @@ module Utopia::Controller::RespondSpec
|
|
97
100
|
end
|
98
101
|
|
99
102
|
it "should get json error response" do
|
100
|
-
|
103
|
+
header 'Accept', 'application/json'
|
104
|
+
|
105
|
+
get '/errors/file-not-found'
|
101
106
|
|
102
107
|
expect(last_response.status).to be == 404
|
103
108
|
expect(last_response.headers['Content-Type']).to be == 'application/json; charset=utf-8'
|
@@ -105,7 +110,9 @@ module Utopia::Controller::RespondSpec
|
|
105
110
|
end
|
106
111
|
|
107
112
|
it "should get version 1 response" do
|
108
|
-
|
113
|
+
header 'Accept', 'application/json;version=1'
|
114
|
+
|
115
|
+
get '/api/fetch'
|
109
116
|
|
110
117
|
expect(last_response.status).to be == 200
|
111
118
|
expect(last_response.headers['Content-Type']).to be == 'application/json; charset=utf-8'
|
@@ -113,11 +120,22 @@ module Utopia::Controller::RespondSpec
|
|
113
120
|
end
|
114
121
|
|
115
122
|
it "should get version 2 response" do
|
116
|
-
|
123
|
+
header 'Accept', 'application/json;version=2'
|
124
|
+
|
125
|
+
get '/api/fetch'
|
117
126
|
|
118
127
|
expect(last_response.status).to be == 200
|
119
128
|
expect(last_response.headers['Content-Type']).to be == 'application/json; charset=utf-8'
|
120
129
|
expect(last_response.body).to be == '{"message":"Goodbye World"}'
|
121
130
|
end
|
131
|
+
|
132
|
+
|
133
|
+
it "should work even if no accept header specified" do
|
134
|
+
get '/api/fetch'
|
135
|
+
|
136
|
+
expect(last_response.status).to be == 200
|
137
|
+
expect(last_response.headers['Content-Type']).to be == 'application/json; charset=utf-8'
|
138
|
+
expect(last_response.body).to be == '{}'
|
139
|
+
end
|
122
140
|
end
|
123
141
|
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
|
2
2
|
prepend Respond
|
3
3
|
|
4
|
+
# If the request doesn't match application/json specifically, it would be passed through:
|
5
|
+
respond.with_passthrough
|
4
6
|
respond.with_json
|
5
|
-
respond.otherwise_passthrough
|
6
7
|
|
8
|
+
# The reason why this test is important is that it tests the behaviour of error handling. Normally, if a request comes into the middleware and fails due to an unhandled exception, this is passed along by Utopia::ExceptionHandler. If the client is expecting JSON, they should get a JSON error response.
|
7
9
|
on 'file-not-found' do
|
8
10
|
fail! 404, {message: 'File not found'}
|
9
11
|
end
|
12
|
+
|
13
|
+
# Accept: text/html, application/json, */*
|
@@ -93,7 +93,7 @@ module Utopia::Controller::SequenceSpec
|
|
93
93
|
|
94
94
|
result = controller.process!(request, Utopia::Path["/variable"])
|
95
95
|
expect(result).to be == nil
|
96
|
-
expect(variables.to_hash).to be == {"variable"
|
96
|
+
expect(variables.to_hash).to be == {"variable" => :value}
|
97
97
|
end
|
98
98
|
|
99
99
|
it "should call direct controller methods" do
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env rspec
|
2
|
+
|
3
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
|
23
|
+
require 'utopia/controller/variables'
|
24
|
+
|
25
|
+
RSpec.describe Utopia::Controller::Variables do
|
26
|
+
class TestController
|
27
|
+
attr_accessor :x, :y, :z
|
28
|
+
|
29
|
+
def copy_instance_variables(from)
|
30
|
+
from.instance_variables.each do |name|
|
31
|
+
self.instance_variable_set(name, from.instance_variable_get(name))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:a) {TestController.new.tap{|controller| controller.x = 10}}
|
37
|
+
let(:b) {TestController.new.tap{|controller| controller.y = 20}}
|
38
|
+
let(:c) {TestController.new.tap{|controller| controller.z = 30}}
|
39
|
+
|
40
|
+
it "should fetch a key" do
|
41
|
+
subject << a
|
42
|
+
|
43
|
+
expect(subject[:x]).to be == 10
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should give a default when key is not found" do
|
47
|
+
subject << a
|
48
|
+
|
49
|
+
expect(subject.fetch(:y, :default)).to be == :default
|
50
|
+
expect(subject.fetch(:y){:default}).to be == :default
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should convert to hash" do
|
54
|
+
subject << a << b
|
55
|
+
|
56
|
+
expect(subject.to_hash).to be == {'x' => 10, 'y' => 20}
|
57
|
+
end
|
58
|
+
end
|
@@ -33,10 +33,13 @@ module Utopia::ExceptionHandlerSpec
|
|
33
33
|
let(:app) {Rack::Builder.parse_file(File.expand_path('exception_handler_spec.ru', __dir__)).first}
|
34
34
|
|
35
35
|
it "should successfully call the controller method" do
|
36
|
+
# This request will raise an exception, and then redirect to the /exception url which will fail again, and cause a fatal error.
|
37
|
+
|
36
38
|
get "/blow?fatal=true"
|
37
39
|
|
38
|
-
expect(last_response.status).to be ==
|
39
|
-
expect(last_response.
|
40
|
+
expect(last_response.status).to be == 500
|
41
|
+
expect(last_response.headers['Content-Type']).to be == 'text/plain'
|
42
|
+
expect(last_response.body).to be_include 'fatal error'
|
40
43
|
end
|
41
44
|
|
42
45
|
it "should fail with a 500 error" do
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'utopia/http'
|
22
|
+
|
23
|
+
RSpec.describe Utopia::HTTP::Status.new(:found) do
|
24
|
+
it "should load symbolic status" do
|
25
|
+
expect(subject.to_i).to be == 302
|
26
|
+
end
|
27
|
+
|
28
|
+
it "gives a status string" do
|
29
|
+
expect(subject.to_s).to be == "Found"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can be used as a response body" do
|
33
|
+
body = subject.to_enum(:each).next
|
34
|
+
expect(body).to be == "Found"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec.describe Utopia::HTTP::Status do
|
39
|
+
it "should fail when given invalid code" do
|
40
|
+
expect{Utopia::HTTP::Status.new(1000)}.to raise_error(ArgumentError)
|
41
|
+
end
|
42
|
+
end
|
data/spec/utopia/locale_spec.rb
CHANGED
@@ -21,28 +21,36 @@
|
|
21
21
|
|
22
22
|
require 'utopia/locale'
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
24
|
+
RSpec.shared_examples Utopia::Locale do |input|
|
25
|
+
it "should load locale #{input.inspect}" do
|
26
|
+
expect(Utopia::Locale.load(input)).to be_kind_of(Utopia::Locale)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
RSpec.describe Utopia::Locale do
|
31
|
+
it_behaves_like Utopia::Locale, 'en-US'
|
32
|
+
it_behaves_like Utopia::Locale, ['en', 'US']
|
33
|
+
it_behaves_like Utopia::Locale, Utopia::Locale.load('en-US')
|
34
|
+
|
35
|
+
it "should load from string" do
|
36
|
+
locale = Utopia::Locale.load('en-US')
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
expect(locale.language).to be == 'en'
|
39
|
+
expect(locale.country).to be == 'US'
|
40
|
+
expect(locale.variant).to be == nil
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should load from nil and return nil" do
|
44
|
+
expect(Utopia::Locale.load(nil)).to be == nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should dump nil and give nil" do
|
48
|
+
expect(Utopia::Locale.dump(nil)).to be == nil
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should dump locale and give string" do
|
52
|
+
locale = Utopia::Locale.new('en', 'US')
|
41
53
|
|
42
|
-
|
43
|
-
locale = Utopia::Locale.new('en', 'US')
|
44
|
-
|
45
|
-
expect(Utopia::Locale.dump(locale)).to be == 'en-US'
|
46
|
-
end
|
54
|
+
expect(Utopia::Locale.dump(locale)).to be == 'en-US'
|
47
55
|
end
|
48
56
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rack/test'
|
22
|
+
|
23
|
+
RSpec.shared_context "rack app" do |rackup_path|
|
24
|
+
include Rack::Test::Methods
|
25
|
+
|
26
|
+
let(:app) {Rack::Builder.parse_file(File.expand_path(rackup_path, __dir__)).first}
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'rack_helper'
|
22
|
+
require 'utopia/redirector'
|
23
|
+
|
24
|
+
RSpec.describe Utopia::Redirector do
|
25
|
+
include_context "rack app", "redirector_spec.ru"
|
26
|
+
|
27
|
+
it "should be permanently moved" do
|
28
|
+
get "/a"
|
29
|
+
|
30
|
+
expect(last_response.status).to be == 301
|
31
|
+
expect(last_response.headers['Location']).to be == '/b'
|
32
|
+
expect(last_response.headers['Cache-Control']).to include("max-age=86400")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should be permanently moved" do
|
36
|
+
get "/"
|
37
|
+
|
38
|
+
expect(last_response.status).to be == 301
|
39
|
+
expect(last_response.headers['Location']).to be == '/c'
|
40
|
+
expect(last_response.headers['Cache-Control']).to include("max-age=86400")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should redirect on 404" do
|
44
|
+
get "/foo"
|
45
|
+
|
46
|
+
expect(last_response.status).to be == 404
|
47
|
+
expect(last_response.body).to be == "File not found :("
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should blow up if internal error redirect also fails" do
|
51
|
+
expect{get "/teapot"}.to raise_error Utopia::FailedRequestError
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
use Utopia::Redirector,
|
3
|
+
patterns: [
|
4
|
+
Utopia::Redirector::DIRECTORY_INDEX,
|
5
|
+
[:moved, "/a", "/b"],
|
6
|
+
],
|
7
|
+
strings: {
|
8
|
+
'/' => '/c',
|
9
|
+
},
|
10
|
+
errors: {
|
11
|
+
404 => "/error",
|
12
|
+
418 => "/teapot",
|
13
|
+
}
|
14
|
+
|
15
|
+
def error_handler(env)
|
16
|
+
request = Rack::Request.new(env)
|
17
|
+
if request.path_info == "/error"
|
18
|
+
[200, {}, ["File not found :("]]
|
19
|
+
elsif request.path_info == "/teapot"
|
20
|
+
[418, {}, ["I'm a teapot!"]]
|
21
|
+
else
|
22
|
+
[404, {}, []]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
run self.method(:error_handler)
|
data/utopia.gemspec
CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_dependency 'rack', '~> 1.6'
|
31
31
|
spec.add_dependency 'rack-cache', '~> 1.2.0'
|
32
32
|
|
33
|
-
spec.add_dependency 'http-accept', '~> 1.
|
33
|
+
spec.add_dependency 'http-accept', '~> 1.4.0'
|
34
34
|
|
35
35
|
spec.add_dependency 'mail', '~> 2.6.3'
|
36
36
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: utopia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: trenni
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 1.
|
75
|
+
version: 1.4.0
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 1.
|
82
|
+
version: 1.4.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: mail
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -290,10 +290,12 @@ files:
|
|
290
290
|
- spec/utopia/controller/respond_spec/errors/file-not-found.xnode
|
291
291
|
- spec/utopia/controller/rewrite_spec.rb
|
292
292
|
- spec/utopia/controller/sequence_spec.rb
|
293
|
+
- spec/utopia/controller/variables_spec.rb
|
293
294
|
- spec/utopia/exception_handler_spec.rb
|
294
295
|
- spec/utopia/exception_handler_spec.ru
|
295
296
|
- spec/utopia/exception_handler_spec/controller.rb
|
296
297
|
- spec/utopia/extensions_spec.rb
|
298
|
+
- spec/utopia/http/status_spec.rb
|
297
299
|
- spec/utopia/locale_spec.rb
|
298
300
|
- spec/utopia/localization_spec.rb
|
299
301
|
- spec/utopia/localization_spec.ru
|
@@ -312,7 +314,10 @@ files:
|
|
312
314
|
- spec/utopia/pages/test.txt
|
313
315
|
- spec/utopia/path/matcher_spec.rb
|
314
316
|
- spec/utopia/path_spec.rb
|
317
|
+
- spec/utopia/rack_helper.rb
|
315
318
|
- spec/utopia/rack_spec.rb
|
319
|
+
- spec/utopia/redirector_spec.rb
|
320
|
+
- spec/utopia/redirector_spec.ru
|
316
321
|
- spec/utopia/session_spec.rb
|
317
322
|
- spec/utopia/session_spec.ru
|
318
323
|
- spec/utopia/static_spec.rb
|
@@ -386,10 +391,12 @@ test_files:
|
|
386
391
|
- spec/utopia/controller/respond_spec/errors/file-not-found.xnode
|
387
392
|
- spec/utopia/controller/rewrite_spec.rb
|
388
393
|
- spec/utopia/controller/sequence_spec.rb
|
394
|
+
- spec/utopia/controller/variables_spec.rb
|
389
395
|
- spec/utopia/exception_handler_spec.rb
|
390
396
|
- spec/utopia/exception_handler_spec.ru
|
391
397
|
- spec/utopia/exception_handler_spec/controller.rb
|
392
398
|
- spec/utopia/extensions_spec.rb
|
399
|
+
- spec/utopia/http/status_spec.rb
|
393
400
|
- spec/utopia/locale_spec.rb
|
394
401
|
- spec/utopia/localization_spec.rb
|
395
402
|
- spec/utopia/localization_spec.ru
|
@@ -408,7 +415,10 @@ test_files:
|
|
408
415
|
- spec/utopia/pages/test.txt
|
409
416
|
- spec/utopia/path/matcher_spec.rb
|
410
417
|
- spec/utopia/path_spec.rb
|
418
|
+
- spec/utopia/rack_helper.rb
|
411
419
|
- spec/utopia/rack_spec.rb
|
420
|
+
- spec/utopia/redirector_spec.rb
|
421
|
+
- spec/utopia/redirector_spec.ru
|
412
422
|
- spec/utopia/session_spec.rb
|
413
423
|
- spec/utopia/session_spec.ru
|
414
424
|
- spec/utopia/static_spec.rb
|