utopia 2.32.1 → 3.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +1 -1
- data/bake/utopia/environment.rb +6 -6
- data/bake/utopia/server.rb +4 -1
- data/bake/utopia/site.rb +14 -14
- data/bake/utopia/static.rb +26 -13
- data/bake/utopia.rb +1 -1
- data/context/getting-started.md +7 -5
- data/context/index.yaml +1 -2
- data/context/middleware.md +42 -13
- data/lib/utopia/application.rb +83 -0
- data/lib/utopia/content/builder.rb +25 -0
- data/lib/utopia/content/document.rb +64 -6
- data/lib/utopia/content/link.rb +50 -5
- data/lib/utopia/content/links.rb +44 -7
- data/lib/utopia/content/markup.rb +74 -0
- data/lib/utopia/content/middleware.rb +48 -20
- data/lib/utopia/content/namespace.rb +10 -1
- data/lib/utopia/content/node.rb +66 -4
- data/lib/utopia/content/response.rb +12 -3
- data/lib/utopia/content.rb +3 -0
- data/lib/utopia/controller/actions.md +4 -4
- data/lib/utopia/controller/actions.rb +51 -3
- data/lib/utopia/controller/base.rb +72 -29
- data/lib/utopia/controller/middleware.rb +20 -15
- data/lib/utopia/controller/respond.rb +36 -32
- data/lib/utopia/controller/responder.rb +106 -45
- data/lib/utopia/controller/result.rb +11 -0
- data/lib/utopia/controller/rewrite.rb +39 -0
- data/lib/utopia/controller/variables.rb +15 -3
- data/lib/utopia/controller.rb +2 -0
- data/lib/utopia/exceptions/handler.rb +20 -11
- data/lib/utopia/exceptions/mailer.rb +56 -51
- data/lib/utopia/extensions/array_split.rb +6 -0
- data/lib/utopia/extensions/date_comparisons.rb +6 -0
- data/lib/utopia/http.rb +11 -48
- data/lib/utopia/import_map.rb +19 -11
- data/lib/utopia/localization/locales.rb +60 -0
- data/lib/utopia/localization/middleware.rb +107 -73
- data/lib/utopia/localization/preferences.rb +76 -0
- data/lib/utopia/localization/resolver.rb +47 -0
- data/lib/utopia/localization.rb +6 -0
- data/lib/utopia/middleware.rb +3 -4
- data/lib/utopia/path/matcher.rb +15 -0
- data/lib/utopia/path.rb +146 -10
- data/lib/utopia/redirection/client_redirect.rb +87 -0
- data/lib/utopia/redirection/directory_index.rb +41 -0
- data/lib/utopia/redirection/errors.rb +78 -0
- data/lib/utopia/redirection/moved.rb +52 -0
- data/lib/utopia/redirection/request_failure.rb +26 -0
- data/lib/utopia/redirection/rewrite.rb +42 -0
- data/lib/utopia/redirection.rb +7 -171
- data/lib/utopia/request.rb +202 -0
- data/lib/utopia/response.rb +73 -0
- data/lib/utopia/session/lazy_hash.rb +27 -1
- data/lib/utopia/session/middleware.rb +103 -30
- data/lib/utopia/session/serialization.rb +8 -0
- data/lib/utopia/session.rb +5 -0
- data/lib/utopia/setup.rb +23 -3
- data/lib/utopia/shell.rb +28 -7
- data/lib/utopia/static/local_file.rb +84 -61
- data/lib/utopia/static/middleware.rb +65 -33
- data/lib/utopia/static/mime_types.rb +38 -29
- data/lib/utopia/static.rb +2 -0
- data/lib/utopia/version.rb +3 -2
- data/lib/utopia.rb +1 -1
- data/license.md +1 -1
- data/readme.md +30 -5
- data/releases.md +12 -1
- data/setup/site/bake.rb +1 -1
- data/setup/site/config/application.rb +51 -0
- data/setup/site/config/serve.rb +8 -0
- data/setup/site/falcon.rb +17 -4
- data/setup/site/fixtures/website.rb +27 -11
- data/setup/site/gems.rb +1 -3
- data/setup/site/lib/readme.txt +1 -1
- data/setup/site/pages/welcome/index.xnode +3 -3
- data/setup/site/readme.md +0 -3
- data/setup/site/test/website.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +60 -34
- metadata.gz.sig +0 -0
- data/lib/utopia/localization/wrapper.rb +0 -52
- data/setup/site/Guardfile +0 -12
- data/setup/site/config.ru +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0a73ea69813127b09be72632c42f1ce6a6a4e94678c823575125223ae19b520
|
|
4
|
+
data.tar.gz: 8c50126b47f9184bce9237dade14818cf70d959f0aae2fb34f8fb486a22a0d0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12bf144743c5b6cae0f147e60d37815c081025a86c6ac7c4146fbb392087819ddafa80c431579855ddd1e7b565fbc24a00902df3bc56222e6d218e48e5eade98
|
|
7
|
+
data.tar.gz: 76d0216724af21e389d71b6236ab36ebf5810f6365a536f000e6601d83c8df03a451a9e5eecc2cdcb045c8c1b206018a02a4762433e2e6b227a1508dd6df34f2
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
�
|
|
1
|
+
<,z��x��f�����e�>z�r<��(���nI7r�_`���8ҍ��,+8�/���N�",4RD���0�I7T�]Ձ2]�%�0��q�E��2�P�ډ��/xA{coXK������)bѐ�9I(?�!}?~2qOҿt���}�mTcv%+�u���n��0X������n�z����{ʣA�/U����M���nw_�+�md�3G�9'T87�GeA|��*�e�6J�[>��2L�Z,7TC���p��&U-�x�%ۤ�c<#����d�4DŽ�������Hé�\�
|
data/bake/utopia/environment.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2018-
|
|
4
|
+
# Copyright, 2018-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
# Update environment variables in config/environment.yaml
|
|
7
7
|
def initialize(...)
|
|
@@ -24,7 +24,7 @@ end
|
|
|
24
24
|
# @parameter root [String] The root directory of the project.
|
|
25
25
|
def defaults(name, root: context.root)
|
|
26
26
|
update_environment(root, name) do |store|
|
|
27
|
-
Console.info(self)
|
|
27
|
+
Console.info(self){"Setting up defaults for environment #{name}..."}
|
|
28
28
|
# Set some useful defaults for the environment.
|
|
29
29
|
store["UTOPIA_SESSION_SECRET"] ||= SecureRandom.hex(40)
|
|
30
30
|
end
|
|
@@ -40,10 +40,10 @@ def update(name, root: context.root, **variables)
|
|
|
40
40
|
key = key.to_s
|
|
41
41
|
|
|
42
42
|
if value && !value.empty?
|
|
43
|
-
Console.info(self)
|
|
43
|
+
Console.info(self){"ENV[#{key.inspect}] will default to #{value.inspect} unless otherwise specified."}
|
|
44
44
|
store[key] = value
|
|
45
45
|
else
|
|
46
|
-
Console.info(self)
|
|
46
|
+
Console.info(self){"ENV[#{key.inspect}] will be unset unless otherwise specified."}
|
|
47
47
|
store.delete(key)
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -63,10 +63,10 @@ def read(name, root: context.root)
|
|
|
63
63
|
environment_path = self.environment_path(root, name)
|
|
64
64
|
|
|
65
65
|
if File.exist?(environment_path)
|
|
66
|
-
Console.debug(self)
|
|
66
|
+
Console.debug(self){"Loading environment #{name} from #{environment_path}..."}
|
|
67
67
|
YAML.load_file(environment_path)
|
|
68
68
|
else
|
|
69
|
-
Console.debug(self)
|
|
69
|
+
Console.debug(self){"No environment #{name} found at #{environment_path}."}
|
|
70
70
|
{}
|
|
71
71
|
end
|
|
72
72
|
end
|
data/bake/utopia/server.rb
CHANGED
|
@@ -31,7 +31,10 @@ SERVER_ROOT = File.join(SETUP_ROOT, "server")
|
|
|
31
31
|
def update(root: context.root)
|
|
32
32
|
# It's okay to call this on an existing repo, it will only update config as required to enable --shared.
|
|
33
33
|
# --shared allows multiple users to access the site with the same group.
|
|
34
|
-
|
|
34
|
+
arguments = ["git", "init", "--shared"]
|
|
35
|
+
arguments << "--initial-branch=main" unless File.exist?(File.join(root, ".git"))
|
|
36
|
+
|
|
37
|
+
system(*arguments, chdir: root) or fail "could not initialize repository"
|
|
35
38
|
|
|
36
39
|
system("git", "config", "receive.denyCurrentBranch", "ignore", chdir: root) or fail "could not set configuration"
|
|
37
40
|
system("git", "config", "core.worktree", root, chdir: root) or fail "could not set configuration"
|
data/bake/utopia/site.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2017-
|
|
4
|
+
# Copyright, 2017-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2020, by Michael Adams.
|
|
6
6
|
|
|
7
7
|
def initialize(...)
|
|
@@ -18,20 +18,20 @@ end
|
|
|
18
18
|
SETUP_ROOT = File.expand_path("../../setup", __dir__)
|
|
19
19
|
|
|
20
20
|
# Configuration files which should be installed/updated:
|
|
21
|
-
CONFIGURATION_FILES = [".gitignore", "config.
|
|
21
|
+
CONFIGURATION_FILES = [".gitignore", "config/application.rb", "config/environment.rb", "config/serve.rb", "falcon.rb", "gems.rb", "bake.rb", "test/website.rb", "fixtures/website.rb"]
|
|
22
22
|
|
|
23
23
|
# Directories that should exist:
|
|
24
24
|
DIRECTORIES = ["config", "lib", "pages", "public", "bake", "fixtures", "test"]
|
|
25
25
|
|
|
26
26
|
# Directories that should be removed during upgrade process:
|
|
27
|
-
OLD_PATHS = ["access_log", "cache", "tmp", "Rakefile", "tasks", ".bowerrc"]
|
|
27
|
+
OLD_PATHS = ["access_log", "cache", "tmp", "Rakefile", "tasks", ".bowerrc", "Guardfile"]
|
|
28
28
|
|
|
29
29
|
# The root directory of the template site:
|
|
30
30
|
SITE_ROOT = File.join(SETUP_ROOT, "site")
|
|
31
31
|
|
|
32
32
|
# Create a new local Utopia website using the default template.
|
|
33
33
|
def create(root: context.root)
|
|
34
|
-
Console.debug(self)
|
|
34
|
+
Console.debug(self){"Setting up site in #{root} for Utopia v#{Utopia::VERSION}..."}
|
|
35
35
|
|
|
36
36
|
DIRECTORIES.each do |directory|
|
|
37
37
|
FileUtils.mkdir_p(File.join(root, directory))
|
|
@@ -57,9 +57,9 @@ def create(root: context.root)
|
|
|
57
57
|
|
|
58
58
|
if File.exist?(destination_path)
|
|
59
59
|
buffer = File.read(destination_path).gsub("$UTOPIA_VERSION", Utopia::VERSION)
|
|
60
|
-
File.open(destination_path, "w")
|
|
60
|
+
File.open(destination_path, "w"){|file| file.write(buffer)}
|
|
61
61
|
else
|
|
62
|
-
Console.warn(self)
|
|
62
|
+
Console.warn(self){"Could not open #{destination_path}, maybe it should be removed from CONFIGURATION_FILES?"}
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -67,10 +67,10 @@ def create(root: context.root)
|
|
|
67
67
|
|
|
68
68
|
context.lookup("utopia:environment:setup").call(root: root)
|
|
69
69
|
|
|
70
|
-
if !File.exist?(".git")
|
|
71
|
-
Console.info(self)
|
|
70
|
+
if !File.exist?(File.join(root, ".git"))
|
|
71
|
+
Console.info(self){"Setting up git repository..."}
|
|
72
72
|
|
|
73
|
-
system("git", "init", chdir: root) or warn "could not create git repository"
|
|
73
|
+
system("git", "init", "--initial-branch=main", chdir: root) or warn "could not create git repository"
|
|
74
74
|
system("git", "add", ".", chdir: root) or warn "could not add all files"
|
|
75
75
|
system("git", "commit", "-q", "-m", "Initial Utopia v#{Utopia::VERSION} site.", chdir: root) or warn "could not commit files"
|
|
76
76
|
end
|
|
@@ -95,7 +95,7 @@ end
|
|
|
95
95
|
def upgrade(root: context.root)
|
|
96
96
|
message = "Upgrade to utopia v#{Utopia::VERSION}."
|
|
97
97
|
|
|
98
|
-
Console.info(self)
|
|
98
|
+
Console.info(self){"Upgrading #{root}..."}
|
|
99
99
|
|
|
100
100
|
commit_changes(root, message) do
|
|
101
101
|
DIRECTORIES.each do |directory|
|
|
@@ -106,7 +106,7 @@ def upgrade(root: context.root)
|
|
|
106
106
|
path = File.join(root, path)
|
|
107
107
|
|
|
108
108
|
if File.exist?(path)
|
|
109
|
-
Console.info(self)
|
|
109
|
+
Console.info(self){"Removing #{path}..."}
|
|
110
110
|
FileUtils.rm_rf(path)
|
|
111
111
|
end
|
|
112
112
|
end
|
|
@@ -115,11 +115,11 @@ def upgrade(root: context.root)
|
|
|
115
115
|
source_path = File.join(SITE_ROOT, configuration_file)
|
|
116
116
|
destination_path = File.join(root, configuration_file)
|
|
117
117
|
|
|
118
|
-
Console.info(self)
|
|
118
|
+
Console.info(self){"Updating #{destination_path}..."}
|
|
119
119
|
|
|
120
120
|
FileUtils.copy_entry(source_path, destination_path)
|
|
121
121
|
buffer = File.read(destination_path).gsub("$UTOPIA_VERSION", Utopia::VERSION)
|
|
122
|
-
File.open(destination_path, "w")
|
|
122
|
+
File.open(destination_path, "w"){|file| file.write(buffer)}
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
context.lookup("utopia:environment:setup").call(root: root)
|
|
@@ -166,7 +166,7 @@ def move_static!(root)
|
|
|
166
166
|
if File.lstat(new_static_path).symlink?
|
|
167
167
|
FileUtils.rm_f new_static_path
|
|
168
168
|
else
|
|
169
|
-
Console.warn(self)
|
|
169
|
+
Console.warn(self){"Can't move pages/_static to public/_static, destination already exists."}
|
|
170
170
|
return
|
|
171
171
|
end
|
|
172
172
|
end
|
data/bake/utopia/static.rb
CHANGED
|
@@ -3,22 +3,29 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2017-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# Generate a static copy of the application.
|
|
7
|
+
# @parameter output_path [String] The output path for the generated site.
|
|
8
|
+
# @parameter application_path [String] The application configuration path.
|
|
9
|
+
# @parameter public_path [String] The public assets path.
|
|
10
|
+
# @parameter force [Boolean] Remove the output directory before generating the site.
|
|
11
|
+
def generate(output_path: "static", application_path: "config/application.rb", public_path: "public", force: true)
|
|
7
12
|
require "falcon/server"
|
|
8
|
-
require "async/io"
|
|
9
13
|
require "async/http/endpoint"
|
|
10
14
|
require "async/container"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
require "utopia/application"
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
application_path = File.expand_path(application_path, Dir.pwd)
|
|
19
|
+
public_path = File.expand_path(public_path, Dir.pwd)
|
|
13
20
|
container_class = Async::Container::Threaded
|
|
14
21
|
server_port = 9090
|
|
15
22
|
|
|
16
|
-
app
|
|
23
|
+
app = Utopia::Application.load(application_path)
|
|
17
24
|
|
|
18
|
-
container = container_class.run(count:
|
|
25
|
+
container = container_class.run(count: 1) do
|
|
19
26
|
Async do
|
|
20
27
|
server = Falcon::Server.new(
|
|
21
|
-
Falcon::Server.
|
|
28
|
+
Falcon::Server.protocol_middleware(app),
|
|
22
29
|
Async::HTTP::Endpoint.parse("http://localhost:#{server_port}")
|
|
23
30
|
)
|
|
24
31
|
|
|
@@ -28,17 +35,23 @@ def generate(output_path: "static")
|
|
|
28
35
|
|
|
29
36
|
output_path = File.expand_path(output_path, Dir.pwd)
|
|
30
37
|
|
|
31
|
-
# Delete
|
|
32
|
-
|
|
38
|
+
# Delete existing output when explicitly requested:
|
|
39
|
+
if force
|
|
40
|
+
FileUtils.rm_rf(output_path)
|
|
41
|
+
end
|
|
33
42
|
|
|
34
43
|
# Copy all public assets:
|
|
35
44
|
FileUtils::Verbose.mkpath(output_path)
|
|
36
|
-
Dir.glob(File.join(
|
|
45
|
+
Dir.glob(File.join(public_path, "*")) do |path|
|
|
37
46
|
FileUtils::Verbose.cp_r(path, output_path)
|
|
38
47
|
end
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
begin
|
|
50
|
+
# Generate HTML pages:
|
|
51
|
+
unless system("wget", "--mirror", "--recursive", "--continue", "--convert-links", "--adjust-extension", "--no-host-directories", "--directory-prefix", output_path.to_s, "http://localhost:#{server_port}")
|
|
52
|
+
raise "Static site generation failed!"
|
|
53
|
+
end
|
|
54
|
+
ensure
|
|
55
|
+
container.stop
|
|
56
|
+
end
|
|
44
57
|
end
|
data/bake/utopia.rb
CHANGED
data/context/getting-started.md
CHANGED
|
@@ -4,7 +4,7 @@ This guide explains how to set up a `utopia` website for local development and d
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
Utopia is built on Ruby
|
|
7
|
+
Utopia is built on Ruby. Therefore, Ruby should be installed and working. Then, to install `utopia` and all required dependencies, run:
|
|
8
8
|
|
|
9
9
|
~~~ bash
|
|
10
10
|
$ gem install utopia
|
|
@@ -32,10 +32,12 @@ You will now have a basic template site running on `https://localhost:9292`.
|
|
|
32
32
|
Utopia includes a redirection middleware to redirect all root-level requests to a given URI. The default being `/welcome/index`:
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
|
-
# in config.
|
|
35
|
+
# in config/application.rb
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
Application = Utopia::Application.build do
|
|
38
|
+
use Utopia::Redirection::Rewrite,
|
|
39
|
+
"/" => "/welcome/index"
|
|
40
|
+
end
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
The content for this page is stored in `pages/welcome/index.xnode`. The format of this page is a subset of HTML5 - open and close tags are strictly enforced.
|
|
@@ -84,7 +86,7 @@ website
|
|
|
84
86
|
|
|
85
87
|
Least Coverage:
|
|
86
88
|
pages/_page.xnode: 6 lines not executed!
|
|
87
|
-
config.
|
|
89
|
+
config/application.rb: 4 lines not executed!
|
|
88
90
|
pages/welcome/index.xnode: 2 lines not executed!
|
|
89
91
|
pages/_heading.xnode: 1 lines not executed!
|
|
90
92
|
|
data/context/index.yaml
CHANGED
|
@@ -13,8 +13,7 @@ files:
|
|
|
13
13
|
and deployment.
|
|
14
14
|
- path: middleware.md
|
|
15
15
|
title: Middleware
|
|
16
|
-
description: This guide gives an overview of the different
|
|
17
|
-
by Utopia.
|
|
16
|
+
description: This guide gives an overview of the different middleware used by Utopia.
|
|
18
17
|
- path: server-setup.md
|
|
19
18
|
title: Server Setup
|
|
20
19
|
description: This guide explains how to deploy a `utopia` web application.
|
data/context/middleware.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Middleware
|
|
2
2
|
|
|
3
|
-
This guide gives an overview of the different
|
|
3
|
+
This guide gives an overview of the different middleware used by Utopia.
|
|
4
4
|
|
|
5
5
|
## Static
|
|
6
6
|
|
|
7
|
-
The {ruby Utopia::Static} middleware services static files efficiently
|
|
7
|
+
The {ruby Utopia::Static} middleware services static files efficiently and supports `ETag` based caching. Normally, you'd prefer to put static files into `public/_static` but it's also acceptable to put static content into `pages/` if it makes sense.
|
|
8
8
|
|
|
9
9
|
~~~ ruby
|
|
10
10
|
use Utopia::Static,
|
|
11
11
|
# The root path to serve files from:
|
|
12
12
|
root: "path/to/root",
|
|
13
|
-
# The
|
|
13
|
+
# The file extension groups to recognize/serve:
|
|
14
14
|
types: [:default, :xiph],
|
|
15
15
|
# Cache-Control header for files:
|
|
16
16
|
cache_control: 'public, max-age=7200'
|
|
@@ -29,14 +29,18 @@ use Utopia::Redirection::Rewrite,
|
|
|
29
29
|
use Utopia::Redirection::DirectoryIndex,
|
|
30
30
|
index: 'index.html'
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
~~~
|
|
33
|
+
|
|
34
|
+
Place {ruby Utopia::Redirection::Errors} after all client-visible redirection middleware. It maps unhandled error responses to internal error documents while retaining the original response status. Because its internal requests invoke the downstream application directly, redirects configured before it are bypassed:
|
|
35
|
+
|
|
36
|
+
~~~ ruby
|
|
33
37
|
use Utopia::Redirection::Errors,
|
|
34
38
|
404 => '/errors/file-not-found'
|
|
35
39
|
~~~
|
|
36
40
|
|
|
37
41
|
## Localization
|
|
38
42
|
|
|
39
|
-
The {ruby Utopia::Localization} middleware
|
|
43
|
+
The {ruby Utopia::Localization} middleware computes immutable localization preferences from the request path, host, and `accept-language` header. Localization-aware resource middleware, including {ruby Utopia::Static} and {ruby Utopia::Content}, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.
|
|
40
44
|
|
|
41
45
|
~~~ ruby
|
|
42
46
|
use Utopia::Localization,
|
|
@@ -53,7 +57,7 @@ pages/index.ja.xnode
|
|
|
53
57
|
pages/index.zh.xnode
|
|
54
58
|
~~~
|
|
55
59
|
|
|
56
|
-
You can
|
|
60
|
+
You can access the selected locale in a view using `localization.locale`. Controllers can inspect the request preferences using `request.localization`.
|
|
57
61
|
|
|
58
62
|
## Controller
|
|
59
63
|
|
|
@@ -73,7 +77,7 @@ A controller is a file within the specified root directory (typically `pages`) w
|
|
|
73
77
|
def passthrough(request, path)
|
|
74
78
|
# Call one of:
|
|
75
79
|
|
|
76
|
-
#
|
|
80
|
+
# Respond immediately with a complete Protocol::HTTP::Response.
|
|
77
81
|
# def respond!(response)
|
|
78
82
|
|
|
79
83
|
# This will cause the controller to skip the request.
|
|
@@ -85,17 +89,36 @@ def passthrough(request, path)
|
|
|
85
89
|
# Controller relative redirect.
|
|
86
90
|
# def goto!(target, status = 302)
|
|
87
91
|
|
|
88
|
-
# Respond with an error which
|
|
92
|
+
# Respond with an error which indicates some kind of failure.
|
|
89
93
|
# def fail!(error = 400, message = nil)
|
|
90
94
|
|
|
91
|
-
# Succeed the
|
|
92
|
-
# def succeed!(status: 200, headers: {}
|
|
93
|
-
# options may include content: string or body: Enumerable (as per Rack specifications
|
|
95
|
+
# Succeed with a semantic value which the Respond layer serializes.
|
|
96
|
+
# def succeed!(value = nil, status: 200, headers: {})
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
succeed!
|
|
96
99
|
end
|
|
97
100
|
```
|
|
98
101
|
|
|
102
|
+
Controllers which return semantic values should prepend {ruby Utopia::Controller::Respond} and configure serializers. Serializer blocks return wire-ready response bodies, so they can return streaming {ruby Protocol::HTTP::Body::Readable} objects without buffering them.
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
prepend Utopia::Controller::Respond, Utopia::Controller::Actions
|
|
106
|
+
|
|
107
|
+
responds.with("application/json") do |media_range, value|
|
|
108
|
+
JSON.dump(value)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
on "show" do
|
|
112
|
+
succeed!({"name" => "Samuel"})
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use `respond!` when a controller has already constructed a complete response and no content negotiation is required:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
respond! Utopia::Response[200, {"content-type" => "text/plain"}, ["Hello World"]]
|
|
120
|
+
```
|
|
121
|
+
|
|
99
122
|
The controller layer can do more complex operations by prepending modules into it.
|
|
100
123
|
|
|
101
124
|
```ruby
|
|
@@ -108,7 +131,7 @@ end
|
|
|
108
131
|
|
|
109
132
|
on "edit" do |request, path|
|
|
110
133
|
if request.post?
|
|
111
|
-
@user.update_attributes(request[
|
|
134
|
+
@user.update_attributes(parse_body(request)["user"])
|
|
112
135
|
end
|
|
113
136
|
end
|
|
114
137
|
|
|
@@ -155,3 +178,9 @@ use Utopia::Session,
|
|
|
155
178
|
```
|
|
156
179
|
|
|
157
180
|
All session data is stored on the client, but it's encrypted with a salt and the secret key. It is impossible for the client to decrypt the data without the secret stored on the server.
|
|
181
|
+
|
|
182
|
+
When the middleware is installed, the session is available on the request:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
request.session[:user_id] = user.id
|
|
186
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http/middleware"
|
|
7
|
+
require "protocol/http/middleware/builder"
|
|
8
|
+
|
|
9
|
+
require_relative "request"
|
|
10
|
+
require_relative "response"
|
|
11
|
+
|
|
12
|
+
module Utopia
|
|
13
|
+
# The protocol-facing entrypoint for a Utopia application.
|
|
14
|
+
#
|
|
15
|
+
# This object accepts {Protocol::HTTP::Request} instances, dispatches to the
|
|
16
|
+
# Utopia application stack, and normalizes the result back to a
|
|
17
|
+
# {Protocol::HTTP::Response}.
|
|
18
|
+
class Application < Protocol::HTTP::Middleware
|
|
19
|
+
PATH = "config/application.rb".freeze
|
|
20
|
+
|
|
21
|
+
# Build a Utopia application stack using the protocol HTTP middleware builder.
|
|
22
|
+
# @parameter default_app [Protocol::HTTP::Middleware] The default application used when the block does not call `run`.
|
|
23
|
+
# @parameter block [Proc] The middleware builder block.
|
|
24
|
+
# @returns [Application] The protocol-facing Utopia application.
|
|
25
|
+
def self.build(default_app = Response::NotFound, &block)
|
|
26
|
+
builder = Protocol::HTTP::Middleware::Builder.new(default_app)
|
|
27
|
+
|
|
28
|
+
if block
|
|
29
|
+
if block.arity.zero?
|
|
30
|
+
builder.instance_exec(&block)
|
|
31
|
+
else
|
|
32
|
+
block.call(builder)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return self.new(builder.to_app)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Build the default Utopia application.
|
|
40
|
+
# @returns [Application] The default protocol-facing Utopia application.
|
|
41
|
+
def self.default
|
|
42
|
+
self.build
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Load a Utopia application from a conventional configuration file.
|
|
46
|
+
#
|
|
47
|
+
# If the file defines an `Application` constant, it will be returned
|
|
48
|
+
# directly. If the constant is a class, it will be instantiated.
|
|
49
|
+
# If the file does not exist, or does not define `Application`, the default
|
|
50
|
+
# application is returned.
|
|
51
|
+
#
|
|
52
|
+
# @parameter path [String] The application configuration path.
|
|
53
|
+
# @parameter options [Hash] Options passed to the application constructor.
|
|
54
|
+
# @returns [Protocol::HTTP::Middleware] The loaded protocol-facing application.
|
|
55
|
+
def self.load(path = PATH, **options)
|
|
56
|
+
if File.exist?(path)
|
|
57
|
+
top = Module.new
|
|
58
|
+
top.class_eval(File.read(path), path)
|
|
59
|
+
|
|
60
|
+
if top.const_defined?(:Application, false)
|
|
61
|
+
application = top.const_get(:Application)
|
|
62
|
+
|
|
63
|
+
if application.is_a?(Class)
|
|
64
|
+
return application.new(**options)
|
|
65
|
+
else
|
|
66
|
+
return application
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return self.default
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Process a protocol HTTP request.
|
|
75
|
+
# @parameter request [Protocol::HTTP::Request] The incoming protocol request.
|
|
76
|
+
# @returns [Protocol::HTTP::Response] The normalized protocol response.
|
|
77
|
+
def call(request)
|
|
78
|
+
request = Request.new(request)
|
|
79
|
+
|
|
80
|
+
return Response.wrap(super(request))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -11,6 +11,11 @@ module Utopia
|
|
|
11
11
|
|
|
12
12
|
# A builder for rendering Utopia content that extends XRB::Builder with Utopia-specific functionality.
|
|
13
13
|
class Builder < XRB::Builder
|
|
14
|
+
# Initialize rendering state for a content node.
|
|
15
|
+
# @parameter parent [Builder | Nil] The enclosing builder state.
|
|
16
|
+
# @parameter tag [XRB::Tag | Nil] The tag that opened this state.
|
|
17
|
+
# @parameter node [Utopia::Content::Node] The content node.
|
|
18
|
+
# @parameter attributes [Hash] The attributes.
|
|
14
19
|
def initialize(parent, tag, node, attributes = tag.to_hash, **options)
|
|
15
20
|
super(**options)
|
|
16
21
|
|
|
@@ -35,16 +40,26 @@ module Utopia
|
|
|
35
40
|
|
|
36
41
|
attr :deferred
|
|
37
42
|
|
|
43
|
+
# Insert a deferred-content marker and retain its rendering block.
|
|
44
|
+
# @parameter value [Object | Nil] An unused compatibility argument.
|
|
45
|
+
# @yields {|document| ...} The deferred rendering operation.
|
|
46
|
+
# @returns [String] The closed deferred-content tag.
|
|
38
47
|
def defer(value = nil, &block)
|
|
39
48
|
@deferred << block
|
|
40
49
|
|
|
41
50
|
XRB::Tag.closed(DEFERRED_TAG_NAME, :id => @deferred.size - 1)
|
|
42
51
|
end
|
|
43
52
|
|
|
53
|
+
# Fetch an attribute for the current content node.
|
|
54
|
+
# @parameter key [String | Symbol] The lookup key.
|
|
55
|
+
# @returns [Object | Nil] The attribute value.
|
|
44
56
|
def [](key)
|
|
45
57
|
@attributes[key]
|
|
46
58
|
end
|
|
47
59
|
|
|
60
|
+
# Render this node's captured content into a document.
|
|
61
|
+
# @parameter document [Utopia::Content::Document] The content document.
|
|
62
|
+
# @returns [String] The rendered output buffer.
|
|
48
63
|
def call(document)
|
|
49
64
|
@content = @output.dup
|
|
50
65
|
@output.clear
|
|
@@ -74,6 +89,9 @@ module Utopia
|
|
|
74
89
|
end
|
|
75
90
|
end
|
|
76
91
|
|
|
92
|
+
# Write a complete tag to the output.
|
|
93
|
+
# @parameter tag [XRB::Tag] The tag.
|
|
94
|
+
# @returns [String] The output buffer.
|
|
77
95
|
def tag_complete(tag)
|
|
78
96
|
tag.write(@output)
|
|
79
97
|
end
|
|
@@ -83,11 +101,18 @@ module Utopia
|
|
|
83
101
|
@tags.empty?
|
|
84
102
|
end
|
|
85
103
|
|
|
104
|
+
# Tag begin.
|
|
105
|
+
# @parameter tag [XRB::Tag] The opening tag.
|
|
106
|
+
# @returns [String] The output buffer.
|
|
86
107
|
def tag_begin(tag)
|
|
87
108
|
@tags << tag
|
|
88
109
|
tag.write_opening_tag(@output)
|
|
89
110
|
end
|
|
90
111
|
|
|
112
|
+
# Tag end.
|
|
113
|
+
# @parameter tag [XRB::Tag] The closing tag.
|
|
114
|
+
# @returns [String] The output buffer.
|
|
115
|
+
# @raises [UnbalancedTagError] If the closing tag does not match the most recent opening tag.
|
|
91
116
|
def tag_end(tag)
|
|
92
117
|
raise UnbalancedTagError.new(tag) unless @tags.pop.name == tag.name
|
|
93
118
|
tag.write_closing_tag(@output)
|