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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5899d352ea4513b585949a124dfba139c9bd9aa5
4
- data.tar.gz: fbd5fa65a09a3b3238bc04ea1eca9d1e56c10fe8
3
+ metadata.gz: 27fc0800c0d44763978e981de97541dbf2d0d2ef
4
+ data.tar.gz: 346b6f43e6e056667c8ab6c30b8b185bfb1cdd58
5
5
  SHA512:
6
- metadata.gz: deea556aa6cc2fda2e28af33e986b2b8149b1a1afb35111c3c51bbe0ed0eff1e1b3da1cae7dc14e941a7f62d39c8996c70ac56163c3dca3d5339a34cbc81fbe0
7
- data.tar.gz: 22d7503edae10b6f21e694762a1d23c3502e7f23275839e1192ad4dd06ba5fe33ec28abd3f8ecd6261a4af94e94cfe7b6232f7af1302644898ca1e7cb146f3cb
6
+ metadata.gz: e9eedc27a561f991f9d3e637371d51770f31eeeb42d4a46a85f4461a2211b8985b5f86af4f3e5da8115ce45f6b13bfdd12fdc2eafc4b0fa9083dac44d1b6f0af
7
+ data.tar.gz: d9feff14bb7a57d3d0df656537751abe5e1fb9b1608f49e870a8a4e2f17650e80e41be6601b8411395b60b33faa9b980f75d3b3862455e9c77850d2a07886a4c
@@ -47,17 +47,15 @@ module Utopia
47
47
  class Controller
48
48
  CONTROLLER_RB = 'controller.rb'.freeze
49
49
 
50
- def initialize(app, **options)
50
+ def initialize(app, root: nil, cache_controllers: false)
51
51
  @app = app
52
- @root = options[:root] || Utopia::default_root
52
+ @root = root || Utopia::default_root
53
53
 
54
- if options[:cache_controllers]
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
- class Callback < Struct.new(:content_type, :block)
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
- media_types = browser_preferred_media_types(request.env)
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 = @controllers.last
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
- # This ensures that most variables will be at the top and controllers can naturally interactive with instance variables.
34
- controller.copy_instance_variables(top) if top
40
+ return self
35
41
  end
36
-
37
- def fetch(key)
38
- @controllers.reverse_each do |controller|
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 key
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
- @controllers.each do |controller|
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) { nil }
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.puts "<!DOCTYPE html><html><head><title>Fatal Error</title></head><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 [400, {HTTP::CONTENT_TYPE => "text/html"}, body]
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
- # An error has occurred, log it:
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
- # ACCEPT = 'Accept'.freeze
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
- class Locale < Struct.new(:language, :country, :variant)
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
@@ -26,8 +26,6 @@ require_relative 'path'
26
26
  require_relative 'extensions/rack'
27
27
 
28
28
  module Utopia
29
- LOG = Logger.new($stderr)
30
-
31
29
  PAGES_PATH = 'pages'.freeze
32
30
 
33
31
  # This is used for shared controller variables which get consumed by the content middleware:
@@ -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 redirect(uri, match_data)
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
- return uri.call(match_data)
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
- LOG.error{"#{self.class.name}: Error while processing #{type.inspect}!"}
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
- LOG.warn{"#{self.class.name}: Could not find any mime type for #{type.inspect}"}
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
- # LOG.info("Requesting ranges: #{ranges.inspect} (#{size})")
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
- # LOG.debug {"Serving file #{full_path.inspect}, range #{@range.inspect}"}
201
+ # puts "Serving file #{full_path.inspect}, range #{@range.inspect}"
200
202
 
201
203
  return response
202
204
  end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
- VERSION = "1.3.1"
22
+ VERSION = "1.3.2"
23
23
  end
@@ -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
- get '/errors/file-not-found', nil, {'HTTP_ACCEPT' => "application/json"}
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
- get '/api/fetch', nil, {'HTTP_ACCEPT' => "application/json;version=1"}
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
- get '/api/fetch', nil, {'HTTP_ACCEPT' => "application/json;version=2"}
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,6 +1,5 @@
1
1
 
2
2
  prepend Respond
3
-
4
3
  respond.with_json
5
4
 
6
5
  class VersionedResponse
@@ -14,6 +13,8 @@ class VersionedResponse
14
13
  {"message" => "Hello World"}
15
14
  elsif options[:version] == '2'
16
15
  {"message" => "Goodbye World"}
16
+ else
17
+ {}
17
18
  end
18
19
  end
19
20
  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"=>:value}
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 == 400
39
- expect(last_response.body).to be_include 'Fatal Error'
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
@@ -6,6 +6,7 @@ on 'blow' do
6
6
  raise TharSheBlows.new("Arrrh!")
7
7
  end
8
8
 
9
+ # The ExceptionHandler middleware will redirect here when an exception occurs. If this also fails, things get ugly.
9
10
  on 'exception' do |request|
10
11
  if request['fatal']
11
12
  raise TharSheBlows.new("Yarrh!")
@@ -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
@@ -21,28 +21,36 @@
21
21
 
22
22
  require 'utopia/locale'
23
23
 
24
- module Utopia::LocaleSpec
25
- describe Utopia::Locale do
26
- it "should load from string" do
27
- locale = Utopia::Locale.load('en-US')
28
-
29
- expect(locale.language).to be == 'en'
30
- expect(locale.country).to be == 'US'
31
- expect(locale.variant).to be == nil
32
- end
33
-
34
- it "should load from nil and return nil" do
35
- expect(Utopia::Locale.load(nil)).to be == nil
36
- end
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
- it "should dump nil and give nil" do
39
- expect(Utopia::Locale.dump(nil)).to be == nil
40
- end
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
- it "should dump locale and give string" do
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.2.0'
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.1
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-16 00:00:00.000000000 Z
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.2.0
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.2.0
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