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