utopia 1.0.11 → 1.1.0

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