utopia 1.2.4 → 1.3.0
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/.travis.yml +2 -2
- data/Gemfile +2 -0
- data/lib/utopia/controller.rb +1 -0
- data/lib/utopia/controller/action.rb +9 -15
- data/lib/utopia/controller/base.rb +1 -1
- data/lib/utopia/controller/respond.rb +177 -0
- data/lib/utopia/controller/rewrite.rb +2 -5
- data/lib/utopia/http.rb +6 -1
- data/lib/utopia/localization.rb +6 -11
- data/lib/utopia/static.rb +53 -48
- data/lib/utopia/version.rb +1 -1
- data/setup/site/.bowerrc +4 -0
- data/setup/site/pages/_static/utopia.svg +1 -0
- data/setup/site/pages/errors/controller.rb +9 -0
- data/setup/site/public/_static +1 -0
- data/spec/utopia/controller/action_spec.rb +14 -0
- data/spec/utopia/controller/respond_spec.rb +123 -0
- data/spec/utopia/controller/respond_spec.ru +6 -0
- data/spec/utopia/controller/respond_spec/api/controller.rb +26 -0
- data/spec/utopia/controller/respond_spec/errors/controller.rb +9 -0
- data/spec/utopia/controller/respond_spec/errors/file-not-found.xnode +7 -0
- data/utopia.gemspec +3 -1
- metadata +33 -5
- data/setup/site/pages/_static/utopia.svg +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 388fe83677fc504ecff20427f5838afdb1987a62
|
4
|
+
data.tar.gz: a60e58681a7329cf679ed40e0e95ae9e257553ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 556d111b28b4b31ad303a762fb4bba67bf0789df8ca56baee29539de9d3d600561f970f6542aa455a5c743330eb0c1dde75fd24dd6c12dd89424c6193280019f
|
7
|
+
data.tar.gz: fe16120edbe602d50746e735f641139e27eee266cc6f2a4a638b376fa79c321a1a89c6ec62346cefe92dccf594b6a53ad1b4cae7507b1c5a2a41bda2358b1c32
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/lib/utopia/controller.rb
CHANGED
@@ -21,15 +21,14 @@
|
|
21
21
|
module Utopia
|
22
22
|
class Controller
|
23
23
|
class Action < Hash
|
24
|
-
def initialize
|
25
|
-
@path = nil
|
24
|
+
def initialize(options = {}, &block)
|
26
25
|
@options = options
|
27
|
-
@callback =
|
26
|
+
@callback = block
|
28
27
|
|
29
|
-
super
|
28
|
+
super()
|
30
29
|
end
|
31
30
|
|
32
|
-
attr_accessor :
|
31
|
+
attr_accessor :callback, :options
|
33
32
|
|
34
33
|
def callback?
|
35
34
|
@callback != nil
|
@@ -40,15 +39,15 @@ module Utopia
|
|
40
39
|
end
|
41
40
|
|
42
41
|
def eql? other
|
43
|
-
super and @callback.eql? other.callback and @options.eql? other.options
|
42
|
+
super and @callback.eql? other.callback and @options.eql? other.options
|
44
43
|
end
|
45
44
|
|
46
45
|
def hash
|
47
|
-
[super, callback, options
|
46
|
+
[super, @callback, @options].hash
|
48
47
|
end
|
49
48
|
|
50
49
|
def == other
|
51
|
-
super and @callback == other.callback and @options == other.options
|
50
|
+
super and @callback == other.callback and @options == other.options
|
52
51
|
end
|
53
52
|
|
54
53
|
protected
|
@@ -91,22 +90,17 @@ module Utopia
|
|
91
90
|
def define(path, **options, &callback)
|
92
91
|
current = self
|
93
92
|
|
94
|
-
path.
|
93
|
+
path.reverse_each do |name|
|
95
94
|
current = (current[name.to_sym] ||= Action.new)
|
96
95
|
end
|
97
96
|
|
98
|
-
current.path = path
|
99
97
|
current.options = options
|
100
98
|
current.callback = callback
|
101
99
|
|
102
100
|
return current
|
103
101
|
end
|
104
102
|
|
105
|
-
def
|
106
|
-
@callback ? @callback.arity : 0
|
107
|
-
end
|
108
|
-
|
109
|
-
def invoke!(controller, *arguments)
|
103
|
+
def call(controller, *arguments)
|
110
104
|
controller.instance_exec(*arguments, self, &@callback)
|
111
105
|
end
|
112
106
|
|
@@ -0,0 +1,177 @@
|
|
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 '../http'
|
22
|
+
require_relative '../path/matcher'
|
23
|
+
|
24
|
+
module Utopia
|
25
|
+
class Controller
|
26
|
+
# This controller layer provides a convenient way to respond to different requested content types.
|
27
|
+
module Respond
|
28
|
+
def self.prepended(base)
|
29
|
+
base.extend(ClassMethods)
|
30
|
+
end
|
31
|
+
|
32
|
+
module Converter
|
33
|
+
def self.update_response(response, updated_headers)
|
34
|
+
status, headers, body = response
|
35
|
+
|
36
|
+
# Generate a new body:
|
37
|
+
body = body.collect{|content| yield content}
|
38
|
+
|
39
|
+
# Update the headers with the requested content type:
|
40
|
+
headers = headers.merge(updated_headers)
|
41
|
+
|
42
|
+
return [status, headers, body]
|
43
|
+
end
|
44
|
+
|
45
|
+
class Callback < Struct.new(:content_type, :block)
|
46
|
+
def headers
|
47
|
+
{HTTP::CONTENT_TYPE => self.content_type}
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(context, response, media_range)
|
51
|
+
Converter.update_response(response, headers) do |content|
|
52
|
+
context.instance_exec(content, media_range, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.new(*args)
|
58
|
+
Callback.new(*args)
|
59
|
+
end
|
60
|
+
|
61
|
+
# To accept incoming requests with content-type JSON (e.g. POST with JSON data), consider using `Rack::PostBodyContentTypeParser`.
|
62
|
+
module ToJSON
|
63
|
+
APPLICATION_JSON = 'application/json'.freeze
|
64
|
+
HEADERS = {HTTP::CONTENT_TYPE => APPLICATION_JSON}.freeze
|
65
|
+
|
66
|
+
def self.content_type
|
67
|
+
APPLICATION_JSON
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.serialize(content, media_range)
|
71
|
+
options = {}
|
72
|
+
|
73
|
+
if version = media_range.parameters['version']
|
74
|
+
options[:version] = version.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
return content.to_json(options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.call(context, response, media_range)
|
81
|
+
Converter.update_response(response, HEADERS) do |content|
|
82
|
+
self.serialize(content, media_range)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Responder
|
89
|
+
HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
|
90
|
+
NOT_ACCEPTABLE_RESPONSE = [406, {}, []].freeze
|
91
|
+
|
92
|
+
def initialize
|
93
|
+
@converters = HTTP::Accept::MediaTypes::Map.new
|
94
|
+
@otherwise = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def freeze
|
98
|
+
@converters.freeze
|
99
|
+
@otherwise.freeze
|
100
|
+
|
101
|
+
super
|
102
|
+
end
|
103
|
+
|
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
|
+
# Add a converter for the specified content type. Call the block with the response content if the request accepts the specified content_type.
|
114
|
+
def with(content_type, &block)
|
115
|
+
@converters << Converter::Callback.new(content_type, block)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Add a converter for JSON when requests accept 'application/json'
|
119
|
+
def with_json
|
120
|
+
@converters << Converter::ToJSON
|
121
|
+
end
|
122
|
+
|
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
|
+
def call(context, request, path, response)
|
134
|
+
media_types = browser_preferred_media_types(request.env)
|
135
|
+
|
136
|
+
converter, media_range = @converters.for(media_types)
|
137
|
+
|
138
|
+
if converter
|
139
|
+
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
|
+
else
|
150
|
+
NOT_ACCEPTABLE_RESPONSE
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
module ClassMethods
|
156
|
+
def respond
|
157
|
+
@responder ||= Responder.new
|
158
|
+
end
|
159
|
+
|
160
|
+
def response_for(context, request, path, response)
|
161
|
+
if @responder
|
162
|
+
@responder.call(context, request, path, response)
|
163
|
+
else
|
164
|
+
response
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Rewrite the path before processing the request if possible.
|
170
|
+
def passthrough(request, path)
|
171
|
+
if response = super
|
172
|
+
self.class.response_for(self, request, path, response)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -23,9 +23,6 @@ require_relative '../path/matcher'
|
|
23
23
|
|
24
24
|
module Utopia
|
25
25
|
class Controller
|
26
|
-
class RewriteError < ArgumentError
|
27
|
-
end
|
28
|
-
|
29
26
|
module Rewrite
|
30
27
|
def self.prepended(base)
|
31
28
|
base.extend(ClassMethods)
|
@@ -101,7 +98,7 @@ module Utopia
|
|
101
98
|
return path
|
102
99
|
end
|
103
100
|
|
104
|
-
def
|
101
|
+
def call(context, request, path)
|
105
102
|
path.components = apply(context, request, path).components
|
106
103
|
end
|
107
104
|
end
|
@@ -113,7 +110,7 @@ module Utopia
|
|
113
110
|
|
114
111
|
def rewrite_request(controller, request, path)
|
115
112
|
if @rewriter
|
116
|
-
@rewriter.
|
113
|
+
@rewriter.call(controller, request, path)
|
117
114
|
end
|
118
115
|
end
|
119
116
|
end
|
data/lib/utopia/http.rb
CHANGED
@@ -20,8 +20,12 @@
|
|
20
20
|
|
21
21
|
require 'rack'
|
22
22
|
|
23
|
+
require 'http/accept'
|
24
|
+
|
23
25
|
module Utopia
|
24
26
|
module HTTP
|
27
|
+
Accept = ::HTTP::Accept
|
28
|
+
|
25
29
|
# A list of commonly used HTTP status codes.
|
26
30
|
# For help choosing the right status code, see http://racksburg.com/choosing-an-http-status-code/
|
27
31
|
STATUS_CODES = {
|
@@ -78,10 +82,11 @@ module Utopia
|
|
78
82
|
500 => 'Internal Server Error'.freeze,
|
79
83
|
501 => 'Not Implemented'.freeze,
|
80
84
|
503 => 'Service Unavailable'.freeze
|
81
|
-
}
|
85
|
+
}.merge(Rack::Utils::HTTP_STATUS_CODES)
|
82
86
|
|
83
87
|
CONTENT_TYPE = 'Content-Type'.freeze
|
84
88
|
LOCATION = 'Location'.freeze
|
89
|
+
# ACCEPT = 'Accept'.freeze
|
85
90
|
|
86
91
|
# A small HTTP status wrapper that verifies the status code within a given range.
|
87
92
|
class Status
|
data/lib/utopia/localization.rb
CHANGED
@@ -68,7 +68,7 @@ module Utopia
|
|
68
68
|
def initialize(app, **options)
|
69
69
|
@app = app
|
70
70
|
|
71
|
-
@all_locales = options[:locales]
|
71
|
+
@all_locales = HTTP::Accept::Languages::Locales.new(options[:locales])
|
72
72
|
|
73
73
|
# Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'].
|
74
74
|
unless @default_locales = options[:default_locales]
|
@@ -85,8 +85,6 @@ module Utopia
|
|
85
85
|
|
86
86
|
@nonlocalized = options.fetch(:nonlocalized, [])
|
87
87
|
|
88
|
-
# puts "All:#{@all_locales.inspect} defaults:#{@default_locales.inspect} default:#{default_locale}"
|
89
|
-
|
90
88
|
self.freeze
|
91
89
|
end
|
92
90
|
|
@@ -142,9 +140,7 @@ module Utopia
|
|
142
140
|
def request_preferred_locale(env)
|
143
141
|
path = Path[env[Rack::PATH_INFO]]
|
144
142
|
|
145
|
-
if @all_locales.
|
146
|
-
request_locale = path.first
|
147
|
-
|
143
|
+
if request_locale = @all_locales.patterns[path.first]
|
148
144
|
# Remove the localization prefix:
|
149
145
|
path.delete_at(0)
|
150
146
|
|
@@ -158,12 +154,11 @@ module Utopia
|
|
158
154
|
# No user prefered languages:
|
159
155
|
return [] unless accept_languages
|
160
156
|
|
161
|
-
|
162
|
-
|
163
|
-
}.sort{|a, b| b[1] <=> a[1]}.collect(&:first)
|
157
|
+
# Extract the ordered list of languages:
|
158
|
+
languages = HTTP::Accept::Languages.parse(accept_languages)
|
164
159
|
|
165
|
-
# Returns available languages based on the order
|
166
|
-
return
|
160
|
+
# Returns available languages based on the order languages:
|
161
|
+
return @all_locales & languages
|
167
162
|
end
|
168
163
|
|
169
164
|
def nonlocalized?(env)
|
data/lib/utopia/static.rb
CHANGED
@@ -61,7 +61,58 @@ module Utopia
|
|
61
61
|
:media, :text, :archive, :images, :fonts
|
62
62
|
]
|
63
63
|
}
|
64
|
-
|
64
|
+
|
65
|
+
class MimeTypeLoader
|
66
|
+
def initialize(library)
|
67
|
+
@extensions = {}
|
68
|
+
@library = library
|
69
|
+
end
|
70
|
+
|
71
|
+
attr :extensions
|
72
|
+
|
73
|
+
def self.extensions_for(types, library = MIME_TYPES)
|
74
|
+
loader = self.new(library)
|
75
|
+
loader.expand(types)
|
76
|
+
return loader.extensions
|
77
|
+
end
|
78
|
+
|
79
|
+
def extract_extensions(mime_types)
|
80
|
+
mime_types.select{|mime_type| !mime_type.obsolete?}.each do |mime_type|
|
81
|
+
mime_type.extensions.each do |ext|
|
82
|
+
@extensions["." + ext] = mime_type.content_type
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def expand(types)
|
88
|
+
types.each do |type|
|
89
|
+
current_count = @extensions.size
|
90
|
+
|
91
|
+
begin
|
92
|
+
case type
|
93
|
+
when Symbol
|
94
|
+
self.expand(MIME_TYPES[type])
|
95
|
+
when Array
|
96
|
+
@extensions["." + type[0]] = type[1]
|
97
|
+
when String
|
98
|
+
self.extract_extensions MIME::Types.of(type)
|
99
|
+
when Regexp
|
100
|
+
self.extract_extensions MIME::Types[type]
|
101
|
+
when MIME::Type
|
102
|
+
self.extract_extensions.call([type])
|
103
|
+
end
|
104
|
+
rescue
|
105
|
+
LOG.error{"#{self.class.name}: Error while processing #{type.inspect}!"}
|
106
|
+
raise $!
|
107
|
+
end
|
108
|
+
|
109
|
+
if @extensions.size == current_count
|
110
|
+
LOG.warn{"#{self.class.name}: Could not find any mime type for #{type.inspect}"}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
65
116
|
private
|
66
117
|
|
67
118
|
class LocalFile
|
@@ -151,59 +202,13 @@ module Utopia
|
|
151
202
|
end
|
152
203
|
end
|
153
204
|
|
154
|
-
def load_mime_types(types)
|
155
|
-
result = {}
|
156
|
-
|
157
|
-
extract_extensions = lambda do |mime_type|
|
158
|
-
# LOG.info "Extracting #{mime_type.inspect}"
|
159
|
-
mime_type.extensions.each{|ext| result["." + ext] = mime_type.content_type}
|
160
|
-
end
|
161
|
-
|
162
|
-
types.each do |type|
|
163
|
-
current_count = result.size
|
164
|
-
# LOG.info "Processing #{type.inspect}"
|
165
|
-
|
166
|
-
begin
|
167
|
-
case type
|
168
|
-
when Symbol
|
169
|
-
result = load_mime_types(MIME_TYPES[type]).merge(result)
|
170
|
-
when Array
|
171
|
-
result["." + type[0]] = type[1]
|
172
|
-
when String
|
173
|
-
MIME::Types.of(type).select{|mime_type| !mime_type.obsolete?}.each do |mime_type|
|
174
|
-
extract_extensions.call(mime_type)
|
175
|
-
end
|
176
|
-
when Regexp
|
177
|
-
MIME::Types[type].select{|mime_type| !mime_type.obsolete?}.each do |mime_type|
|
178
|
-
extract_extensions.call(mime_type)
|
179
|
-
end
|
180
|
-
when MIME::Type
|
181
|
-
extract_extensions.call(type)
|
182
|
-
end
|
183
|
-
rescue
|
184
|
-
LOG.error "#{self.class.name}: Error while processing #{type.inspect}!"
|
185
|
-
raise $!
|
186
|
-
end
|
187
|
-
|
188
|
-
if result.size == current_count
|
189
|
-
LOG.warn "#{self.class.name}: Could not find any mime type for #{type.inspect}"
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
return result
|
194
|
-
end
|
195
|
-
|
196
205
|
public
|
197
206
|
|
198
207
|
def initialize(app, **options)
|
199
208
|
@app = app
|
200
209
|
@root = (options[:root] || Utopia::default_root).freeze
|
201
210
|
|
202
|
-
|
203
|
-
@extensions = load_mime_types(options[:types])
|
204
|
-
else
|
205
|
-
@extensions = load_mime_types(MIME_TYPES[:default])
|
206
|
-
end
|
211
|
+
@extensions = MimeTypeLoader.extensions_for(options[:types] || MIME_TYPES[:default])
|
207
212
|
|
208
213
|
@cache_control = (options[:cache_control] || "public, max-age=3600")
|
209
214
|
|
data/lib/utopia/version.rb
CHANGED
data/setup/site/.bowerrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
../../../../materials/utopia.svg
|
@@ -0,0 +1 @@
|
|
1
|
+
../pages/_static
|
@@ -24,6 +24,20 @@ require 'utopia/controller'
|
|
24
24
|
|
25
25
|
module Utopia::Controller::ActionSpec
|
26
26
|
describe Utopia::Controller::Action do
|
27
|
+
it "should be a hash key" do
|
28
|
+
a = Utopia::Controller::Action.new
|
29
|
+
b = Utopia::Controller::Action.new
|
30
|
+
c = Utopia::Controller::Action.new {sleep}
|
31
|
+
|
32
|
+
expect(a).to be == b
|
33
|
+
expect(a.hash).to be == b.hash
|
34
|
+
expect(a).to be_eql b
|
35
|
+
|
36
|
+
expect(a).to_not be == c
|
37
|
+
expect(a.hash).to_not be == c.hash
|
38
|
+
expect(a).to_not be_eql c
|
39
|
+
end
|
40
|
+
|
27
41
|
it "should resolve callbacks" do
|
28
42
|
actions = Utopia::Controller::Action.new
|
29
43
|
|
@@ -0,0 +1,123 @@
|
|
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 'rack/test'
|
24
|
+
require 'rack/mock'
|
25
|
+
require 'json'
|
26
|
+
|
27
|
+
require 'utopia/content'
|
28
|
+
require 'utopia/controller'
|
29
|
+
|
30
|
+
module Utopia::Controller::RespondSpec
|
31
|
+
describe Utopia::Controller do
|
32
|
+
class TestController < Utopia::Controller::Base
|
33
|
+
prepend Utopia::Controller::Respond
|
34
|
+
|
35
|
+
respond.with("application/json") do |content|
|
36
|
+
JSON::dump(content)
|
37
|
+
end
|
38
|
+
|
39
|
+
respond.with("text/plain") do |content|
|
40
|
+
content.inspect
|
41
|
+
end
|
42
|
+
|
43
|
+
on 'fetch' do |request, path|
|
44
|
+
success! content: {user_id: 10}
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.uri_path
|
48
|
+
Utopia::Path['/']
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
let(:controller) {TestController.new}
|
53
|
+
|
54
|
+
def mock_request(*args)
|
55
|
+
request = Rack::Request.new(Rack::MockRequest.env_for(*args))
|
56
|
+
return request, Utopia::Path[request.path_info]
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should serialize response as JSON" do
|
60
|
+
request, path = mock_request("/fetch")
|
61
|
+
relative_path = path - controller.class.uri_path
|
62
|
+
|
63
|
+
request.env['HTTP_ACCEPT'] = "application/json"
|
64
|
+
|
65
|
+
status, headers, body = controller.process!(request, relative_path)
|
66
|
+
|
67
|
+
expect(status).to be == 200
|
68
|
+
expect(headers['Content-Type']).to be == "application/json"
|
69
|
+
expect(body.join).to be == '{"user_id":10}'
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should serialize response as text" do
|
73
|
+
request, path = mock_request("/fetch")
|
74
|
+
relative_path = path - controller.class.uri_path
|
75
|
+
|
76
|
+
request.env['HTTP_ACCEPT'] = "text/*"
|
77
|
+
|
78
|
+
status, headers, body = controller.process!(request, relative_path)
|
79
|
+
|
80
|
+
expect(status).to be == 200
|
81
|
+
expect(headers['Content-Type']).to be == "text/plain"
|
82
|
+
expect(body.join).to be == '{:user_id=>10}'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe Utopia::Controller do
|
87
|
+
include Rack::Test::Methods
|
88
|
+
|
89
|
+
let(:app) {Rack::Builder.parse_file(File.expand_path('respond_spec.ru', __dir__)).first}
|
90
|
+
|
91
|
+
it "should get html error page" do
|
92
|
+
get '/errors/file-not-found'
|
93
|
+
|
94
|
+
expect(last_response.status).to be == 200
|
95
|
+
expect(last_response.headers['Content-Type']).to be == 'text/html'
|
96
|
+
expect(last_response.body).to be_include "<heading>File Not Found</heading>"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should get json error response" do
|
100
|
+
get '/errors/file-not-found', nil, {'HTTP_ACCEPT' => "application/json"}
|
101
|
+
|
102
|
+
expect(last_response.status).to be == 404
|
103
|
+
expect(last_response.headers['Content-Type']).to be == 'application/json'
|
104
|
+
expect(last_response.body).to be == '{"message":"File not found"}'
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should get version 1 response" do
|
108
|
+
get '/api/fetch', nil, {'HTTP_ACCEPT' => "application/json;version=1"}
|
109
|
+
|
110
|
+
expect(last_response.status).to be == 200
|
111
|
+
expect(last_response.headers['Content-Type']).to be == 'application/json'
|
112
|
+
expect(last_response.body).to be == '{"message":"Hello World"}'
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should get version 2 response" do
|
116
|
+
get '/api/fetch', nil, {'HTTP_ACCEPT' => "application/json;version=2"}
|
117
|
+
|
118
|
+
expect(last_response.status).to be == 200
|
119
|
+
expect(last_response.headers['Content-Type']).to be == 'application/json'
|
120
|
+
expect(last_response.body).to be == '{"message":"Goodbye World"}'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
prepend Respond
|
3
|
+
|
4
|
+
respond.with_json
|
5
|
+
|
6
|
+
class VersionedResponse
|
7
|
+
def to_json(options = {})
|
8
|
+
JSON::dump(self.as_json(options))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Modelled after http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
|
12
|
+
def as_json(options = {})
|
13
|
+
if options[:version] == '1'
|
14
|
+
{"message" => "Hello World"}
|
15
|
+
elsif options[:version] == '2'
|
16
|
+
{"message" => "Goodbye World"}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# To get different verions of the response, use:
|
22
|
+
# Accept: application/json;version=1
|
23
|
+
# Accept: application/json;version=2
|
24
|
+
on 'fetch' do
|
25
|
+
success! content: VersionedResponse.new
|
26
|
+
end
|
data/utopia.gemspec
CHANGED
@@ -30,7 +30,9 @@ 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 '
|
33
|
+
spec.add_dependency 'http-accept', '~> 1.1.3'
|
34
|
+
|
35
|
+
spec.add_dependency 'mail', '~> 2.6.3'
|
34
36
|
|
35
37
|
spec.add_dependency 'concurrent-ruby', '~> 1.0.0'
|
36
38
|
|
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.
|
4
|
+
version: 1.3.0
|
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-
|
11
|
+
date: 2016-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: trenni
|
@@ -66,20 +66,34 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 1.2.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: http-accept
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.1.3
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.1.3
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: mail
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
87
|
- - "~>"
|
74
88
|
- !ruby/object:Gem::Version
|
75
|
-
version: 2.6.
|
89
|
+
version: 2.6.3
|
76
90
|
type: :runtime
|
77
91
|
prerelease: false
|
78
92
|
version_requirements: !ruby/object:Gem::Requirement
|
79
93
|
requirements:
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
|
-
version: 2.6.
|
96
|
+
version: 2.6.3
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: concurrent-ruby
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -182,6 +196,7 @@ files:
|
|
182
196
|
- lib/utopia/controller.rb
|
183
197
|
- lib/utopia/controller/action.rb
|
184
198
|
- lib/utopia/controller/base.rb
|
199
|
+
- lib/utopia/controller/respond.rb
|
185
200
|
- lib/utopia/controller/rewrite.rb
|
186
201
|
- lib/utopia/controller/variables.rb
|
187
202
|
- lib/utopia/exception_handler.rb
|
@@ -209,6 +224,7 @@ files:
|
|
209
224
|
- materials/utopia.svg
|
210
225
|
- setup/.bowerrc
|
211
226
|
- setup/server/git/hooks/post-receive
|
227
|
+
- setup/site/.bowerrc
|
212
228
|
- setup/site/Gemfile
|
213
229
|
- setup/site/README.md
|
214
230
|
- setup/site/Rakefile
|
@@ -222,10 +238,12 @@ files:
|
|
222
238
|
- setup/site/pages/_static/site.css
|
223
239
|
- setup/site/pages/_static/utopia-background.svg
|
224
240
|
- setup/site/pages/_static/utopia.svg
|
241
|
+
- setup/site/pages/errors/controller.rb
|
225
242
|
- setup/site/pages/errors/exception.xnode
|
226
243
|
- setup/site/pages/errors/file-not-found.xnode
|
227
244
|
- setup/site/pages/links.yaml
|
228
245
|
- setup/site/pages/welcome/index.xnode
|
246
|
+
- setup/site/public/_static
|
229
247
|
- setup/site/public/readme.txt
|
230
248
|
- setup/site/tmp/readme.txt
|
231
249
|
- spec/utopia/content/link_spec.rb
|
@@ -265,6 +283,11 @@ files:
|
|
265
283
|
- spec/utopia/controller/middleware_spec/empty/controller.rb
|
266
284
|
- spec/utopia/controller/middleware_spec/redirect/controller.rb
|
267
285
|
- spec/utopia/controller/middleware_spec/redirect/test/controller.rb
|
286
|
+
- spec/utopia/controller/respond_spec.rb
|
287
|
+
- spec/utopia/controller/respond_spec.ru
|
288
|
+
- spec/utopia/controller/respond_spec/api/controller.rb
|
289
|
+
- spec/utopia/controller/respond_spec/errors/controller.rb
|
290
|
+
- spec/utopia/controller/respond_spec/errors/file-not-found.xnode
|
268
291
|
- spec/utopia/controller/rewrite_spec.rb
|
269
292
|
- spec/utopia/controller/sequence_spec.rb
|
270
293
|
- spec/utopia/exception_handler_spec.rb
|
@@ -314,7 +337,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
314
337
|
version: '0'
|
315
338
|
requirements: []
|
316
339
|
rubyforge_project:
|
317
|
-
rubygems_version: 2.
|
340
|
+
rubygems_version: 2.5.2
|
318
341
|
signing_key:
|
319
342
|
specification_version: 4
|
320
343
|
summary: Utopia is a framework for building dynamic content-driven websites.
|
@@ -356,6 +379,11 @@ test_files:
|
|
356
379
|
- spec/utopia/controller/middleware_spec/empty/controller.rb
|
357
380
|
- spec/utopia/controller/middleware_spec/redirect/controller.rb
|
358
381
|
- spec/utopia/controller/middleware_spec/redirect/test/controller.rb
|
382
|
+
- spec/utopia/controller/respond_spec.rb
|
383
|
+
- spec/utopia/controller/respond_spec.ru
|
384
|
+
- spec/utopia/controller/respond_spec/api/controller.rb
|
385
|
+
- spec/utopia/controller/respond_spec/errors/controller.rb
|
386
|
+
- spec/utopia/controller/respond_spec/errors/file-not-found.xnode
|
359
387
|
- spec/utopia/controller/rewrite_spec.rb
|
360
388
|
- spec/utopia/controller/sequence_spec.rb
|
361
389
|
- spec/utopia/exception_handler_spec.rb
|
@@ -1 +0,0 @@
|
|
1
|
-
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 420 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><rect x="0" y="0" width="420" height="56" style="fill:#f79433;"/><rect x="0" y="56" width="420" height="24" style="fill:#4e8dd8;"/><g><path d="M75.145,70.819c2.37,-3.097 4.173,-6.921 5.111,-11.365c0.91,-4.318 1.498,-9.261 1.498,-14.692l0,-44.762l-62.754,0l0,44.762c0,2.628 0.244,5.333 0.407,8.035c0.168,2.782 0.674,5.515 1.345,8.118c0.68,2.644 1.739,5.173 3.067,7.517c1.363,2.405 3.263,4.526 5.609,6.303c2.319,1.755 5.245,3.163 8.677,4.172c1.617,0.478 3.416,1.093 5.354,1.093l13.856,0c3.071,0 5.797,-1.058 8.131,-2.001c4.042,-1.631 7.305,-4.049 9.699,-7.18Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M151.481,18.701l0,-18.701l-62.754,-0.022l0,18.723l22.246,0l0.02,61.299l17.93,0l-0.02,-61.299l22.578,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M229.926,39.999c0,-22.051 -16.979,-39.992 -37.852,-39.992c-20.872,0 -37.851,17.942 -37.851,39.992c0,22.054 16.979,39.994 37.851,39.994c20.873,0 37.852,-17.94 37.852,-39.994Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M269.238,50.909c9.717,0 17.181,-2.066 22.183,-6.395c5.087,-4.399 7.667,-10.942 7.667,-19.575c0,-3.257 -0.393,-5.962 -1.167,-8.476c-0.778,-2.528 -1.883,-4.934 -3.281,-6.814c-1.401,-1.882 -3.098,-3.458 -5.045,-4.703c-1.895,-1.21 -4.003,-2.198 -6.264,-2.943c-2.239,-0.737 -4.64,-1.263 -7.139,-1.56c-2.464,-0.292 -5.016,-0.443 -7.587,-0.443l-29.468,0l0,80l17.93,0l0,-29.091l12.171,0Z" style="fill:#fff;fill-rule:nonzero;"/><rect x="304.879" y="0" width="17.93" height="80" style="fill:#fff;"/><path d="M362.589,0l-29.477,80l75.888,0l-31.247,-80l-15.164,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></g></svg>
|