utopia 1.3.1 → 1.3.2

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