utopia 0.12.6 → 1.0.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -2
  3. data/Gemfile +6 -0
  4. data/README.md +48 -14
  5. data/Rakefile +5 -0
  6. data/bin/utopia +132 -15
  7. data/lib/utopia.rb +13 -10
  8. data/lib/utopia/content.rb +140 -0
  9. data/lib/utopia/content/link.rb +124 -0
  10. data/lib/utopia/content/links.rb +228 -0
  11. data/lib/utopia/content/node.rb +387 -0
  12. data/lib/utopia/content/processor.rb +128 -0
  13. data/lib/utopia/content/tag.rb +102 -0
  14. data/lib/utopia/controller.rb +137 -0
  15. data/lib/utopia/controller/action.rb +112 -0
  16. data/lib/utopia/controller/base.rb +174 -0
  17. data/lib/utopia/{middleware/controller → controller}/variables.rb +36 -38
  18. data/lib/utopia/exception_handler.rb +79 -0
  19. data/lib/utopia/extensions/array.rb +2 -2
  20. data/lib/utopia/localization.rb +143 -0
  21. data/lib/utopia/mail_exceptions.rb +136 -0
  22. data/lib/utopia/middleware.rb +7 -22
  23. data/lib/utopia/path.rb +150 -60
  24. data/lib/utopia/redirector.rb +152 -0
  25. data/lib/utopia/{extensions/hash.rb → session.rb} +4 -6
  26. data/lib/utopia/session/encrypted_cookie.rb +46 -48
  27. data/lib/utopia/{middleware/directory_index.rb → session/lazy_hash.rb} +44 -27
  28. data/lib/utopia/static.rb +255 -0
  29. data/lib/utopia/tags/deferred.rb +12 -8
  30. data/lib/utopia/tags/environment.rb +18 -6
  31. data/lib/utopia/tags/node.rb +12 -8
  32. data/lib/utopia/tags/override.rb +12 -12
  33. data/lib/utopia/version.rb +1 -1
  34. data/setup/.bowerrc +3 -0
  35. data/{lib/utopia/setup → setup}/Gemfile +1 -1
  36. data/setup/Rakefile +4 -0
  37. data/{lib/utopia/setup → setup}/cache/head/readme.txt +0 -0
  38. data/{lib/utopia/setup → setup}/cache/meta/readme.txt +0 -0
  39. data/setup/config.ru +64 -0
  40. data/{lib/utopia/setup → setup}/lib/readme.txt +0 -0
  41. data/{lib/utopia/setup → setup}/pages/_heading.xnode +0 -0
  42. data/{lib/utopia/setup → setup}/pages/_page.xnode +1 -1
  43. data/{lib/utopia/setup → setup}/pages/_static/icon.png +0 -0
  44. data/setup/pages/_static/site.css +70 -0
  45. data/{lib/utopia/setup → setup}/pages/errors/exception.xnode +0 -0
  46. data/{lib/utopia/setup → setup}/pages/errors/file-not-found.xnode +0 -0
  47. data/{lib/utopia/setup → setup}/pages/links.yaml +0 -0
  48. data/setup/pages/welcome/index.xnode +17 -0
  49. data/{lib/utopia/setup → setup}/public/readme.txt +0 -0
  50. data/spec/utopia/content/link_spec.rb +108 -0
  51. data/spec/utopia/content/links/foo/index.xnode +0 -0
  52. data/spec/utopia/content/links/foo/links.yaml +2 -0
  53. data/spec/utopia/content/links/foo/test.de.xnode +0 -0
  54. data/spec/utopia/content/links/foo/test.en.xnode +0 -0
  55. data/spec/utopia/content/links/links.yaml +9 -0
  56. data/spec/utopia/content/links/welcome.xnode +0 -0
  57. data/spec/utopia/content/localized/five/index.en.xnode +0 -0
  58. data/spec/utopia/content/localized/four/index.en.xnode +0 -0
  59. data/spec/utopia/content/localized/four/index.zh.xnode +0 -0
  60. data/spec/utopia/content/localized/four/links.yaml +4 -0
  61. data/spec/utopia/content/localized/links.yaml +16 -0
  62. data/spec/utopia/content/localized/one.xnode +0 -0
  63. data/spec/utopia/content/localized/three/index.xnode +0 -0
  64. data/spec/utopia/content/localized/two.en.xnode +0 -0
  65. data/spec/utopia/content/localized/two.zh.xnode +0 -0
  66. data/spec/utopia/content/node/ordered/first.xnode +0 -0
  67. data/spec/utopia/content/node/ordered/index.xnode +0 -0
  68. data/spec/utopia/content/node/ordered/links.yaml +4 -0
  69. data/spec/utopia/content/node/ordered/second.xnode +0 -0
  70. data/spec/utopia/content/node/related/foo.en.xnode +0 -0
  71. data/spec/utopia/content/node/related/foo.ja.xnode +0 -0
  72. data/spec/utopia/content/node/related/links.yaml +4 -0
  73. data/spec/utopia/content/node_spec.rb +63 -0
  74. data/spec/utopia/{middleware/content_spec.rb → content/processor_spec.rb} +34 -23
  75. data/spec/utopia/content_spec.rb +87 -0
  76. data/spec/utopia/content_spec.ru +10 -0
  77. data/spec/utopia/{middleware/controller_spec.rb → controller_spec.rb} +61 -16
  78. data/spec/utopia/controller_spec.ru +4 -0
  79. data/spec/utopia/extensions_spec.rb +6 -17
  80. data/spec/utopia/localization_spec.rb +60 -0
  81. data/spec/utopia/localization_spec.ru +11 -0
  82. data/{lib/utopia/tags.rb → spec/utopia/middleware_spec.rb} +8 -14
  83. data/spec/utopia/{middleware/content_root → pages}/_heading.xnode +0 -0
  84. data/spec/utopia/pages/content/_show-value.xnode +1 -0
  85. data/spec/utopia/pages/content/test-partial.xnode +1 -0
  86. data/spec/utopia/pages/controller/controller.rb +28 -0
  87. data/spec/utopia/pages/controller/index.xnode +1 -0
  88. data/spec/utopia/pages/controller/nested/controller.rb +4 -0
  89. data/spec/utopia/{middleware/content_root → pages}/index.xnode +0 -0
  90. data/spec/utopia/pages/localized.de.txt +1 -0
  91. data/spec/utopia/pages/localized.en.txt +1 -0
  92. data/spec/utopia/pages/localized.jp.txt +1 -0
  93. data/spec/utopia/pages/node/index.xnode +1 -0
  94. data/spec/utopia/pages/test.txt +1 -0
  95. data/spec/utopia/path_spec.rb +109 -0
  96. data/spec/utopia/rack_spec.rb +2 -0
  97. data/spec/utopia/session_spec.rb +82 -0
  98. data/spec/utopia/session_spec.ru +20 -0
  99. data/spec/utopia/spec_helper.rb +16 -0
  100. data/{lib/utopia/extensions/string.rb → spec/utopia/static_spec.rb} +24 -15
  101. data/spec/utopia/static_spec.ru +4 -0
  102. data/utopia.gemspec +3 -3
  103. metadata +138 -54
  104. data/lib/utopia/extensions/regexp.rb +0 -33
  105. data/lib/utopia/link.rb +0 -288
  106. data/lib/utopia/middleware/all.rb +0 -33
  107. data/lib/utopia/middleware/content.rb +0 -157
  108. data/lib/utopia/middleware/content/node.rb +0 -386
  109. data/lib/utopia/middleware/content/processor.rb +0 -123
  110. data/lib/utopia/middleware/controller.rb +0 -130
  111. data/lib/utopia/middleware/controller/action.rb +0 -121
  112. data/lib/utopia/middleware/controller/base.rb +0 -184
  113. data/lib/utopia/middleware/exception_handler.rb +0 -80
  114. data/lib/utopia/middleware/localization.rb +0 -147
  115. data/lib/utopia/middleware/localization/name.rb +0 -69
  116. data/lib/utopia/middleware/mail_exceptions.rb +0 -138
  117. data/lib/utopia/middleware/redirector.rb +0 -146
  118. data/lib/utopia/middleware/requester.rb +0 -126
  119. data/lib/utopia/middleware/static.rb +0 -295
  120. data/lib/utopia/setup.rb +0 -60
  121. data/lib/utopia/setup/config.ru +0 -47
  122. data/lib/utopia/setup/pages/_static/background.png +0 -0
  123. data/lib/utopia/setup/pages/_static/site.css +0 -48
  124. data/lib/utopia/setup/pages/welcome/index.xnode +0 -7
  125. data/lib/utopia/tag.rb +0 -105
  126. data/lib/utopia/tags/all.rb +0 -34
@@ -0,0 +1,136 @@
1
+ # Copyright, 2013, 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 'net/smtp'
22
+ require 'mail'
23
+
24
+ module Utopia
25
+ # Catches all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
26
+ class MailExceptions
27
+ # A basic local non-authenticated SMTP server.
28
+ LOCAL_SMTP = [:smtp, {
29
+ :address => "localhost",
30
+ :port => 25,
31
+ :enable_starttls_auto => false
32
+ }]
33
+
34
+ def initialize(app, config = {})
35
+ @app = app
36
+
37
+ @to = config[:to] || "postmaster"
38
+ @from = config.fetch(:from) {(ENV['USER'] || 'rack') + "@localhost"}
39
+ @subject = config[:subject] || '%{exception} [PID %{pid} : %{cwd}]'
40
+ @delivery_method = config.fetch(:delivery_method, LOCAL_SMTP)
41
+
42
+ @dump_environment = config.fetch(:dump_environment, false)
43
+ end
44
+
45
+ def call(env)
46
+ begin
47
+ return @app.call(env)
48
+ rescue => exception
49
+ send_notification exception, env
50
+
51
+ raise
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ REQUEST_KEYS = [:ip, :referrer, :path, :user_agent]
58
+
59
+ def generate_body(exception, env)
60
+ io = StringIO.new
61
+
62
+ # Dump out useful rack environment variables:
63
+ request = Rack::Request.new(env)
64
+
65
+ io.puts "#{request.request_method} #{request.url}"
66
+
67
+ io.puts
68
+
69
+ REQUEST_KEYS.each do |key|
70
+ value = request.send(key)
71
+ io.puts "request.#{key}: #{value.inspect}"
72
+ end
73
+
74
+ request.params.each do |key, value|
75
+ io.puts "request.params.#{key}: #{value.inspect}"
76
+ end
77
+
78
+ io.puts
79
+
80
+ io.puts "#{exception.class.name}: #{exception.to_s}"
81
+
82
+ if exception.respond_to?(:backtrace)
83
+ io.puts exception.backtrace
84
+ else
85
+ io.puts exception.to_s
86
+ end
87
+
88
+ return io.string
89
+ end
90
+
91
+ def generate_mail(exception, env)
92
+ attributes = {
93
+ exception: exception.class.name,
94
+ pid: $$,
95
+ cwd: Dir.getwd,
96
+ }
97
+
98
+ mail = Mail.new(
99
+ :from => @from,
100
+ :to => @to,
101
+ :subject => @subject % attributes
102
+ )
103
+
104
+ mail.text_part = Mail::Part.new
105
+ mail.text_part.body = generate_body(exception, env)
106
+
107
+ if body = extract_body(env) and body.size > 0
108
+ mail.attachments['body.bin'] = body
109
+ end
110
+
111
+ if @dump_environment
112
+ mail.attachments['environment.yaml'] = YAML::dump(env)
113
+ end
114
+
115
+ return mail
116
+ end
117
+
118
+ def send_notification(exception, env)
119
+ mail = generate_mail(exception, env)
120
+
121
+ mail.delivery_method(*@delivery_method) if @delivery_method
122
+
123
+ mail.deliver
124
+ rescue => mail_exception
125
+ $stderr.puts mail_exception.to_s
126
+ $stderr.puts mail_exception.backtrace
127
+ end
128
+
129
+ def extract_body(env)
130
+ if io = env['rack.input']
131
+ io.rewind if io.respond_to?(:rewind)
132
+ io.read
133
+ end
134
+ end
135
+ end
136
+ end
@@ -18,34 +18,19 @@
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 'pathname'
22
21
  require 'logger'
23
22
 
24
- require 'utopia/http'
23
+ require_relative 'http'
24
+ require_relative 'path'
25
25
 
26
- require 'utopia/extensions/rack'
26
+ require_relative 'extensions/rack'
27
27
 
28
28
  module Utopia
29
29
  LOG = Logger.new($stderr)
30
- LOG.level = Logger::DEBUG
31
30
 
32
- module Middleware
33
- def self.default_root(subdir = "pages")
34
- Pathname.new(Dir.pwd).join(subdir).cleanpath.to_s
35
- end
36
-
37
- def self.failure(status = 500, message = "Non-specific error")
38
- body = "#{HTTP::STATUS_DESCRIPTIONS[status] || status.to_s}: #{message}"
39
-
40
- return [status, {
41
- "Content-Type" => "text/plain",
42
- "Content-Length" => body.size.to_s,
43
- "X-Cascade" => "pass"
44
- }, [body]]
45
- end
31
+ PAGES_PATH = 'pages'.freeze
32
+
33
+ def self.default_root(subdirectory = PAGES_PATH, pwd = Dir.pwd)
34
+ File.expand_path(subdirectory, pwd)
46
35
  end
47
36
  end
48
-
49
- require 'utopia/path'
50
- require 'utopia/tag'
51
-
@@ -19,19 +19,88 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Utopia
22
+ class Basename
23
+ # A basename represents a file name with an optional extension. You can specify a specific extension to identify or specify true to select any extension after the last trailing dot.
24
+ def initialize(name, extension = false)
25
+ if extension
26
+ if extension == true
27
+ offset = name.rindex('.')
28
+ else
29
+ offset = name.rindex(extension) - 1
30
+ end
31
+
32
+ @name = name[0...offset]
33
+ @extension = name[offset+1..-1]
34
+ else
35
+ @name = name
36
+ @extension = nil
37
+ end
38
+ end
39
+
40
+ def rename(name)
41
+ copy = self.dup
42
+
43
+ copy.send(:instance_variable_set, :@name, name)
44
+
45
+ return copy
46
+ end
47
+
48
+ attr :name
49
+ attr :extension
50
+
51
+ def parts
52
+ @parts ||= @name.split('.')
53
+ end
54
+
55
+ def variant
56
+ parts.last if parts.size > 1
57
+ end
58
+
59
+ def to_str
60
+ "#{name}#{extension}"
61
+ end
62
+
63
+ def to_s
64
+ to_str
65
+ end
66
+ end
67
+
22
68
  class Path
23
69
  SEPARATOR = "/"
24
70
 
25
71
  include Comparable
26
72
 
27
73
  def initialize(components = [])
28
- # To ensure we don't do anything stupid we freeze the components
29
- @components = components.dup.freeze
74
+ @components = components
30
75
  end
31
76
 
32
- # Shorthand constructor
33
- def self.[](path)
34
- create(path)
77
+ def freeze
78
+ @components.freeze
79
+
80
+ super
81
+ end
82
+
83
+ # Returns the length of the prefix which is shared by two strings.
84
+ def self.prefix_length(a, b)
85
+ [a.size, b.size].min.times{|i| return i if a[i] != b[i]}
86
+ end
87
+
88
+ # Return the shortest relative path to get to path from root:
89
+ def self.shortest_path(path, root)
90
+ path = self.create(path)
91
+ root = self.create(root).dirname
92
+
93
+ # Find the common prefix:
94
+ i = prefix_length(path.components, root.components) || 0
95
+
96
+ # The difference between the root path and the required path, taking into account the common prefix:
97
+ up = root.components.size - i
98
+
99
+ return self.create([".."] * up + path.components[i..-1])
100
+ end
101
+
102
+ def shortest_path(root)
103
+ self.class.shortest_path(self, root)
35
104
  end
36
105
 
37
106
  def self.unescape(string)
@@ -40,22 +109,20 @@ module Utopia
40
109
  }
41
110
  end
42
111
 
112
+ def self.[] path
113
+ self.create(path)
114
+ end
115
+
43
116
  def self.create(path)
44
117
  case path
45
118
  when Path
46
119
  return path
47
120
  when Array
48
- return Path.new(path)
121
+ return self.new(path)
49
122
  when String
50
- path = unescape(path)
51
- # Ends with SEPARATOR
52
- if path[-1,1] == SEPARATOR
53
- return Path.new(path.split(SEPARATOR) << "")
54
- else
55
- return Path.new(path.split(SEPARATOR))
56
- end
123
+ return self.new(unescape(path).split(SEPARATOR, -1))
57
124
  when Symbol
58
- return Path.new([path])
125
+ return self.new([path])
59
126
  end
60
127
  end
61
128
 
@@ -81,7 +148,7 @@ module Utopia
81
148
  if absolute?
82
149
  return self
83
150
  else
84
- return Path.new([""] + @components)
151
+ return self.class.new([""] + @components)
85
152
  end
86
153
  end
87
154
 
@@ -97,12 +164,12 @@ module Utopia
97
164
  to_str
98
165
  end
99
166
 
100
- def [] index
101
- @components[index]
167
+ def join(other)
168
+ self.class.new(@components + other).simplify
102
169
  end
103
170
 
104
- def join(other)
105
- Path.new(@components + other).simplify
171
+ def expand(root)
172
+ root + self
106
173
  end
107
174
 
108
175
  def +(other)
@@ -131,13 +198,13 @@ module Utopia
131
198
  i += 1
132
199
  end
133
200
 
134
- return Path.create(@components[i,@components.size])
201
+ return self.class.new(@components[i,@components.size])
135
202
  end
136
203
 
137
204
  def simplify
138
205
  result = absolute? ? [""] : []
139
206
 
140
- components.each do |bit|
207
+ @components.each do |bit|
141
208
  if bit == ".."
142
209
  result.pop
143
210
  elsif bit != "." && bit != ""
@@ -146,35 +213,22 @@ module Utopia
146
213
  end
147
214
 
148
215
  result << "" if directory?
149
- return Path.new(result)
150
- end
151
-
152
- def basename(ext = nil)
153
- if ext == true
154
- File.basename(components.last, extension)
155
- elsif String === ext
156
- File.basename(components.last, ext)
157
- else
158
- components.last
159
- end
216
+
217
+ return self.class.new(result)
160
218
  end
161
219
 
162
- def extension
163
- if components.last
164
- components.last.split(".").last
165
- else
166
- nil
167
- end
220
+ def basename(*args)
221
+ Basename.new(@components.last, *args)
168
222
  end
169
-
223
+
170
224
  def dirname(count = 1)
171
- path = Path.new(components[0...-count])
225
+ path = self.class.new(@components[0...-count])
172
226
 
173
227
  return absolute? ? path.to_absolute : path
174
228
  end
175
229
 
176
- def to_local_path
177
- components.join(File::SEPARATOR)
230
+ def to_local_path(separator = File::SEPARATOR)
231
+ @components.join(separator)
178
232
  end
179
233
 
180
234
  def descend(&block)
@@ -182,10 +236,10 @@ module Utopia
182
236
 
183
237
  parent_path = []
184
238
 
185
- components.each do |component|
239
+ @components.each do |component|
186
240
  parent_path << component
187
241
 
188
- yield self.class.new(parent_path)
242
+ yield self.class.new(parent_path.dup)
189
243
  end
190
244
  end
191
245
 
@@ -209,7 +263,7 @@ module Utopia
209
263
  end
210
264
 
211
265
  if at
212
- return [Path.new(@components[0...at]), Path.new(@components[at+1..-1])]
266
+ return [self.class.new(@components[0...at]), self.class.new(@components[at+1..-1])]
213
267
  else
214
268
  return nil
215
269
  end
@@ -231,38 +285,74 @@ module Utopia
231
285
  end
232
286
  end
233
287
 
234
- def starts_with? other
288
+ def start_with? other
235
289
  other.components.each_with_index do |part, index|
236
290
  return false if @components[index] != part
237
291
  end
238
292
 
239
293
  return true
240
294
  end
241
-
295
+
242
296
  def hash
243
297
  @components.hash
244
298
  end
245
-
299
+
300
+ def [] index
301
+ return @components[component_offset(index)]
302
+ end
303
+
304
+ # Replaces a named component, indexing as per
305
+ def []= index, value
306
+ return @components[component_offset(index)] = value
307
+ end
308
+
309
+ def delete_at(index)
310
+ @components.delete_at(component_offset(index))
311
+ end
312
+
313
+ def first
314
+ if absolute?
315
+ @components[1]
316
+ else
317
+ @components[0]
318
+ end
319
+ end
320
+
246
321
  def last
247
322
  if directory?
248
- components[-2]
323
+ @components[-2]
249
324
  else
250
- components[-1]
325
+ @components[-1]
251
326
  end
252
327
  end
253
-
254
- def self.locale(name, extension = false)
255
- if String === extension
256
- name = File.basename(name, extension)
257
- elsif extension
258
- name = name.split
328
+
329
+ def extension
330
+ basename(true).extension
331
+ end
332
+
333
+ private
334
+
335
+ # We adjust the index slightly so that indices reference path components rather than the directory markers at the start and end of the path components array.
336
+ def component_offset(index)
337
+ if Range === index
338
+ Range.new(adjust_index(index.first), adjust_index(index.last), index.exclude_end?)
339
+ else
340
+ adjust_index(index)
259
341
  end
260
-
261
- name.split(".")[1..-1].join(".")
262
342
  end
263
343
 
264
- def locale (extension = false)
265
- return Path.locale(last, extension)
344
+ def adjust_index(index)
345
+ if index < 0
346
+ index -= 1 if directory?
347
+ else
348
+ index += 1 if absolute?
349
+ end
350
+
351
+ return index
266
352
  end
267
353
  end
354
+
355
+ def Path(path)
356
+ Path.create(path)
357
+ end
268
358
  end