utopia 0.12.6 → 1.0.0

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