utopia 1.0.11 → 1.1.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/.rspec +2 -0
- data/.travis.yml +1 -0
- data/bin/utopia +1 -1
- data/lib/utopia/content.rb +10 -12
- data/lib/utopia/content/link.rb +8 -11
- data/lib/utopia/content/links.rb +1 -1
- data/lib/utopia/content/node.rb +8 -7
- data/lib/utopia/controller.rb +40 -39
- data/lib/utopia/controller/action.rb +14 -12
- data/lib/utopia/controller/base.rb +31 -32
- data/lib/utopia/controller/rewrite.rb +104 -0
- data/lib/utopia/exception_handler.rb +1 -1
- data/lib/utopia/extensions/rack.rb +21 -21
- data/lib/utopia/http.rb +14 -9
- data/lib/utopia/localization.rb +1 -1
- data/lib/utopia/middleware.rb +3 -0
- data/lib/utopia/path.rb +81 -25
- data/lib/utopia/path/matcher.rb +94 -0
- data/lib/utopia/redirector.rb +4 -4
- data/lib/utopia/session/encrypted_cookie.rb +1 -1
- data/lib/utopia/static.rb +1 -1
- data/lib/utopia/tags/node.rb +1 -1
- data/lib/utopia/version.rb +1 -1
- data/setup/site/config.ru +1 -1
- data/spec/utopia/content/link_spec.rb +8 -8
- data/spec/utopia/content/node_spec.rb +1 -1
- data/spec/utopia/content_spec.rb +3 -3
- data/spec/utopia/content_spec.ru +1 -1
- data/spec/utopia/controller/action_spec.rb +61 -0
- data/spec/utopia/controller/middleware_spec.rb +71 -0
- data/spec/utopia/controller/middleware_spec.ru +4 -0
- data/spec/utopia/controller/middleware_spec/controller/controller.rb +24 -0
- data/spec/utopia/{pages → controller/middleware_spec}/controller/index.xnode +0 -0
- data/spec/utopia/{pages → controller/middleware_spec}/controller/nested/controller.rb +0 -0
- data/spec/utopia/controller/rewrite_spec.rb +66 -0
- data/spec/utopia/{controller_spec.rb → controller/sequence_spec.rb} +11 -84
- data/spec/utopia/exception_handler_spec.rb +3 -3
- data/spec/utopia/exception_handler_spec.ru +2 -2
- data/spec/utopia/exception_handler_spec/controller.rb +15 -0
- data/spec/utopia/path/matcher_spec.rb +65 -0
- data/spec/utopia/rack_spec.rb +0 -12
- data/utopia.gemspec +1 -1
- metadata +27 -14
- data/spec/utopia/controller_spec.ru +0 -4
- data/spec/utopia/pages/controller/controller.rb +0 -43
@@ -0,0 +1,104 @@
|
|
1
|
+
# Copyright, 2014, 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
|
+
class RewriteError < ArgumentError
|
27
|
+
end
|
28
|
+
|
29
|
+
module Rewrite
|
30
|
+
def self.prepended(base)
|
31
|
+
base.extend(ClassMethods)
|
32
|
+
end
|
33
|
+
|
34
|
+
class Rule
|
35
|
+
def initialize(arguments, block)
|
36
|
+
@arguments = arguments
|
37
|
+
@block = block
|
38
|
+
end
|
39
|
+
|
40
|
+
attr :arguments
|
41
|
+
attr :block
|
42
|
+
|
43
|
+
def apply_match_to_context(match_data, context)
|
44
|
+
match_data.names.each do |name|
|
45
|
+
context.instance_variable_set("@#{name}", match_data[name])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ExtractPrefixRule < Rule
|
51
|
+
def apply(context, request, path)
|
52
|
+
@matcher ||= Path::Matcher.new(@arguments)
|
53
|
+
|
54
|
+
if match_data = @matcher.match(path)
|
55
|
+
apply_match_to_context(match_data, context)
|
56
|
+
|
57
|
+
if @block
|
58
|
+
context.instance_exec(request, path, match_data, &@block)
|
59
|
+
end
|
60
|
+
|
61
|
+
return match_data.post_match
|
62
|
+
else
|
63
|
+
return path
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Rewriter
|
69
|
+
def initialize
|
70
|
+
@rules = []
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_prefix(**arguments, &block)
|
74
|
+
@rules << ExtractPrefixRule.new(arguments, block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def apply(context, request, path)
|
78
|
+
@rules.each do |rule|
|
79
|
+
path = rule.apply(context, request, path)
|
80
|
+
end
|
81
|
+
|
82
|
+
return path
|
83
|
+
end
|
84
|
+
|
85
|
+
def invoke!(context, request, path)
|
86
|
+
path.components = apply(context, request, path).components
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module ClassMethods
|
91
|
+
def rewrite
|
92
|
+
@rewriter ||= Rewriter.new
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Rewrite the path before processing the request if possible.
|
97
|
+
def passthrough(request, path)
|
98
|
+
self.class.rewrite.invoke!(self, request, path)
|
99
|
+
|
100
|
+
super
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -20,29 +20,29 @@
|
|
20
20
|
|
21
21
|
require 'rack'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
class Rack::Response
|
30
|
-
# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
|
31
|
-
def do_not_cache!
|
32
|
-
self["Cache-Control"] = "no-cache, must-revalidate"
|
33
|
-
self["Expires"] = Time.now.httpdate
|
23
|
+
module Rack
|
24
|
+
unless defined? EXPIRES
|
25
|
+
EXPIRES = 'Expires'.freeze
|
34
26
|
end
|
35
27
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
28
|
+
class Response
|
29
|
+
# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
|
30
|
+
def do_not_cache!
|
31
|
+
headers[CACHE_CONTROL] = "no-cache, must-revalidate"
|
32
|
+
headers[EXPIRES] = Time.now.httpdate
|
33
|
+
end
|
34
|
+
|
35
|
+
# Specify that the content should be cached.
|
36
|
+
def cache!(duration = 3600)
|
37
|
+
unless headers[CACHE_CONTROL] =~ /no-cache/
|
38
|
+
headers[CACHE_CONTROL] = "public, max-age=#{duration}"
|
39
|
+
headers[EXPIRES] = (Time.now + duration).httpdate
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Specify the content type of the response data.
|
44
|
+
def content_type!(value)
|
45
|
+
headers[CONTENT_TYPE] = value.to_s
|
41
46
|
end
|
42
|
-
end
|
43
|
-
|
44
|
-
# Specify the content type of the response data.
|
45
|
-
def content_type!(value)
|
46
|
-
self["Content-Type"] = value.to_s
|
47
47
|
end
|
48
48
|
end
|
data/lib/utopia/http.rb
CHANGED
@@ -18,6 +18,8 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
+
require 'rack'
|
22
|
+
|
21
23
|
module Utopia
|
22
24
|
module HTTP
|
23
25
|
STATUS_CODES = {
|
@@ -42,15 +44,18 @@ module Utopia
|
|
42
44
|
}
|
43
45
|
|
44
46
|
STATUS_DESCRIPTIONS = {
|
45
|
-
400 =>
|
46
|
-
401 =>
|
47
|
-
403 =>
|
48
|
-
404 =>
|
49
|
-
405 =>
|
50
|
-
416 =>
|
51
|
-
500 =>
|
52
|
-
501 =>
|
53
|
-
503 =>
|
47
|
+
400 => 'Bad Request'.freeze,
|
48
|
+
401 => 'Permission Denied'.freeze,
|
49
|
+
403 => 'Access Forbidden'.freeze,
|
50
|
+
404 => 'Resource Not Found'.freeze,
|
51
|
+
405 => 'Unsupported Method'.freeze,
|
52
|
+
416 => 'Byte range unsatisfiable'.freeze,
|
53
|
+
500 => 'Internal Server Error'.freeze,
|
54
|
+
501 => 'Not Implemented'.freeze,
|
55
|
+
503 => 'Service Unavailable'.freeze
|
54
56
|
}
|
57
|
+
|
58
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
59
|
+
LOCATION = 'Location'.freeze
|
55
60
|
end
|
56
61
|
end
|
data/lib/utopia/localization.rb
CHANGED
@@ -49,7 +49,7 @@ module Utopia
|
|
49
49
|
LOCALIZATION_KEY = 'utopia.localization'.freeze
|
50
50
|
CURRENT_LOCALE_KEY = 'utopia.localization.current_locale'.freeze
|
51
51
|
|
52
|
-
def initialize(app, options
|
52
|
+
def initialize(app, **options)
|
53
53
|
@app = app
|
54
54
|
|
55
55
|
@default_locale = options[:default_locale] || "en"
|
data/lib/utopia/middleware.rb
CHANGED
@@ -30,6 +30,9 @@ module Utopia
|
|
30
30
|
|
31
31
|
PAGES_PATH = 'pages'.freeze
|
32
32
|
|
33
|
+
# This is used for shared controller variables which get consumed by the content middleware:
|
34
|
+
VARIABLES_KEY = 'utopia.variables'.freeze
|
35
|
+
|
33
36
|
def self.default_root(subdirectory = PAGES_PATH, pwd = Dir.pwd)
|
34
37
|
File.expand_path(subdirectory, pwd)
|
35
38
|
end
|
data/lib/utopia/path.rb
CHANGED
@@ -66,14 +66,16 @@ module Utopia
|
|
66
66
|
end
|
67
67
|
|
68
68
|
class Path
|
69
|
-
SEPARATOR = "/"
|
70
|
-
|
71
69
|
include Comparable
|
72
|
-
|
70
|
+
|
71
|
+
SEPARATOR = '/'.freeze
|
72
|
+
|
73
73
|
def initialize(components = [])
|
74
74
|
@components = components
|
75
75
|
end
|
76
76
|
|
77
|
+
attr :components, true
|
78
|
+
|
77
79
|
def freeze
|
78
80
|
@components.freeze
|
79
81
|
|
@@ -103,6 +105,7 @@ module Utopia
|
|
103
105
|
self.class.shortest_path(self, root)
|
104
106
|
end
|
105
107
|
|
108
|
+
# Converts '+' into whitespace and hex encoded characters into their equivalent characters.
|
106
109
|
def self.unescape(string)
|
107
110
|
string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) {
|
108
111
|
[$1.delete('%')].pack('H*')
|
@@ -112,7 +115,32 @@ module Utopia
|
|
112
115
|
def self.[] path
|
113
116
|
self.create(path)
|
114
117
|
end
|
115
|
-
|
118
|
+
|
119
|
+
# Expand a relative path relative to a root.
|
120
|
+
def self.expand(path, root)
|
121
|
+
case path
|
122
|
+
when Path
|
123
|
+
return path.expand(root)
|
124
|
+
when String
|
125
|
+
return path if path.start_with?('/')
|
126
|
+
end
|
127
|
+
|
128
|
+
Path[path].expand(root)
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.split(path)
|
132
|
+
case path
|
133
|
+
when Path
|
134
|
+
return path.to_a
|
135
|
+
when Array
|
136
|
+
return path
|
137
|
+
when String
|
138
|
+
create(path).to_a
|
139
|
+
else
|
140
|
+
[path]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
116
144
|
def self.create(path)
|
117
145
|
case path
|
118
146
|
when Path
|
@@ -121,39 +149,45 @@ module Utopia
|
|
121
149
|
return self.new(path)
|
122
150
|
when String
|
123
151
|
return self.new(unescape(path).split(SEPARATOR, -1))
|
124
|
-
|
152
|
+
else
|
125
153
|
return self.new([path])
|
126
154
|
end
|
127
155
|
end
|
128
156
|
|
129
|
-
|
130
|
-
|
157
|
+
def include?(*args)
|
158
|
+
@components.include?(*args)
|
159
|
+
end
|
160
|
+
|
131
161
|
def directory?
|
132
|
-
return @components.last ==
|
162
|
+
return @components.last == ''
|
133
163
|
end
|
134
164
|
|
135
165
|
def to_directory
|
136
166
|
if directory?
|
137
167
|
return self
|
138
168
|
else
|
139
|
-
return join([
|
169
|
+
return join([''])
|
140
170
|
end
|
141
171
|
end
|
142
172
|
|
173
|
+
def relative?
|
174
|
+
@components.first != ''
|
175
|
+
end
|
176
|
+
|
143
177
|
def absolute?
|
144
|
-
|
178
|
+
@components.first == ''
|
145
179
|
end
|
146
180
|
|
147
181
|
def to_absolute
|
148
182
|
if absolute?
|
149
183
|
return self
|
150
184
|
else
|
151
|
-
return self.class.new([
|
185
|
+
return self.class.new([''] + @components)
|
152
186
|
end
|
153
187
|
end
|
154
188
|
|
155
189
|
def to_str
|
156
|
-
if @components == [
|
190
|
+
if @components == ['']
|
157
191
|
SEPARATOR
|
158
192
|
else
|
159
193
|
@components.join(SEPARATOR)
|
@@ -163,6 +197,18 @@ module Utopia
|
|
163
197
|
def to_s
|
164
198
|
to_str
|
165
199
|
end
|
200
|
+
|
201
|
+
def match(pattern)
|
202
|
+
to_str.match(pattern)
|
203
|
+
end
|
204
|
+
|
205
|
+
def =~ (pattern)
|
206
|
+
to_str =~ pattern
|
207
|
+
end
|
208
|
+
|
209
|
+
def to_a
|
210
|
+
@components
|
211
|
+
end
|
166
212
|
|
167
213
|
def join(other)
|
168
214
|
self.class.new(@components + other).simplify
|
@@ -186,6 +232,10 @@ module Utopia
|
|
186
232
|
end
|
187
233
|
end
|
188
234
|
|
235
|
+
def with_prefix(*args)
|
236
|
+
self.class.create(*args) + self
|
237
|
+
end
|
238
|
+
|
189
239
|
# Computes the difference of the path.
|
190
240
|
# /a/b/c - /a/b -> c
|
191
241
|
# a/b/c - a/b -> c
|
@@ -202,17 +252,17 @@ module Utopia
|
|
202
252
|
end
|
203
253
|
|
204
254
|
def simplify
|
205
|
-
result = absolute? ? [
|
255
|
+
result = absolute? ? [''] : []
|
206
256
|
|
207
257
|
@components.each do |bit|
|
208
258
|
if bit == ".."
|
209
259
|
result.pop
|
210
|
-
elsif bit != "." && bit !=
|
260
|
+
elsif bit != "." && bit != ''
|
211
261
|
result << bit
|
212
262
|
end
|
213
263
|
end
|
214
264
|
|
215
|
-
result <<
|
265
|
+
result << '' if directory?
|
216
266
|
|
217
267
|
return self.class.new(result)
|
218
268
|
end
|
@@ -242,7 +292,7 @@ module Utopia
|
|
242
292
|
yield self.class.new(parent_path.dup)
|
243
293
|
end
|
244
294
|
end
|
245
|
-
|
295
|
+
|
246
296
|
def ascend(&block)
|
247
297
|
return to_enum(:ascend) unless block_given?
|
248
298
|
|
@@ -278,13 +328,23 @@ module Utopia
|
|
278
328
|
end
|
279
329
|
|
280
330
|
def eql? other
|
281
|
-
|
282
|
-
|
331
|
+
self.class.eql?(other.class) and @components.eql?(other.components)
|
332
|
+
end
|
333
|
+
|
334
|
+
def hash
|
335
|
+
@components.hash
|
336
|
+
end
|
337
|
+
|
338
|
+
def == other
|
339
|
+
return false unless other
|
340
|
+
|
341
|
+
if other.is_a? String
|
342
|
+
self.to_s == other
|
283
343
|
else
|
284
|
-
|
344
|
+
self.to_a == other.to_a
|
285
345
|
end
|
286
346
|
end
|
287
|
-
|
347
|
+
|
288
348
|
def start_with? other
|
289
349
|
other.components.each_with_index do |part, index|
|
290
350
|
return false if @components[index] != part
|
@@ -293,10 +353,6 @@ module Utopia
|
|
293
353
|
return true
|
294
354
|
end
|
295
355
|
|
296
|
-
def hash
|
297
|
-
@components.hash
|
298
|
-
end
|
299
|
-
|
300
356
|
def [] index
|
301
357
|
return @components[component_offset(index)]
|
302
358
|
end
|
@@ -352,7 +408,7 @@ module Utopia
|
|
352
408
|
end
|
353
409
|
end
|
354
410
|
|
355
|
-
def Path(path)
|
411
|
+
def self.Path(path)
|
356
412
|
Path.create(path)
|
357
413
|
end
|
358
414
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright, 2015, 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 '../path'
|
22
|
+
|
23
|
+
module Utopia
|
24
|
+
class Path
|
25
|
+
class Matcher
|
26
|
+
class MatchData
|
27
|
+
def initialize(named_parts, post_match)
|
28
|
+
@named_parts = named_parts
|
29
|
+
@post_match = Path[post_match]
|
30
|
+
end
|
31
|
+
|
32
|
+
attr :named_parts
|
33
|
+
attr :post_match
|
34
|
+
|
35
|
+
def [] key
|
36
|
+
@named_parts[key]
|
37
|
+
end
|
38
|
+
|
39
|
+
def names
|
40
|
+
@named_parts.keys
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# patterns = {key: /\d+/, 'foo', }
|
45
|
+
def initialize(patterns = [])
|
46
|
+
@patterns = patterns
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.[](patterns)
|
50
|
+
self.new(patterns)
|
51
|
+
end
|
52
|
+
|
53
|
+
def coerce(klass, value)
|
54
|
+
if klass == Integer
|
55
|
+
Integer(value) rescue nil
|
56
|
+
elsif klass == Float
|
57
|
+
Float(value) rescue nil
|
58
|
+
elsif klass == String
|
59
|
+
value.to_s
|
60
|
+
else
|
61
|
+
klass.new(value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# This is a path prefix matching algorithm. The pattern is an array of String, Symbol, Regexp, or nil. The components is an array of String.
|
66
|
+
# As long as the components match the patterns,
|
67
|
+
def match(path)
|
68
|
+
components = path.to_a
|
69
|
+
|
70
|
+
return nil if components.size < @patterns.size
|
71
|
+
|
72
|
+
named_parts = {}
|
73
|
+
|
74
|
+
@patterns.each_with_index do |(key, matcher), index|
|
75
|
+
component = components[index]
|
76
|
+
|
77
|
+
if matcher.is_a? Class
|
78
|
+
return nil unless value = coerce(matcher, component)
|
79
|
+
|
80
|
+
named_parts[key] = value
|
81
|
+
elsif matcher
|
82
|
+
return nil unless matcher === component
|
83
|
+
|
84
|
+
named_parts[key] = component
|
85
|
+
else
|
86
|
+
named_parts[key] = component
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
return MatchData.new(named_parts, components[@patterns.size..-1])
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|