utopia 0.12.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -2
- data/Gemfile +6 -0
- data/README.md +48 -14
- data/Rakefile +5 -0
- data/bin/utopia +132 -15
- data/lib/utopia.rb +13 -10
- data/lib/utopia/content.rb +140 -0
- data/lib/utopia/content/link.rb +124 -0
- data/lib/utopia/content/links.rb +228 -0
- data/lib/utopia/content/node.rb +387 -0
- data/lib/utopia/content/processor.rb +128 -0
- data/lib/utopia/content/tag.rb +102 -0
- data/lib/utopia/controller.rb +137 -0
- data/lib/utopia/controller/action.rb +112 -0
- data/lib/utopia/controller/base.rb +174 -0
- data/lib/utopia/{middleware/controller → controller}/variables.rb +36 -38
- data/lib/utopia/exception_handler.rb +79 -0
- data/lib/utopia/extensions/array.rb +2 -2
- data/lib/utopia/localization.rb +143 -0
- data/lib/utopia/mail_exceptions.rb +136 -0
- data/lib/utopia/middleware.rb +7 -22
- data/lib/utopia/path.rb +150 -60
- data/lib/utopia/redirector.rb +152 -0
- data/lib/utopia/{extensions/hash.rb → session.rb} +4 -6
- data/lib/utopia/session/encrypted_cookie.rb +46 -48
- data/lib/utopia/{middleware/directory_index.rb → session/lazy_hash.rb} +44 -27
- data/lib/utopia/static.rb +255 -0
- data/lib/utopia/tags/deferred.rb +12 -8
- data/lib/utopia/tags/environment.rb +18 -6
- data/lib/utopia/tags/node.rb +12 -8
- data/lib/utopia/tags/override.rb +12 -12
- data/lib/utopia/version.rb +1 -1
- data/setup/.bowerrc +3 -0
- data/{lib/utopia/setup → setup}/Gemfile +1 -1
- data/setup/Rakefile +4 -0
- data/{lib/utopia/setup → setup}/cache/head/readme.txt +0 -0
- data/{lib/utopia/setup → setup}/cache/meta/readme.txt +0 -0
- data/setup/config.ru +64 -0
- data/{lib/utopia/setup → setup}/lib/readme.txt +0 -0
- data/{lib/utopia/setup → setup}/pages/_heading.xnode +0 -0
- data/{lib/utopia/setup → setup}/pages/_page.xnode +1 -1
- data/{lib/utopia/setup → setup}/pages/_static/icon.png +0 -0
- data/setup/pages/_static/site.css +70 -0
- data/{lib/utopia/setup → setup}/pages/errors/exception.xnode +0 -0
- data/{lib/utopia/setup → setup}/pages/errors/file-not-found.xnode +0 -0
- data/{lib/utopia/setup → setup}/pages/links.yaml +0 -0
- data/setup/pages/welcome/index.xnode +17 -0
- data/{lib/utopia/setup → setup}/public/readme.txt +0 -0
- data/spec/utopia/content/link_spec.rb +108 -0
- data/spec/utopia/content/links/foo/index.xnode +0 -0
- data/spec/utopia/content/links/foo/links.yaml +2 -0
- data/spec/utopia/content/links/foo/test.de.xnode +0 -0
- data/spec/utopia/content/links/foo/test.en.xnode +0 -0
- data/spec/utopia/content/links/links.yaml +9 -0
- data/spec/utopia/content/links/welcome.xnode +0 -0
- data/spec/utopia/content/localized/five/index.en.xnode +0 -0
- data/spec/utopia/content/localized/four/index.en.xnode +0 -0
- data/spec/utopia/content/localized/four/index.zh.xnode +0 -0
- data/spec/utopia/content/localized/four/links.yaml +4 -0
- data/spec/utopia/content/localized/links.yaml +16 -0
- data/spec/utopia/content/localized/one.xnode +0 -0
- data/spec/utopia/content/localized/three/index.xnode +0 -0
- data/spec/utopia/content/localized/two.en.xnode +0 -0
- data/spec/utopia/content/localized/two.zh.xnode +0 -0
- data/spec/utopia/content/node/ordered/first.xnode +0 -0
- data/spec/utopia/content/node/ordered/index.xnode +0 -0
- data/spec/utopia/content/node/ordered/links.yaml +4 -0
- data/spec/utopia/content/node/ordered/second.xnode +0 -0
- data/spec/utopia/content/node/related/foo.en.xnode +0 -0
- data/spec/utopia/content/node/related/foo.ja.xnode +0 -0
- data/spec/utopia/content/node/related/links.yaml +4 -0
- data/spec/utopia/content/node_spec.rb +63 -0
- data/spec/utopia/{middleware/content_spec.rb → content/processor_spec.rb} +34 -23
- data/spec/utopia/content_spec.rb +87 -0
- data/spec/utopia/content_spec.ru +10 -0
- data/spec/utopia/{middleware/controller_spec.rb → controller_spec.rb} +61 -16
- data/spec/utopia/controller_spec.ru +4 -0
- data/spec/utopia/extensions_spec.rb +6 -17
- data/spec/utopia/localization_spec.rb +60 -0
- data/spec/utopia/localization_spec.ru +11 -0
- data/{lib/utopia/tags.rb → spec/utopia/middleware_spec.rb} +8 -14
- data/spec/utopia/{middleware/content_root → pages}/_heading.xnode +0 -0
- data/spec/utopia/pages/content/_show-value.xnode +1 -0
- data/spec/utopia/pages/content/test-partial.xnode +1 -0
- data/spec/utopia/pages/controller/controller.rb +28 -0
- data/spec/utopia/pages/controller/index.xnode +1 -0
- data/spec/utopia/pages/controller/nested/controller.rb +4 -0
- data/spec/utopia/{middleware/content_root → pages}/index.xnode +0 -0
- data/spec/utopia/pages/localized.de.txt +1 -0
- data/spec/utopia/pages/localized.en.txt +1 -0
- data/spec/utopia/pages/localized.jp.txt +1 -0
- data/spec/utopia/pages/node/index.xnode +1 -0
- data/spec/utopia/pages/test.txt +1 -0
- data/spec/utopia/path_spec.rb +109 -0
- data/spec/utopia/rack_spec.rb +2 -0
- data/spec/utopia/session_spec.rb +82 -0
- data/spec/utopia/session_spec.ru +20 -0
- data/spec/utopia/spec_helper.rb +16 -0
- data/{lib/utopia/extensions/string.rb → spec/utopia/static_spec.rb} +24 -15
- data/spec/utopia/static_spec.ru +4 -0
- data/utopia.gemspec +3 -3
- metadata +138 -54
- data/lib/utopia/extensions/regexp.rb +0 -33
- data/lib/utopia/link.rb +0 -288
- data/lib/utopia/middleware/all.rb +0 -33
- data/lib/utopia/middleware/content.rb +0 -157
- data/lib/utopia/middleware/content/node.rb +0 -386
- data/lib/utopia/middleware/content/processor.rb +0 -123
- data/lib/utopia/middleware/controller.rb +0 -130
- data/lib/utopia/middleware/controller/action.rb +0 -121
- data/lib/utopia/middleware/controller/base.rb +0 -184
- data/lib/utopia/middleware/exception_handler.rb +0 -80
- data/lib/utopia/middleware/localization.rb +0 -147
- data/lib/utopia/middleware/localization/name.rb +0 -69
- data/lib/utopia/middleware/mail_exceptions.rb +0 -138
- data/lib/utopia/middleware/redirector.rb +0 -146
- data/lib/utopia/middleware/requester.rb +0 -126
- data/lib/utopia/middleware/static.rb +0 -295
- data/lib/utopia/setup.rb +0 -60
- data/lib/utopia/setup/config.ru +0 -47
- data/lib/utopia/setup/pages/_static/background.png +0 -0
- data/lib/utopia/setup/pages/_static/site.css +0 -48
- data/lib/utopia/setup/pages/welcome/index.xnode +0 -7
- data/lib/utopia/tag.rb +0 -105
- data/lib/utopia/tags/all.rb +0 -34
@@ -0,0 +1,174 @@
|
|
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
|
+
|
23
|
+
module Utopia
|
24
|
+
class Controller
|
25
|
+
class Base
|
26
|
+
def self.base_path
|
27
|
+
self.const_get(:BASE_PATH)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.uri_path
|
31
|
+
self.const_get(:URI_PATH)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.controller
|
35
|
+
self.const_get(:CONTROLLER)
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def require_local(path)
|
40
|
+
require File.join(base_path, path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def direct?(path)
|
44
|
+
path.dirname == uri_path
|
45
|
+
end
|
46
|
+
|
47
|
+
def actions
|
48
|
+
@actions ||= Action.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def on(path, options = {}, &block)
|
52
|
+
if Symbol === path
|
53
|
+
path = ['**', path]
|
54
|
+
end
|
55
|
+
|
56
|
+
actions.define(Path.create(path).components, options, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def lookup(path)
|
60
|
+
possible_actions = actions.select((path - uri_path).components)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Given a path, look up all matched actions.
|
65
|
+
def lookup(path)
|
66
|
+
self.class.lookup(path)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Given a request, call associated actions if at least one exists.
|
70
|
+
def passthrough(request, path)
|
71
|
+
actions = lookup(path)
|
72
|
+
|
73
|
+
if actions.size > 0
|
74
|
+
variables = request.controller
|
75
|
+
controller_clone = self.clone
|
76
|
+
|
77
|
+
variables << controller_clone
|
78
|
+
|
79
|
+
response = catch(:response) do
|
80
|
+
# By default give nothing - i.e. keep on processing:
|
81
|
+
actions.each do |action|
|
82
|
+
action.invoke!(controller_clone, request, path)
|
83
|
+
end and nil
|
84
|
+
end
|
85
|
+
|
86
|
+
if response
|
87
|
+
return controller_clone.respond_with(*response)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# Copy the instance variables from the previous controller to the next controller (usually only a few). This allows controllers to share effectively the same instance variables while still being separate classes/instances.
|
95
|
+
def copy_instance_variables(from)
|
96
|
+
from.instance_variables.each do |name|
|
97
|
+
instance_variable_set(name, from.instance_variable_get(name))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def call(env)
|
102
|
+
self.class.controller.app.call(env)
|
103
|
+
end
|
104
|
+
|
105
|
+
def respond!(*args)
|
106
|
+
throw :response, args
|
107
|
+
end
|
108
|
+
|
109
|
+
def ignore!
|
110
|
+
throw :response, nil
|
111
|
+
end
|
112
|
+
|
113
|
+
def redirect! (target, status = 302)
|
114
|
+
respond! :redirect => target.to_str, :status => status
|
115
|
+
end
|
116
|
+
|
117
|
+
def rewrite! location
|
118
|
+
throw :rewrite, location.to_str
|
119
|
+
end
|
120
|
+
|
121
|
+
def fail!(error = :bad_request)
|
122
|
+
respond! error
|
123
|
+
end
|
124
|
+
|
125
|
+
def success!(*args)
|
126
|
+
respond! :success, *args
|
127
|
+
end
|
128
|
+
|
129
|
+
def respond_with(*args)
|
130
|
+
return args[0] if args[0] == nil || Array === args[0]
|
131
|
+
|
132
|
+
status = 200
|
133
|
+
options = nil
|
134
|
+
|
135
|
+
if Numeric === args[0] || Symbol === args[0]
|
136
|
+
status = args[0]
|
137
|
+
options = args[1] || {}
|
138
|
+
else
|
139
|
+
options = args[0]
|
140
|
+
status = options[:status] || status
|
141
|
+
end
|
142
|
+
|
143
|
+
status = Utopia::HTTP::STATUS_CODES[status] || status
|
144
|
+
headers = options[:headers] || {}
|
145
|
+
|
146
|
+
if options[:type]
|
147
|
+
headers['Content-Type'] ||= options[:type]
|
148
|
+
end
|
149
|
+
|
150
|
+
if options[:redirect]
|
151
|
+
headers["Location"] = options[:redirect]
|
152
|
+
status = 302 if status < 300 || status >= 400
|
153
|
+
end
|
154
|
+
|
155
|
+
body = []
|
156
|
+
if options[:body]
|
157
|
+
body = options[:body]
|
158
|
+
elsif options[:content]
|
159
|
+
body = [options[:content]]
|
160
|
+
elsif status >= 300
|
161
|
+
body = [Utopia::HTTP::STATUS_DESCRIPTIONS[status] || "Status #{status}"]
|
162
|
+
end
|
163
|
+
|
164
|
+
return [status, headers, body]
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return nil if this controller didn't do anything. Request will keep on processing. Return a valid rack response if the controller can do so.
|
168
|
+
def process!(request, path)
|
169
|
+
# puts "process! #{request} #{path}"
|
170
|
+
passthrough(request, path)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -19,53 +19,51 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
module Utopia
|
22
|
-
|
23
|
-
class
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
22
|
+
class Controller
|
23
|
+
class Variables
|
24
|
+
def initialize
|
25
|
+
@controllers = []
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
28
|
+
def << controller
|
29
|
+
top = @controllers.last
|
30
|
+
|
31
|
+
@controllers << controller
|
32
|
+
|
33
|
+
# This ensures that most variables will be at the top and controllers can naturally interactive with instance variables.
|
34
|
+
controller.copy_instance_variables(top) if top
|
35
|
+
end
|
37
36
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
if block_given?
|
46
|
-
yield key
|
47
|
-
else
|
48
|
-
raise KeyError.new(key)
|
37
|
+
def fetch(key)
|
38
|
+
@controllers.reverse_each do |controller|
|
39
|
+
if controller.instance_variables.include?(key)
|
40
|
+
return controller.instance_variable_get(key)
|
49
41
|
end
|
50
42
|
end
|
43
|
+
|
44
|
+
if block_given?
|
45
|
+
yield key
|
46
|
+
else
|
47
|
+
raise KeyError.new(key)
|
48
|
+
end
|
49
|
+
end
|
51
50
|
|
52
|
-
|
53
|
-
|
51
|
+
def to_hash
|
52
|
+
attributes = {}
|
54
53
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
54
|
+
@controllers.each do |controller|
|
55
|
+
controller.instance_variables.each do |name|
|
56
|
+
key = name[1..-1]
|
57
|
+
|
58
|
+
attributes[key] = controller.instance_variable_get(name)
|
61
59
|
end
|
62
|
-
|
63
|
-
return attributes
|
64
60
|
end
|
65
61
|
|
66
|
-
|
67
|
-
|
68
|
-
|
62
|
+
return attributes
|
63
|
+
end
|
64
|
+
|
65
|
+
def [] key
|
66
|
+
fetch("@#{key}".to_sym) { nil }
|
69
67
|
end
|
70
68
|
end
|
71
69
|
end
|
@@ -0,0 +1,79 @@
|
|
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 'middleware'
|
22
|
+
|
23
|
+
require 'trenni/strings'
|
24
|
+
|
25
|
+
module Utopia
|
26
|
+
class ExceptionHandler
|
27
|
+
def initialize(app, location)
|
28
|
+
@app = app
|
29
|
+
|
30
|
+
@location = location
|
31
|
+
end
|
32
|
+
|
33
|
+
def fatal_error(env, exception)
|
34
|
+
body = StringIO.new
|
35
|
+
|
36
|
+
body.puts "<!DOCTYPE html><html><head><title>Fatal Error</title></head><body>"
|
37
|
+
body.puts "<h1>Fatal Error</h1>"
|
38
|
+
body.puts "<p>While requesting resource #{Trenni::Strings::to_html env['PATH_INFO']}, a fatal error occurred.</p>"
|
39
|
+
body.puts "<blockquote><strong>#{Trenni::Strings::to_html exception.class.name}</strong>: #{Trenni::Strings::to_html exception.to_s}</blockquote>"
|
40
|
+
body.puts "<p>There is nothing more we can do to fix the problem at this point.</p>"
|
41
|
+
body.puts "<p>We apologize for the inconvenience.</p>"
|
42
|
+
body.puts "</body></html>"
|
43
|
+
body.rewind
|
44
|
+
|
45
|
+
return [400, {"Content-Type" => "text/html"}, body]
|
46
|
+
end
|
47
|
+
|
48
|
+
def redirect(env, ex)
|
49
|
+
return @app.call(env.merge('PATH_INFO' => @location, 'REQUEST_METHOD' => 'GET'))
|
50
|
+
end
|
51
|
+
|
52
|
+
def call(env)
|
53
|
+
begin
|
54
|
+
return @app.call(env)
|
55
|
+
rescue Exception => exception
|
56
|
+
# An error has occurred, log it:
|
57
|
+
log = ::Logger.new(env['rack.errors'] || $stderr)
|
58
|
+
|
59
|
+
log.error "Exception #{exception.to_s.dump}!"
|
60
|
+
|
61
|
+
ex.backtrace.each do |bt|
|
62
|
+
log.error bt
|
63
|
+
end
|
64
|
+
|
65
|
+
# If the error occurred while accessing the error handler, we finish with a fatal error:
|
66
|
+
if env['PATH_INFO'] == @location
|
67
|
+
return fatal_error(env, ex)
|
68
|
+
else
|
69
|
+
# If redirection fails, we also finish with a fatal error:
|
70
|
+
begin
|
71
|
+
return redirect(env, ex)
|
72
|
+
rescue
|
73
|
+
return fatal_error(env, ex)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# Copyright, 2012, 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 'middleware'
|
22
|
+
|
23
|
+
module Rack
|
24
|
+
class Request
|
25
|
+
def current_locale
|
26
|
+
env[Utopia::Localization::CURRENT_LOCALE_KEY]
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_locale
|
30
|
+
localization.default_locale
|
31
|
+
end
|
32
|
+
|
33
|
+
def all_locales
|
34
|
+
localization.all_locales
|
35
|
+
end
|
36
|
+
|
37
|
+
def localization
|
38
|
+
env[Utopia::Localization::LOCALIZATION_KEY]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Utopia
|
44
|
+
# If you request a URL which has localized content, a localized redirect would be returned based on the content requested.
|
45
|
+
class Localization
|
46
|
+
RESOURCE_NOT_FOUND = [400, {}, []].freeze
|
47
|
+
|
48
|
+
HTTP_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
|
49
|
+
LOCALIZATION_KEY = 'utopia.localization'.freeze
|
50
|
+
CURRENT_LOCALE_KEY = 'utopia.localization.current_locale'.freeze
|
51
|
+
|
52
|
+
def initialize(app, options = {})
|
53
|
+
@app = app
|
54
|
+
|
55
|
+
@default_locale = options[:default_locale] || "en"
|
56
|
+
@all_locales = options[:locales] || ["en"]
|
57
|
+
|
58
|
+
@nonlocalized = options.fetch(:nonlocalized, [])
|
59
|
+
end
|
60
|
+
|
61
|
+
attr :all_locales
|
62
|
+
attr :default_locale
|
63
|
+
|
64
|
+
def preferred_locales(env)
|
65
|
+
request_preferred_locales(env) | browser_preferred_locales(env) | [@default_locale, nil]
|
66
|
+
end
|
67
|
+
|
68
|
+
def request_preferred_locales(env)
|
69
|
+
path = Path[env['PATH_INFO']]
|
70
|
+
|
71
|
+
if all_locales.include? path.first
|
72
|
+
request_locale = path.first
|
73
|
+
|
74
|
+
# Remove the localization prefix.
|
75
|
+
path.delete_at(0)
|
76
|
+
env['PATH_INFO'] = path.to_s
|
77
|
+
|
78
|
+
return [request_locale]
|
79
|
+
else
|
80
|
+
return []
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def browser_preferred_locales(env)
|
85
|
+
accept_languages = env[HTTP_ACCEPT_LANGUAGE]
|
86
|
+
|
87
|
+
# No user prefered languages:
|
88
|
+
return [] unless accept_languages
|
89
|
+
|
90
|
+
languages = accept_languages.split(',').map { |language|
|
91
|
+
language.split(';q=').tap{|x| x[1] = (x[1] || 1.0).to_f}
|
92
|
+
}.sort{|a, b| b[1] <=> a[1]}.collect(&:first)
|
93
|
+
|
94
|
+
# Returns languages based on the order of the first argument
|
95
|
+
return languages & @all_locales
|
96
|
+
end
|
97
|
+
|
98
|
+
def nonlocalized?(env)
|
99
|
+
path_info = env['PATH_INFO']
|
100
|
+
|
101
|
+
@nonlocalized.any? { |pattern| path_info[pattern] != nil }
|
102
|
+
end
|
103
|
+
|
104
|
+
# Set the Vary: header on the response to indicate that this response should include the header in the cache key.
|
105
|
+
def vary(env, response)
|
106
|
+
headers = response[1]
|
107
|
+
|
108
|
+
# This response was based on the Accept-Language header:
|
109
|
+
if headers['Vary']
|
110
|
+
headers['Vary'] += ',Accept-Language'
|
111
|
+
else
|
112
|
+
headers['Vary'] = 'Accept-Language'
|
113
|
+
end
|
114
|
+
|
115
|
+
# Althought this header is generally not supported, we supply it anyway as it is useful for debugging:
|
116
|
+
if locale = env[CURRENT_LOCALE_KEY]
|
117
|
+
# Set the Content-Location to point to the localized URI as requested:
|
118
|
+
headers['Content-Location'] = "/#{locale}" + env['PATH_INFO']
|
119
|
+
end
|
120
|
+
|
121
|
+
return response
|
122
|
+
end
|
123
|
+
|
124
|
+
def call(env)
|
125
|
+
return @app.call(env) if nonlocalized?(env)
|
126
|
+
|
127
|
+
env[LOCALIZATION_KEY] = self
|
128
|
+
|
129
|
+
response = nil
|
130
|
+
|
131
|
+
# We have a non-localized request, but there might be a localized resource. We return the best localization possible:
|
132
|
+
preferred_locales(env).each do |locale|
|
133
|
+
env[CURRENT_LOCALE_KEY] = locale
|
134
|
+
|
135
|
+
response = @app.call(env)
|
136
|
+
|
137
|
+
break unless response[0] >= 400
|
138
|
+
end
|
139
|
+
|
140
|
+
return vary(env, response)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|