utopia 3.0.3 → 3.0.5
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 +0 -0
- data/bake/utopia/components.rb +44 -0
- data/context/integrating-with-javascript.md +1 -1
- data/context/middleware.md +2 -2
- data/lib/traces/provider/utopia/content/middleware.rb +20 -0
- data/lib/traces/provider/utopia/static/middleware.rb +19 -0
- data/lib/traces/provider/utopia.rb +7 -0
- data/lib/utopia/components.rb +174 -0
- data/lib/utopia/content/builder.rb +1 -5
- data/lib/utopia/content/links.rb +5 -3
- data/lib/utopia/content/markup.rb +3 -3
- data/lib/utopia/content/middleware.rb +16 -22
- data/lib/utopia/content/node.rb +7 -9
- data/lib/utopia/controller/rewrite.rb +3 -1
- data/lib/utopia/exceptions/application_errors.rb +11 -0
- data/lib/utopia/exceptions/handler.rb +4 -3
- data/lib/utopia/exceptions/mailer.rb +81 -19
- data/lib/utopia/path.rb +11 -16
- data/lib/utopia/request.rb +5 -20
- data/lib/utopia/session/lazy_hash.rb +28 -9
- data/lib/utopia/session/middleware.rb +74 -49
- data/lib/utopia/static/local_file.rb +7 -3
- data/lib/utopia/static/middleware.rb +0 -12
- data/lib/utopia/version.rb +1 -1
- data/readme.md +9 -0
- data/releases.md +9 -0
- data.tar.gz.sig +0 -0
- metadata +9 -4
- metadata.gz.sig +0 -0
- data/bake/utopia/node.rb +0 -87
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0314485e770b9085839422a7484c179a39e447d81b15b211d70fbb4361fdc8b2'
|
|
4
|
+
data.tar.gz: 6e583362bc02ce48424f8e3adf7b9528bfd0990ea4ff946879d4bfbc95a52624
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a81a495790a61faea6d2ab3873fd4043807134a4dec40b50177318802eefc14484e8dc218373fbf32b8bfe441ff96038ccaa21093acda9610d1ebc3ad6e87bb
|
|
7
|
+
data.tar.gz: 35d548a73022d0307b8a631d3a8c755b1eef9fcb0edb3f7b224ac2ef57b2334018928f3c8a47e4fea6812882ce794973a639af563b5a941627f3cafcce8ca971
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
NPM = ENV["NPM"] || "npm"
|
|
7
|
+
|
|
8
|
+
# Update public components from production JavaScript packages.
|
|
9
|
+
#
|
|
10
|
+
# Packages are copied from their `dist` directory when present, or otherwise
|
|
11
|
+
# from the package root. The `utopia.components` section of `package.json` can
|
|
12
|
+
# specify per-package `include` patterns to select only required files.
|
|
13
|
+
#
|
|
14
|
+
# @parameter root [String] The project root directory.
|
|
15
|
+
def update(root: context.root)
|
|
16
|
+
require "json"
|
|
17
|
+
require "open3"
|
|
18
|
+
require "utopia/components"
|
|
19
|
+
|
|
20
|
+
components = Utopia::Components.new(root)
|
|
21
|
+
production_packages = fetch_production_packages(components.package_root)
|
|
22
|
+
|
|
23
|
+
components.update(production_packages)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def fetch_production_packages(package_root)
|
|
29
|
+
stdout, _status = Open3.capture2(NPM, "ls", "--production", "--json", chdir: package_root.to_s)
|
|
30
|
+
json = JSON.parse(stdout)
|
|
31
|
+
|
|
32
|
+
flatten_package_dependencies(json).sort.uniq
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flatten_package_dependencies(json, into = [])
|
|
36
|
+
if json["dependencies"]
|
|
37
|
+
json["dependencies"].each do |name, details|
|
|
38
|
+
into << name
|
|
39
|
+
flatten_package_dependencies(details, into)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return into
|
|
44
|
+
end
|
|
@@ -17,7 +17,7 @@ $ npm install jquery
|
|
|
17
17
|
Copy the distribution files to `public/_components`:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
$ bundle exec bake utopia:
|
|
20
|
+
$ bundle exec bake utopia:components:update
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
This will copy the library's distribution files (typically from `node_modules/*/dist/`) to your `public/_components/` directory, making them available for local serving.
|
data/context/middleware.md
CHANGED
|
@@ -167,7 +167,7 @@ This template would typically be designed with supporting `_page.xnode` and `_he
|
|
|
167
167
|
|
|
168
168
|
## Session
|
|
169
169
|
|
|
170
|
-
The {ruby Utopia::Session} middleware provides session storage using encrypted client-side cookies. The session management uses symmetric private key encryption to store data on the client and
|
|
170
|
+
The {ruby Utopia::Session} middleware provides session storage using authenticated, encrypted client-side cookies. The session management uses symmetric private key encryption to store data on the client and prevent tampering.
|
|
171
171
|
|
|
172
172
|
```ruby
|
|
173
173
|
use Utopia::Session,
|
|
@@ -177,7 +177,7 @@ use Utopia::Session,
|
|
|
177
177
|
secure: true
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
All session data is stored on the client, but it
|
|
180
|
+
All session data is stored on the client, but it is encrypted and authenticated with a key derived from the secret. The client cannot read or modify the data without the secret stored on the server.
|
|
181
181
|
|
|
182
182
|
When the middleware is installed, the session is available on the request:
|
|
183
183
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../../../../utopia/content/middleware"
|
|
7
|
+
|
|
8
|
+
require "traces/provider"
|
|
9
|
+
|
|
10
|
+
Traces::Provider(Utopia::Content::Middleware) do
|
|
11
|
+
def respond(link, request, localization: request.localization)
|
|
12
|
+
attributes = {
|
|
13
|
+
"link.key" => link.key,
|
|
14
|
+
"link.href" => link.href,
|
|
15
|
+
"link.locale" => localization&.locale,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Traces.trace("utopia.content.respond", attributes: attributes){super}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../../../../utopia/static/middleware"
|
|
7
|
+
|
|
8
|
+
require "traces/provider"
|
|
9
|
+
|
|
10
|
+
Traces::Provider(Utopia::Static::Middleware) do
|
|
11
|
+
def respond(request, path, extension, content_type, localization: request.localization)
|
|
12
|
+
attributes = {
|
|
13
|
+
path: path,
|
|
14
|
+
locale: localization&.locale,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Traces.trace("utopia.static.respond", attributes: attributes){super}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "json"
|
|
8
|
+
require "pathname"
|
|
9
|
+
|
|
10
|
+
module Utopia
|
|
11
|
+
# Installs JavaScript packages from `node_modules` into the public components directory. Package contents are copied from `dist` when it exists, otherwise from the package root.
|
|
12
|
+
#
|
|
13
|
+
# By default, the complete source directory is installed. Projects can limit an individual package to a set of files using `utopia.components` in their `package.json` file.
|
|
14
|
+
class Components
|
|
15
|
+
# Initialize a component installer for the given project root.
|
|
16
|
+
#
|
|
17
|
+
# @parameter root [String | Pathname] The project root directory.
|
|
18
|
+
def initialize(root)
|
|
19
|
+
@root = Pathname.new(root)
|
|
20
|
+
@package_root = @root + "node_modules"
|
|
21
|
+
|
|
22
|
+
@install_root = @root + "public/_components"
|
|
23
|
+
@configuration = load_configuration
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @attribute [Pathname] The directory containing the installed JavaScript packages.
|
|
27
|
+
attr :package_root
|
|
28
|
+
|
|
29
|
+
# Update the specified packages in the public components directory.
|
|
30
|
+
#
|
|
31
|
+
# @parameter package_names [Array(String)] The production package names to install.
|
|
32
|
+
def update(package_names)
|
|
33
|
+
expand_package_paths(@package_root).each do |package_path|
|
|
34
|
+
package_name = package_path.relative_path_from(@package_root).to_s
|
|
35
|
+
|
|
36
|
+
if package_names.include?(package_name)
|
|
37
|
+
install(package_name, package_path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Load the optional per-package installation rules. A missing `package.json`, or a file without `utopia.components`, preserves the default behaviour of copying complete packages.
|
|
45
|
+
# @returns [Hash] The per-package installation rules.
|
|
46
|
+
def load_configuration
|
|
47
|
+
package_path = @root + "package.json"
|
|
48
|
+
|
|
49
|
+
unless package_path.file?
|
|
50
|
+
return {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
configuration = JSON.parse(package_path.read).dig("utopia", "components") || {}
|
|
54
|
+
|
|
55
|
+
unless configuration.is_a?(Hash)
|
|
56
|
+
raise ArgumentError, "utopia.components must be an object!"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return configuration
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Install one package. Distribution directories are preferred because they generally contain the browser-ready form of a package.
|
|
63
|
+
# @parameter package_name [String] The package name relative to `node_modules`.
|
|
64
|
+
# @parameter package_path [Pathname] The package source directory.
|
|
65
|
+
def install(package_name, package_path)
|
|
66
|
+
install_path = @install_root + package_name
|
|
67
|
+
dist_path = package_path + "dist"
|
|
68
|
+
|
|
69
|
+
if dist_path.directory?
|
|
70
|
+
source_path = dist_path
|
|
71
|
+
else
|
|
72
|
+
source_path = package_path
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
configuration = @configuration[package_name]
|
|
76
|
+
|
|
77
|
+
if configuration
|
|
78
|
+
install_selected(package_name, source_path, install_path, configuration)
|
|
79
|
+
else
|
|
80
|
+
FileUtils::Verbose.rm_rf(install_path)
|
|
81
|
+
FileUtils::Verbose.mkpath(install_path.dirname)
|
|
82
|
+
FileUtils::Verbose.cp_r(source_path, install_path)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Install only the files matched by the configured include patterns. Every pattern is resolved before removing the existing installation, so an invalid configuration cannot leave a package partially installed or remove a previously working copy.
|
|
87
|
+
# @parameter package_name [String] The package name relative to `node_modules`.
|
|
88
|
+
# @parameter source_path [Pathname] The package source directory.
|
|
89
|
+
# @parameter install_path [Pathname] The destination directory.
|
|
90
|
+
# @parameter configuration [Hash] The package installation rules.
|
|
91
|
+
def install_selected(package_name, source_path, install_path, configuration)
|
|
92
|
+
unless configuration.is_a?(Hash)
|
|
93
|
+
raise ArgumentError, "utopia.components.#{package_name}.include must be a non-empty array!"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
include_patterns = configuration["include"]
|
|
97
|
+
|
|
98
|
+
unless include_patterns.is_a?(Array) && include_patterns.any?
|
|
99
|
+
raise ArgumentError, "utopia.components.#{package_name}.include must be a non-empty array!"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
paths = include_patterns.flat_map do |pattern|
|
|
103
|
+
included_paths(package_name, source_path, pattern)
|
|
104
|
+
end.uniq.sort
|
|
105
|
+
|
|
106
|
+
FileUtils::Verbose.rm_rf(install_path)
|
|
107
|
+
|
|
108
|
+
paths.each do |relative_path|
|
|
109
|
+
source_file = source_path + relative_path
|
|
110
|
+
install_file = install_path + relative_path
|
|
111
|
+
|
|
112
|
+
FileUtils::Verbose.mkpath(install_file.dirname)
|
|
113
|
+
FileUtils::Verbose.cp(source_file, install_file)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Expand one include pattern into files relative to the package source. Directories are excluded so each result can be copied independently.
|
|
118
|
+
# @parameter package_name [String] The package name used in validation errors.
|
|
119
|
+
# @parameter source_path [Pathname] The package source directory.
|
|
120
|
+
# @parameter pattern [String] The include pattern to expand.
|
|
121
|
+
# @returns [Array(String)] The matching file paths relative to the package source.
|
|
122
|
+
def included_paths(package_name, source_path, pattern)
|
|
123
|
+
unless pattern.is_a?(String) && relative_pattern?(pattern)
|
|
124
|
+
raise ArgumentError, "Invalid include pattern for #{package_name}: #{pattern.inspect}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
paths = Dir.glob(pattern, base: source_path.to_s).select do |relative_path|
|
|
128
|
+
(source_path + relative_path).file?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if paths.empty?
|
|
132
|
+
raise ArgumentError, "Include pattern for #{package_name} matched no files: #{pattern.inspect}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return paths
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Determine whether the pattern is contained within the package source. Absolute paths and parent traversal are rejected because they could otherwise copy arbitrary files from outside the package.
|
|
139
|
+
# @parameter pattern [String] The include pattern to validate.
|
|
140
|
+
# @returns [Boolean] Whether the pattern is relative and does not contain parent traversal.
|
|
141
|
+
def relative_pattern?(pattern)
|
|
142
|
+
path = Pathname.new(pattern)
|
|
143
|
+
|
|
144
|
+
if path.absolute?
|
|
145
|
+
return false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if path.each_filename.any?{|component| component == ".."}
|
|
149
|
+
return false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Enumerate packages in `node_modules`, descending through scoped package directories such as `@socketry` while preserving their scoped names.
|
|
156
|
+
# @parameter root [Pathname] The directory to enumerate.
|
|
157
|
+
# @parameter into [Array(Pathname)] The array into which package paths are appended.
|
|
158
|
+
# @returns [Array(Pathname)] The discovered package directories.
|
|
159
|
+
def expand_package_paths(root, into = [])
|
|
160
|
+
root.children.select(&:directory?).each do |path|
|
|
161
|
+
basename = path.basename.to_s
|
|
162
|
+
|
|
163
|
+
# Handle organisation sub-directories which start with an '@' symbol:
|
|
164
|
+
if basename.start_with?("@")
|
|
165
|
+
expand_package_paths(path, into)
|
|
166
|
+
else
|
|
167
|
+
into << path
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return into
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -82,11 +82,7 @@ module Utopia
|
|
|
82
82
|
def text(content)
|
|
83
83
|
return unless content
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
content.build_markup(self)
|
|
87
|
-
else
|
|
88
|
-
XRB::Markup.append(@output, content)
|
|
89
|
-
end
|
|
85
|
+
content.build_markup(self)
|
|
90
86
|
end
|
|
91
87
|
|
|
92
88
|
# Write a complete tag to the output.
|
data/lib/utopia/content/links.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2015-
|
|
4
|
+
# Copyright, 2015-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "link"
|
|
7
7
|
|
|
@@ -204,8 +204,10 @@ module Utopia
|
|
|
204
204
|
def each(locale)
|
|
205
205
|
return to_enum(:each, locale) unless block_given?
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
@named.each_key do |name|
|
|
208
|
+
if link = lookup(name, locale)
|
|
209
|
+
yield link
|
|
210
|
+
end
|
|
209
211
|
end
|
|
210
212
|
end
|
|
211
213
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2009-
|
|
4
|
+
# Copyright, 2009-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "xrb/parsers"
|
|
7
7
|
require "xrb/entities"
|
|
@@ -108,9 +108,9 @@ module Utopia
|
|
|
108
108
|
# @returns [String] The resulting string.
|
|
109
109
|
def to_s
|
|
110
110
|
if @closing_tag
|
|
111
|
-
"#{start_location}: #{@opening_tag} was not closed!"
|
|
112
|
-
else
|
|
113
111
|
"#{start_location}: #{@opening_tag} was closed by #{@closing_tag}!"
|
|
112
|
+
else
|
|
113
|
+
"#{start_location}: #{@opening_tag} was not closed!"
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
end
|
|
@@ -16,7 +16,6 @@ require_relative "tags"
|
|
|
16
16
|
|
|
17
17
|
require "xrb/template"
|
|
18
18
|
require "concurrent/map"
|
|
19
|
-
require "traces/provider"
|
|
20
19
|
|
|
21
20
|
module Utopia
|
|
22
21
|
module Content
|
|
@@ -66,11 +65,7 @@ module Utopia
|
|
|
66
65
|
end
|
|
67
66
|
|
|
68
67
|
attr :root
|
|
69
|
-
|
|
70
|
-
# TODO we should remove this method and expose `@links` directly.
|
|
71
|
-
def links(path, **options)
|
|
72
|
-
@links.index(path, **options)
|
|
73
|
-
end
|
|
68
|
+
attr :links
|
|
74
69
|
|
|
75
70
|
# Load and cache a content template.
|
|
76
71
|
# @parameter path [Utopia::Path | String] The path.
|
|
@@ -159,12 +154,7 @@ module Utopia
|
|
|
159
154
|
private
|
|
160
155
|
|
|
161
156
|
def lookup_content(name, parent_path)
|
|
162
|
-
if String === name && name.index("/")
|
|
163
|
-
name = Path.create(name)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
157
|
if Path === name
|
|
167
|
-
name = parent_path + name
|
|
168
158
|
name_path = name.components.dup
|
|
169
159
|
name_path[-1] += XNODE_EXTENSION
|
|
170
160
|
else
|
|
@@ -195,6 +185,21 @@ module Utopia
|
|
|
195
185
|
end
|
|
196
186
|
|
|
197
187
|
def content_tag(name, node, parent_path: node.parent_path)
|
|
188
|
+
# Preserve nested names while searching the physical content hierarchy:
|
|
189
|
+
if String === name
|
|
190
|
+
if name.index("/")
|
|
191
|
+
name = Path.create(name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if Path === name
|
|
196
|
+
cache_key = parent_path + name
|
|
197
|
+
|
|
198
|
+
return @node_cache.fetch_or_store(cache_key) do
|
|
199
|
+
lookup_content(name, parent_path)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
198
203
|
full_path = parent_path + name
|
|
199
204
|
|
|
200
205
|
name = full_path.pop
|
|
@@ -216,16 +221,5 @@ module Utopia
|
|
|
216
221
|
end
|
|
217
222
|
end
|
|
218
223
|
|
|
219
|
-
Traces::Provider(Middleware) do
|
|
220
|
-
def respond(link, request, localization: request.localization)
|
|
221
|
-
attributes = {
|
|
222
|
-
"link.key" => link.key,
|
|
223
|
-
"link.href" => link.href,
|
|
224
|
-
"link.locale" => localization&.locale,
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
Traces.trace("utopia.content.middleware.respond", attributes: attributes){super}
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
224
|
end
|
|
231
225
|
end
|
data/lib/utopia/content/node.rb
CHANGED
|
@@ -51,14 +51,12 @@ module Utopia
|
|
|
51
51
|
def local_path(path = ".", base = nil)
|
|
52
52
|
path = Path[path]
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if path.absolute?
|
|
57
|
-
return root.join(*path.components)
|
|
58
|
-
else
|
|
54
|
+
if path.relative?
|
|
59
55
|
base ||= uri_path.dirname
|
|
60
|
-
|
|
56
|
+
path = base + path
|
|
61
57
|
end
|
|
58
|
+
|
|
59
|
+
return Pathname.new(path.to_url_path.local_path(@controller.root))
|
|
62
60
|
end
|
|
63
61
|
|
|
64
62
|
# Resolve a path relative to this node's containing URI path.
|
|
@@ -84,7 +82,7 @@ module Utopia
|
|
|
84
82
|
def links(path = ".", **options, &block)
|
|
85
83
|
path = uri_path.dirname + Path[path]
|
|
86
84
|
|
|
87
|
-
links = @controller.links(path, **options)
|
|
85
|
+
links = @controller.links.index(path, **options)
|
|
88
86
|
|
|
89
87
|
if block_given?
|
|
90
88
|
links.each(&block)
|
|
@@ -96,7 +94,7 @@ module Utopia
|
|
|
96
94
|
# Return localized and indexed variants related to this node.
|
|
97
95
|
# @returns [Array(Link)] The related links.
|
|
98
96
|
def related_links
|
|
99
|
-
@controller.links(@uri_path.dirname, name: @uri_path.basename, indices: true)
|
|
97
|
+
@controller.links.index(@uri_path.dirname, name: @uri_path.basename, indices: true)
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
# Return the directory whose links are siblings of this node.
|
|
@@ -113,7 +111,7 @@ module Utopia
|
|
|
113
111
|
# @parameter options [Hash] The options.
|
|
114
112
|
# @returns [Array(Link)] The sibling links.
|
|
115
113
|
def sibling_links(**options)
|
|
116
|
-
return @controller.links(siblings_path, **options)
|
|
114
|
+
return @controller.links.index(siblings_path, **options)
|
|
117
115
|
end
|
|
118
116
|
|
|
119
117
|
# Lookup the given tag which is being rendered within the given node. Invoked by {Document}.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2015-
|
|
4
|
+
# Copyright, 2015-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "../http"
|
|
7
7
|
require_relative "../path/matcher"
|
|
@@ -48,6 +48,8 @@ module Utopia
|
|
|
48
48
|
# Freeze this object and its internal state.
|
|
49
49
|
# @returns [self] This object.
|
|
50
50
|
def freeze
|
|
51
|
+
return self if frozen?
|
|
52
|
+
|
|
51
53
|
@matcher.freeze
|
|
52
54
|
@block.freeze
|
|
53
55
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Utopia
|
|
7
|
+
module Exceptions
|
|
8
|
+
# Exceptions raised by application code which can be safely handled and reported.
|
|
9
|
+
APPLICATION_ERRORS = [StandardError, ScriptError].freeze
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2014-
|
|
4
|
+
# Copyright, 2014-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2025, by Olle Jonsson.
|
|
6
6
|
|
|
7
7
|
require "console"
|
|
@@ -9,6 +9,7 @@ require "console"
|
|
|
9
9
|
require_relative "../middleware"
|
|
10
10
|
require_relative "../request"
|
|
11
11
|
require_relative "../response"
|
|
12
|
+
require_relative "application_errors"
|
|
12
13
|
|
|
13
14
|
module Utopia
|
|
14
15
|
module Exceptions
|
|
@@ -37,7 +38,7 @@ module Utopia
|
|
|
37
38
|
def call(request)
|
|
38
39
|
begin
|
|
39
40
|
return @delegate.call(request)
|
|
40
|
-
rescue
|
|
41
|
+
rescue *APPLICATION_ERRORS => exception
|
|
41
42
|
Console.warn(self, "An error occurred while processing the request.", error: exception)
|
|
42
43
|
|
|
43
44
|
begin
|
|
@@ -52,7 +53,7 @@ module Utopia
|
|
|
52
53
|
error_response.status = 500
|
|
53
54
|
|
|
54
55
|
return error_response
|
|
55
|
-
rescue
|
|
56
|
+
rescue *APPLICATION_ERRORS => exception
|
|
56
57
|
# If redirection fails, we also finish with a fatal error:
|
|
57
58
|
Console.error(self, "An error occurred while invoking the error handler.", error: exception)
|
|
58
59
|
return Response[500, {"content-type" => "text/plain"}, ["An error occurred while processing the request."]]
|