utopia 2.32.0 → 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 +0 -0
- 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 -165
- 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 +33 -4
- data/releases.md +15 -0
- 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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Utopia
|
|
7
|
+
module Redirection
|
|
8
|
+
# An error handler fails to redirect to a valid page.
|
|
9
|
+
class RequestFailure < StandardError
|
|
10
|
+
# Describe a failed attempt to render an error document.
|
|
11
|
+
# @parameter resource_path [Object] The resource path.
|
|
12
|
+
# @parameter resource_status [Object] The resource status.
|
|
13
|
+
# @parameter error_path [Object] The error path.
|
|
14
|
+
# @parameter error_status [Object] The error status.
|
|
15
|
+
def initialize(resource_path, resource_status, error_path, error_status)
|
|
16
|
+
@resource_path = resource_path
|
|
17
|
+
@resource_status = resource_status
|
|
18
|
+
|
|
19
|
+
@error_path = error_path
|
|
20
|
+
@error_status = error_status
|
|
21
|
+
|
|
22
|
+
super "Requested resource #{@resource_path} resulted in a #{@resource_status} error. Requested error handler #{@error_path} resulted in a #{@error_status} error."
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "client_redirect"
|
|
7
|
+
|
|
8
|
+
module Utopia
|
|
9
|
+
module Redirection
|
|
10
|
+
# Rewrite requests that match the given pattern to a single destination.
|
|
11
|
+
class Rewrite < ClientRedirect
|
|
12
|
+
# Initialize exact-path redirections.
|
|
13
|
+
# @parameter app [Interface(:call)] The downstream application.
|
|
14
|
+
# @parameter patterns [Hash] The path rewrite patterns.
|
|
15
|
+
# @parameter status [Integer] The status.
|
|
16
|
+
def initialize(app, patterns, status: 301)
|
|
17
|
+
@patterns = patterns
|
|
18
|
+
|
|
19
|
+
super(app, status: status)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Freeze this object and its internal state.
|
|
23
|
+
# @returns [self] This object.
|
|
24
|
+
def freeze
|
|
25
|
+
return self if frozen?
|
|
26
|
+
|
|
27
|
+
@patterns.freeze
|
|
28
|
+
|
|
29
|
+
return super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Redirect a path found in the rewrite map.
|
|
33
|
+
# @parameter path [String] The normalized request path.
|
|
34
|
+
# @returns [Protocol::HTTP::Response | Nil] The redirect response when the path is mapped.
|
|
35
|
+
def [] path
|
|
36
|
+
if location = @patterns[path]
|
|
37
|
+
return redirect(location)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/utopia/redirection.rb
CHANGED
|
@@ -1,169 +1,11 @@
|
|
|
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
|
-
require_relative "
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class RequestFailure < StandardError
|
|
13
|
-
def initialize(resource_path, resource_status, error_path, error_status)
|
|
14
|
-
@resource_path = resource_path
|
|
15
|
-
@resource_status = resource_status
|
|
16
|
-
|
|
17
|
-
@error_path = error_path
|
|
18
|
-
@error_status = error_status
|
|
19
|
-
|
|
20
|
-
super "Requested resource #{@resource_path} resulted in a #{@resource_status} error. Requested error handler #{@error_path} resulted in a #{@error_status} error."
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# A middleware which performs internal redirects based on error status codes.
|
|
25
|
-
class Errors
|
|
26
|
-
# @param codes [Hash<Integer,String>] The redirection path for a given error code.
|
|
27
|
-
def initialize(app, codes = {})
|
|
28
|
-
@app = app
|
|
29
|
-
@codes = codes
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def freeze
|
|
33
|
-
return self if frozen?
|
|
34
|
-
|
|
35
|
-
@codes.freeze
|
|
36
|
-
|
|
37
|
-
super
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def unhandled_error?(response)
|
|
41
|
-
response[0] >= 400 && response[1].empty?
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def call(env)
|
|
45
|
-
response = @app.call(env)
|
|
46
|
-
|
|
47
|
-
if unhandled_error?(response) && location = @codes[response[0]]
|
|
48
|
-
error_request = env.merge(Rack::PATH_INFO => location, Rack::REQUEST_METHOD => Rack::GET)
|
|
49
|
-
error_response = @app.call(error_request)
|
|
50
|
-
|
|
51
|
-
if error_response[0] >= 400
|
|
52
|
-
raise RequestFailure.new(env[Rack::PATH_INFO], response[0], location, error_response[0])
|
|
53
|
-
else
|
|
54
|
-
# Feed the error code back with the error document:
|
|
55
|
-
error_response[0] = response[0]
|
|
56
|
-
return error_response
|
|
57
|
-
end
|
|
58
|
-
else
|
|
59
|
-
return response
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# We cache 301 redirects for 24 hours.
|
|
65
|
-
DEFAULT_MAX_AGE = 3600*24
|
|
66
|
-
|
|
67
|
-
# A basic client-side redirect.
|
|
68
|
-
class ClientRedirect
|
|
69
|
-
def initialize(app, status: 307, max_age: DEFAULT_MAX_AGE)
|
|
70
|
-
@app = app
|
|
71
|
-
@status = status
|
|
72
|
-
@max_age = max_age
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def freeze
|
|
76
|
-
return self if frozen?
|
|
77
|
-
|
|
78
|
-
@status.freeze
|
|
79
|
-
@max_age.freeze
|
|
80
|
-
|
|
81
|
-
super
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
attr :status
|
|
85
|
-
attr :max_age
|
|
86
|
-
|
|
87
|
-
def cache_control
|
|
88
|
-
# http://jacquesmattheij.com/301-redirects-a-dangerous-one-way-street
|
|
89
|
-
"max-age=#{self.max_age}"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def headers(location)
|
|
93
|
-
{HTTP::LOCATION => location, HTTP::CACHE_CONTROL => self.cache_control}
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def redirect(location)
|
|
97
|
-
return [self.status, self.headers(location), []]
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def [] path
|
|
101
|
-
false
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def call(env)
|
|
105
|
-
path = env[Rack::PATH_INFO]
|
|
106
|
-
|
|
107
|
-
if redirection = self[path]
|
|
108
|
-
return redirection
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
return @app.call(env)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Redirect urls that end with a `/`, e.g. directories.
|
|
116
|
-
class DirectoryIndex < ClientRedirect
|
|
117
|
-
def initialize(app, index: "index")
|
|
118
|
-
@app = app
|
|
119
|
-
@index = index
|
|
120
|
-
|
|
121
|
-
super(app)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def [] path
|
|
125
|
-
if path.end_with?("/")
|
|
126
|
-
return redirect(path + @index)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Rewrite requests that match the given pattern to a single destination.
|
|
132
|
-
class Rewrite < ClientRedirect
|
|
133
|
-
def initialize(app, patterns, status: 301)
|
|
134
|
-
@patterns = patterns
|
|
135
|
-
|
|
136
|
-
super(app, status: status)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def [] path
|
|
140
|
-
if location = @patterns[path]
|
|
141
|
-
return redirect(location)
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Rewrite requests that match the given pattern to a new prefix.
|
|
147
|
-
class Moved < ClientRedirect
|
|
148
|
-
def initialize(app, pattern, prefix, status: 301, flatten: false)
|
|
149
|
-
@app = app
|
|
150
|
-
|
|
151
|
-
@pattern = pattern
|
|
152
|
-
@prefix = prefix
|
|
153
|
-
@flatten = flatten
|
|
154
|
-
|
|
155
|
-
super(app, status: status)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def [] path
|
|
159
|
-
if path.start_with?(@pattern)
|
|
160
|
-
if @flatten
|
|
161
|
-
return redirect(@prefix)
|
|
162
|
-
else
|
|
163
|
-
return redirect(path.sub(@pattern, @prefix))
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
6
|
+
require_relative "redirection/request_failure"
|
|
7
|
+
require_relative "redirection/errors"
|
|
8
|
+
require_relative "redirection/client_redirect"
|
|
9
|
+
require_relative "redirection/directory_index"
|
|
10
|
+
require_relative "redirection/rewrite"
|
|
11
|
+
require_relative "redirection/moved"
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "stringio"
|
|
7
|
+
require "protocol/http/request"
|
|
8
|
+
require "protocol/url"
|
|
9
|
+
require "protocol/url/form_data/parser"
|
|
10
|
+
|
|
11
|
+
require_relative "path"
|
|
12
|
+
|
|
13
|
+
module Utopia
|
|
14
|
+
# Utopia's application-facing request wrapper.
|
|
15
|
+
#
|
|
16
|
+
# Protocol request methods are delegated to the underlying request; parsing
|
|
17
|
+
# and application conveniences live here rather than on protocol-http itself.
|
|
18
|
+
class Request
|
|
19
|
+
# Build a Utopia request from the given protocol request arguments.
|
|
20
|
+
def self.[](*arguments)
|
|
21
|
+
self.new(Protocol::HTTP::Request[*arguments])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Initialize the request.
|
|
25
|
+
# @parameter delegate [Protocol::HTTP::Request] The underlying protocol request.
|
|
26
|
+
def initialize(delegate)
|
|
27
|
+
@delegate = delegate
|
|
28
|
+
|
|
29
|
+
@url = nil
|
|
30
|
+
@session = nil
|
|
31
|
+
@variables = nil
|
|
32
|
+
@localization = nil
|
|
33
|
+
@exception = nil
|
|
34
|
+
|
|
35
|
+
@query_parameters = nil
|
|
36
|
+
@cookies = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The underlying protocol request.
|
|
40
|
+
attr :delegate
|
|
41
|
+
|
|
42
|
+
# The session associated with this request, if installed.
|
|
43
|
+
attr_accessor :session
|
|
44
|
+
|
|
45
|
+
# The controller variables associated with this request, if installed.
|
|
46
|
+
attr_accessor :variables
|
|
47
|
+
|
|
48
|
+
# The immutable localization preferences associated with this request, if installed.
|
|
49
|
+
attr_accessor :localization
|
|
50
|
+
|
|
51
|
+
# The exception associated with this request, if any.
|
|
52
|
+
attr_accessor :exception
|
|
53
|
+
|
|
54
|
+
private def parse_url
|
|
55
|
+
path, query = @delegate.path.split("?", 2)
|
|
56
|
+
|
|
57
|
+
if scheme = @delegate.scheme and authority = @delegate.authority
|
|
58
|
+
Protocol::URL::Absolute.new(scheme, authority, path, query)
|
|
59
|
+
else
|
|
60
|
+
Protocol::URL::Relative.new(path, query)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The structured request URL.
|
|
65
|
+
# @returns [Protocol::URL::Absolute | Protocol::URL::Relative | Nil] The request URL.
|
|
66
|
+
def url
|
|
67
|
+
@url ||= self.parse_url.normalize!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @returns [Utopia::Path] The path components.
|
|
71
|
+
def path
|
|
72
|
+
Path.new(self.url.path.components(Protocol::URL::Encoding::System))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Assign the decoded application path while preserving the current query string.
|
|
76
|
+
# @parameter value [Utopia::Path | Protocol::URL::Path | String] The path.
|
|
77
|
+
def path= value
|
|
78
|
+
if value.is_a?(Protocol::URL::Path)
|
|
79
|
+
self.url.path = value
|
|
80
|
+
else
|
|
81
|
+
self.url.path = Path[value].to_url_path
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Whether the request method is QUERY.
|
|
86
|
+
# @returns [Boolean] Whether the request method is QUERY.
|
|
87
|
+
def query?
|
|
88
|
+
@delegate.method == "QUERY"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Whether the request method is POST.
|
|
92
|
+
def post?
|
|
93
|
+
@delegate.method == "POST"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The normalized original request path, before any internal request rewrites.
|
|
97
|
+
# @returns [Utopia::Path] The decoded application path.
|
|
98
|
+
def request_path
|
|
99
|
+
path = @delegate.path.split("?", 2).first
|
|
100
|
+
path = Protocol::URL::Path[path].normalize.simplify
|
|
101
|
+
|
|
102
|
+
return Path.new(path.components(Protocol::URL::Encoding::System))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
QUERY_PARSER = Protocol::URL::FormData::Parser.new
|
|
106
|
+
|
|
107
|
+
private def parse_query_parameters(query)
|
|
108
|
+
if query
|
|
109
|
+
return QUERY_PARSER.parse(StringIO.new(query))
|
|
110
|
+
else
|
|
111
|
+
Hash.new
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Decoded query arguments.
|
|
116
|
+
def query_parameters
|
|
117
|
+
@query_parameters ||= parse_query_parameters(self.url.query)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private def parse_cookies(cookie_header)
|
|
121
|
+
cookies = {}
|
|
122
|
+
|
|
123
|
+
return cookies unless cookie_header
|
|
124
|
+
|
|
125
|
+
if cookie_header.respond_to?(:to_str)
|
|
126
|
+
cookie_header = cookie_header.to_str
|
|
127
|
+
else
|
|
128
|
+
cookie_header = cookie_header.to_s
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
cookie_header.split(/;\s*/).each do |pair|
|
|
132
|
+
key, value = pair.split("=", 2)
|
|
133
|
+
cookies[key] = value || ""
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
return cookies
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Decoded request cookies.
|
|
140
|
+
def cookies
|
|
141
|
+
@cookies ||= parse_cookies(self.headers["cookie"])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The request user agent.
|
|
145
|
+
def user_agent
|
|
146
|
+
@delegate.headers["user-agent"]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The request referrer.
|
|
150
|
+
def referrer
|
|
151
|
+
@delegate.headers["referer"]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The remote peer IP address, if available.
|
|
155
|
+
def ip
|
|
156
|
+
@delegate.peer&.ip_address
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Assign the structured request URL.
|
|
160
|
+
# @parameter url [Protocol::URL::Absolute | Protocol::URL::Relative | String | Nil] The request URL.
|
|
161
|
+
def url= url
|
|
162
|
+
@url = Protocol::URL[url]
|
|
163
|
+
@query_parameters = nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Build a derived request with updated protocol fields.
|
|
167
|
+
def with(method: nil, path: self.path)
|
|
168
|
+
delegate = @delegate
|
|
169
|
+
|
|
170
|
+
if method
|
|
171
|
+
delegate = @delegate.dup
|
|
172
|
+
delegate.method = method
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
request = self.class.new(delegate)
|
|
176
|
+
request.session = @session
|
|
177
|
+
request.variables = @variables
|
|
178
|
+
request.localization = @localization
|
|
179
|
+
request.exception = @exception
|
|
180
|
+
request.path = path
|
|
181
|
+
|
|
182
|
+
return request
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# These inherited methods conflict with the protocol request interface, so remove them to allow delegation.
|
|
188
|
+
undef_method :method, :to_s
|
|
189
|
+
|
|
190
|
+
def method_missing(name, ...)
|
|
191
|
+
if @delegate.respond_to?(name)
|
|
192
|
+
@delegate.public_send(name, ...)
|
|
193
|
+
else
|
|
194
|
+
super
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def respond_to_missing?(name, include_private = false)
|
|
199
|
+
@delegate.respond_to?(name) || super(name, include_private)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http/response"
|
|
7
|
+
require "protocol/http/middleware"
|
|
8
|
+
|
|
9
|
+
module Utopia
|
|
10
|
+
# Response helpers for Utopia applications.
|
|
11
|
+
#
|
|
12
|
+
# The canonical transport response remains {Protocol::HTTP::Response}. This
|
|
13
|
+
# module provides convenience constructors and normalization at the application
|
|
14
|
+
# boundary.
|
|
15
|
+
module Response
|
|
16
|
+
CONTENT_TYPE = "content-type".freeze
|
|
17
|
+
LOCATION = "location".freeze
|
|
18
|
+
|
|
19
|
+
NotFound = Protocol::HTTP::Middleware::NotFound
|
|
20
|
+
|
|
21
|
+
# Build a protocol HTTP response.
|
|
22
|
+
# @parameter status [Integer] The HTTP status code.
|
|
23
|
+
# @parameter headers [Hash | Protocol::HTTP::Headers | Nil] The response headers.
|
|
24
|
+
# @parameter body [Object] The response body.
|
|
25
|
+
# @parameter options [Hash] Additional options passed to `Protocol::HTTP::Response[]`.
|
|
26
|
+
# @returns [Protocol::HTTP::Response] The response object.
|
|
27
|
+
def self.[](status, headers = nil, body = nil, **options)
|
|
28
|
+
Protocol::HTTP::Response[status, headers, body, **options]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Normalize a response-like value to a protocol response.
|
|
32
|
+
# @parameter response [Object] The response-like value.
|
|
33
|
+
# @returns [Protocol::HTTP::Response] The normalized response.
|
|
34
|
+
# @raises [TypeError] If the response cannot be normalized.
|
|
35
|
+
def self.wrap(response)
|
|
36
|
+
return response if response.is_a?(Protocol::HTTP::Response)
|
|
37
|
+
|
|
38
|
+
if response.respond_to?(:to_response)
|
|
39
|
+
response = response.to_response
|
|
40
|
+
return response if response.is_a?(Protocol::HTTP::Response)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
raise TypeError, "Expected a Protocol::HTTP::Response, but got #{response.class}!"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build a redirect response.
|
|
47
|
+
# @parameter location [String] The redirect location.
|
|
48
|
+
# @parameter status [Integer] The redirect status code.
|
|
49
|
+
# @parameter headers [Hash] Additional response headers.
|
|
50
|
+
# @returns [Protocol::HTTP::Response] The redirect response.
|
|
51
|
+
def self.redirect(location, status = 302, headers = {})
|
|
52
|
+
self[status, headers.merge(LOCATION => location), []]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Build a plain text response.
|
|
56
|
+
# @parameter content [String] The response content.
|
|
57
|
+
# @parameter status [Integer] The response status.
|
|
58
|
+
# @parameter headers [Hash] Additional response headers.
|
|
59
|
+
# @returns [Protocol::HTTP::Response] The text response.
|
|
60
|
+
def self.text(content, status = 200, headers = {})
|
|
61
|
+
self[status, {CONTENT_TYPE => "text/plain; charset=utf-8"}.merge(headers), [content]]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build an HTML response.
|
|
65
|
+
# @parameter content [String] The response content.
|
|
66
|
+
# @parameter status [Integer] The response status.
|
|
67
|
+
# @parameter headers [Hash] Additional response headers.
|
|
68
|
+
# @returns [Protocol::HTTP::Response] The HTML response.
|
|
69
|
+
def self.html(content, status = 200, headers = {})
|
|
70
|
+
self[status, {CONTENT_TYPE => "text/html; charset=utf-8"}.merge(headers), [content]]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -1,12 +1,14 @@
|
|
|
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
|
|
|
6
6
|
module Utopia
|
|
7
7
|
module Session
|
|
8
8
|
# A simple hash table which fetches it's values only when required.
|
|
9
9
|
class LazyHash
|
|
10
|
+
# Initialize a lazily loaded hash.
|
|
11
|
+
# @yields The block which loads the initial values.
|
|
10
12
|
def initialize(&block)
|
|
11
13
|
@changed = false
|
|
12
14
|
@values = nil
|
|
@@ -14,12 +16,21 @@ module Utopia
|
|
|
14
16
|
@loader = block
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
# The loaded session values, if already loaded.
|
|
20
|
+
# @returns [Hash | Nil] The loaded values.
|
|
17
21
|
attr :values
|
|
18
22
|
|
|
23
|
+
# Fetch a value by key, loading the hash if necessary.
|
|
24
|
+
# @parameter key [Object] The key.
|
|
25
|
+
# @returns [Object | Nil] The value.
|
|
19
26
|
def [] key
|
|
20
27
|
load![key]
|
|
21
28
|
end
|
|
22
29
|
|
|
30
|
+
# Store a value by key.
|
|
31
|
+
# @parameter key [Object] The key.
|
|
32
|
+
# @parameter value [Object] The value.
|
|
33
|
+
# @returns [Object] The stored value.
|
|
23
34
|
def []= key, value
|
|
24
35
|
values = load!
|
|
25
36
|
|
|
@@ -31,10 +42,16 @@ module Utopia
|
|
|
31
42
|
return value
|
|
32
43
|
end
|
|
33
44
|
|
|
45
|
+
# Check whether the hash contains a key.
|
|
46
|
+
# @parameter key [Object] The key.
|
|
47
|
+
# @returns [Boolean] Whether the key exists.
|
|
34
48
|
def include?(key)
|
|
35
49
|
load!.include?(key)
|
|
36
50
|
end
|
|
37
51
|
|
|
52
|
+
# Delete a value by key.
|
|
53
|
+
# @parameter key [Object] The key.
|
|
54
|
+
# @returns [Object | Nil] The deleted value.
|
|
38
55
|
def delete(key)
|
|
39
56
|
load!
|
|
40
57
|
|
|
@@ -43,18 +60,27 @@ module Utopia
|
|
|
43
60
|
@values.delete(key)
|
|
44
61
|
end
|
|
45
62
|
|
|
63
|
+
# Check whether any value has changed.
|
|
64
|
+
# @returns [Boolean] Whether the hash has changed.
|
|
46
65
|
def changed?
|
|
47
66
|
@changed
|
|
48
67
|
end
|
|
49
68
|
|
|
69
|
+
# Load and return the underlying values.
|
|
70
|
+
# @returns [Hash] The loaded values.
|
|
50
71
|
def load!
|
|
51
72
|
@values ||= @loader.call
|
|
52
73
|
end
|
|
53
74
|
|
|
75
|
+
# Check whether the underlying values have been loaded.
|
|
76
|
+
# @returns [Boolean] Whether the values are loaded.
|
|
54
77
|
def loaded?
|
|
55
78
|
!@values.nil?
|
|
56
79
|
end
|
|
57
80
|
|
|
81
|
+
# Check whether the values should be persisted.
|
|
82
|
+
# @parameter timeout [Numeric | Nil] The maximum age before an update is required.
|
|
83
|
+
# @returns [Boolean] Whether an update is required.
|
|
58
84
|
def needs_update?(timeout = nil)
|
|
59
85
|
# If data has changed, we need update:
|
|
60
86
|
return true if @changed
|