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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.travis.yml +1 -0
  4. data/bin/utopia +1 -1
  5. data/lib/utopia/content.rb +10 -12
  6. data/lib/utopia/content/link.rb +8 -11
  7. data/lib/utopia/content/links.rb +1 -1
  8. data/lib/utopia/content/node.rb +8 -7
  9. data/lib/utopia/controller.rb +40 -39
  10. data/lib/utopia/controller/action.rb +14 -12
  11. data/lib/utopia/controller/base.rb +31 -32
  12. data/lib/utopia/controller/rewrite.rb +104 -0
  13. data/lib/utopia/exception_handler.rb +1 -1
  14. data/lib/utopia/extensions/rack.rb +21 -21
  15. data/lib/utopia/http.rb +14 -9
  16. data/lib/utopia/localization.rb +1 -1
  17. data/lib/utopia/middleware.rb +3 -0
  18. data/lib/utopia/path.rb +81 -25
  19. data/lib/utopia/path/matcher.rb +94 -0
  20. data/lib/utopia/redirector.rb +4 -4
  21. data/lib/utopia/session/encrypted_cookie.rb +1 -1
  22. data/lib/utopia/static.rb +1 -1
  23. data/lib/utopia/tags/node.rb +1 -1
  24. data/lib/utopia/version.rb +1 -1
  25. data/setup/site/config.ru +1 -1
  26. data/spec/utopia/content/link_spec.rb +8 -8
  27. data/spec/utopia/content/node_spec.rb +1 -1
  28. data/spec/utopia/content_spec.rb +3 -3
  29. data/spec/utopia/content_spec.ru +1 -1
  30. data/spec/utopia/controller/action_spec.rb +61 -0
  31. data/spec/utopia/controller/middleware_spec.rb +71 -0
  32. data/spec/utopia/controller/middleware_spec.ru +4 -0
  33. data/spec/utopia/controller/middleware_spec/controller/controller.rb +24 -0
  34. data/spec/utopia/{pages → controller/middleware_spec}/controller/index.xnode +0 -0
  35. data/spec/utopia/{pages → controller/middleware_spec}/controller/nested/controller.rb +0 -0
  36. data/spec/utopia/controller/rewrite_spec.rb +66 -0
  37. data/spec/utopia/{controller_spec.rb → controller/sequence_spec.rb} +11 -84
  38. data/spec/utopia/exception_handler_spec.rb +3 -3
  39. data/spec/utopia/exception_handler_spec.ru +2 -2
  40. data/spec/utopia/exception_handler_spec/controller.rb +15 -0
  41. data/spec/utopia/path/matcher_spec.rb +65 -0
  42. data/spec/utopia/rack_spec.rb +0 -12
  43. data/utopia.gemspec +1 -1
  44. metadata +27 -14
  45. data/spec/utopia/controller_spec.ru +0 -4
  46. 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
@@ -42,7 +42,7 @@ module Utopia
42
42
  body.puts "</body></html>"
43
43
  body.rewind
44
44
 
45
- return [400, {"Content-Type" => "text/html"}, body]
45
+ return [400, {HTTP::CONTENT_TYPE => "text/html"}, body]
46
46
  end
47
47
 
48
48
  def redirect(env, exception)
@@ -20,29 +20,29 @@
20
20
 
21
21
  require 'rack'
22
22
 
23
- class Rack::Request
24
- def url_with_path(path = "")
25
- base_url << path
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
- # Specify that the content should be cached.
37
- def cache!(duration = 3600)
38
- unless (self["Cache-Control"] || "").match(/no-cache/)
39
- self["Cache-Control"] = "public, max-age=#{duration}"
40
- self["Expires"] = (Time.now + duration).httpdate
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 => "Bad Request",
46
- 401 => "Permission Denied",
47
- 403 => "Access Forbidden",
48
- 404 => "Resource Not Found",
49
- 405 => "Unsupported Method",
50
- 416 => "Byte range unsatisfiable",
51
- 500 => "Internal Server Error",
52
- 501 => "Not Implemented",
53
- 503 => "Service Unavailable"
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
@@ -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"
@@ -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
- when Symbol
152
+ else
125
153
  return self.new([path])
126
154
  end
127
155
  end
128
156
 
129
- attr :components
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
- return @components.first == ""
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([""] + @components)
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 << "" if directory?
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
- if self.class == other.class
282
- return @components.eql?(other.components)
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
- return false
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