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
|
@@ -15,12 +15,19 @@ module Utopia
|
|
|
15
15
|
# @user = User.find(@id)
|
|
16
16
|
# end
|
|
17
17
|
module Rewrite
|
|
18
|
+
# Extend a controller class with path-rewrite rules.
|
|
19
|
+
# @parameter base [Class] The controller class.
|
|
20
|
+
# @returns [Class] The extended controller class.
|
|
18
21
|
def self.prepended(base)
|
|
19
22
|
base.extend(ClassMethods)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
# A abstract rule which can match against a request path.
|
|
23
26
|
class Rule
|
|
27
|
+
# Copy named regular-expression captures into context instance variables.
|
|
28
|
+
# @parameter match_data [MatchData] The regular expression match.
|
|
29
|
+
# @parameter context [Object] The context.
|
|
30
|
+
# @returns [Array(String)] The capture names.
|
|
24
31
|
def apply_match_to_context(match_data, context)
|
|
25
32
|
match_data.names.each do |name|
|
|
26
33
|
context.instance_variable_set("@#{name}", match_data[name])
|
|
@@ -30,11 +37,16 @@ module Utopia
|
|
|
30
37
|
|
|
31
38
|
# A rule which extracts a prefix pattern from the request path.
|
|
32
39
|
class ExtractPrefixRule < Rule
|
|
40
|
+
# Initialize a typed prefix-extraction rule.
|
|
41
|
+
# @parameter patterns [Hash] The path rewrite patterns.
|
|
42
|
+
# @parameter block [Proc] The block.
|
|
33
43
|
def initialize(patterns, block)
|
|
34
44
|
@matcher = Path::Matcher.new(patterns)
|
|
35
45
|
@block = block
|
|
36
46
|
end
|
|
37
47
|
|
|
48
|
+
# Freeze this object and its internal state.
|
|
49
|
+
# @returns [self] This object.
|
|
38
50
|
def freeze
|
|
39
51
|
@matcher.freeze
|
|
40
52
|
@block.freeze
|
|
@@ -42,6 +54,11 @@ module Utopia
|
|
|
42
54
|
super
|
|
43
55
|
end
|
|
44
56
|
|
|
57
|
+
# Apply this prefix rule and execute its callback when it matches.
|
|
58
|
+
# @parameter context [Object] The context.
|
|
59
|
+
# @parameter request [Utopia::Request] The request.
|
|
60
|
+
# @parameter path [Utopia::Path | String] The path.
|
|
61
|
+
# @returns [Path] The unmatched suffix, or the original path when the rule does not match.
|
|
45
62
|
def apply(context, request, path)
|
|
46
63
|
if match_data = @matcher.match(path)
|
|
47
64
|
apply_match_to_context(match_data, context)
|
|
@@ -59,16 +76,26 @@ module Utopia
|
|
|
59
76
|
|
|
60
77
|
# Rewrite a request path based on a set of defined rules.
|
|
61
78
|
class Rewriter
|
|
79
|
+
# Initialize an empty rewrite rule sequence.
|
|
62
80
|
def initialize
|
|
63
81
|
@rules = []
|
|
64
82
|
end
|
|
65
83
|
|
|
66
84
|
attr :rules
|
|
67
85
|
|
|
86
|
+
# Add a rule that extracts a typed prefix from the request path.
|
|
87
|
+
# @parameter patterns [Hash] The path rewrite patterns.
|
|
88
|
+
# @yields {|request, path, match| ...} The request, original path, and match data when the rule matches.
|
|
89
|
+
# @returns [ExtractPrefixRule] The added rule.
|
|
68
90
|
def extract_prefix(**patterns, &block)
|
|
69
91
|
@rules << ExtractPrefixRule.new(patterns, block)
|
|
70
92
|
end
|
|
71
93
|
|
|
94
|
+
# Apply every rewrite rule in order.
|
|
95
|
+
# @parameter context [Object] The context.
|
|
96
|
+
# @parameter request [Utopia::Request] The request.
|
|
97
|
+
# @parameter path [Utopia::Path | String] The path.
|
|
98
|
+
# @returns [Path] The rewritten path.
|
|
72
99
|
def apply(context, request, path)
|
|
73
100
|
@rules.each do |rule|
|
|
74
101
|
path = rule.apply(context, request, path)
|
|
@@ -77,6 +104,11 @@ module Utopia
|
|
|
77
104
|
return path
|
|
78
105
|
end
|
|
79
106
|
|
|
107
|
+
# Rewrite a path's components in place.
|
|
108
|
+
# @parameter context [Object] The context.
|
|
109
|
+
# @parameter request [Utopia::Request] The request.
|
|
110
|
+
# @parameter path [Utopia::Path | String] The path.
|
|
111
|
+
# @returns [Array(String)] The rewritten components.
|
|
80
112
|
def call(context, request, path)
|
|
81
113
|
path.components = apply(context, request, path).components
|
|
82
114
|
end
|
|
@@ -84,10 +116,17 @@ module Utopia
|
|
|
84
116
|
|
|
85
117
|
# Exposed to the controller class.
|
|
86
118
|
module ClassMethods
|
|
119
|
+
# Return this controller's path rewriter.
|
|
120
|
+
# @returns [Rewriter] The path rewriter.
|
|
87
121
|
def rewrite
|
|
88
122
|
@rewriter ||= Rewriter.new
|
|
89
123
|
end
|
|
90
124
|
|
|
125
|
+
# Apply configured rewrite rules to the request path.
|
|
126
|
+
# @parameter controller [Utopia::Controller::Base] The controller instance.
|
|
127
|
+
# @parameter request [Utopia::Request] The request.
|
|
128
|
+
# @parameter path [Utopia::Path | String] The path.
|
|
129
|
+
# @returns [Array(String) | Nil] The rewritten components, or `nil` when no rewriter is configured.
|
|
91
130
|
def rewrite_request(controller, request, path)
|
|
92
131
|
if @rewriter
|
|
93
132
|
@rewriter.call(controller, request, path)
|
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2014-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative "../middleware"
|
|
7
|
-
|
|
8
6
|
module Utopia
|
|
9
7
|
module Controller
|
|
10
8
|
# Provides a stack-based instance variable lookup mechanism. It can flatten a stack of controllers into a single hash.
|
|
11
9
|
class Variables
|
|
10
|
+
# Initialize an empty controller stack.
|
|
12
11
|
def initialize
|
|
13
12
|
@controllers = []
|
|
14
13
|
end
|
|
15
14
|
|
|
15
|
+
# Return the innermost controller.
|
|
16
|
+
# @returns [Controller::Base | Nil] The current controller.
|
|
16
17
|
def top
|
|
17
18
|
@controllers.last
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
# Push a controller after copying variables from the previous controller.
|
|
22
|
+
# @parameter controller [Utopia::Controller::Base] The controller instance.
|
|
23
|
+
# @returns [self] This variables stack.
|
|
20
24
|
def << controller
|
|
21
25
|
if top = self.top
|
|
22
26
|
# This ensures that most variables will be at the top and controllers can naturally interactive with instance variables:
|
|
@@ -45,6 +49,8 @@ module Utopia
|
|
|
45
49
|
end
|
|
46
50
|
end
|
|
47
51
|
|
|
52
|
+
# Convert the current controller's instance variables to attributes.
|
|
53
|
+
# @returns [Hash(Symbol, Object)] The current controller attributes.
|
|
48
54
|
def to_hash
|
|
49
55
|
attributes = {}
|
|
50
56
|
|
|
@@ -59,13 +65,19 @@ module Utopia
|
|
|
59
65
|
return attributes
|
|
60
66
|
end
|
|
61
67
|
|
|
68
|
+
# Fetch a variable from the innermost controller.
|
|
69
|
+
# @parameter key [String | Symbol] The lookup key.
|
|
70
|
+
# @returns [Object | Nil] The variable value, or `nil` when it is undefined.
|
|
62
71
|
def [] key
|
|
63
72
|
fetch("@#{key}".to_sym, nil)
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
|
|
76
|
+
# Return the controller variables associated with the request.
|
|
77
|
+
# @parameter request [Utopia::Request] The application request.
|
|
78
|
+
# @returns [Variables | Nil] The current variables.
|
|
67
79
|
def self.[] request
|
|
68
|
-
request.
|
|
80
|
+
request.variables
|
|
69
81
|
end
|
|
70
82
|
end
|
|
71
83
|
end
|
data/lib/utopia/controller.rb
CHANGED
|
@@ -6,17 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
require "console"
|
|
8
8
|
|
|
9
|
+
require_relative "../middleware"
|
|
10
|
+
require_relative "../request"
|
|
11
|
+
require_relative "../response"
|
|
12
|
+
|
|
9
13
|
module Utopia
|
|
10
14
|
module Exceptions
|
|
11
15
|
# A middleware which catches exceptions and performs an internal redirect.
|
|
12
|
-
class Handler
|
|
16
|
+
class Handler < Protocol::HTTP::Middleware
|
|
13
17
|
# @param location [String] Peform an internal redirect to this location when an exception is raised.
|
|
14
18
|
def initialize(app, location = "/errors/exception")
|
|
15
|
-
|
|
19
|
+
super(app)
|
|
16
20
|
|
|
17
21
|
@location = location
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# Freeze this object and its internal state.
|
|
25
|
+
# @returns [self] This object.
|
|
20
26
|
def freeze
|
|
21
27
|
return self if frozen?
|
|
22
28
|
|
|
@@ -25,28 +31,31 @@ module Utopia
|
|
|
25
31
|
super
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
# Convert application exceptions into internal-server-error responses.
|
|
35
|
+
# @parameter request [Utopia::Request] The request.
|
|
36
|
+
# @returns [Protocol::HTTP::Response] The application response or a generated error response.
|
|
37
|
+
def call(request)
|
|
29
38
|
begin
|
|
30
|
-
return @
|
|
39
|
+
return @delegate.call(request)
|
|
31
40
|
rescue Exception => exception
|
|
32
41
|
Console.warn(self, "An error occurred while processing the request.", error: exception)
|
|
33
42
|
|
|
34
43
|
begin
|
|
35
44
|
# We do an internal redirection to the error location:
|
|
36
|
-
error_request =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"utopia.exception" => exception,
|
|
45
|
+
error_request = request.with(
|
|
46
|
+
method: "GET",
|
|
47
|
+
path: @location
|
|
40
48
|
)
|
|
49
|
+
error_request.exception = exception
|
|
41
50
|
|
|
42
|
-
error_response = @
|
|
43
|
-
error_response
|
|
51
|
+
error_response = Response.wrap(@delegate.call(error_request))
|
|
52
|
+
error_response.status = 500
|
|
44
53
|
|
|
45
54
|
return error_response
|
|
46
55
|
rescue Exception => exception
|
|
47
56
|
# If redirection fails, we also finish with a fatal error:
|
|
48
57
|
Console.error(self, "An error occurred while invoking the error handler.", error: exception)
|
|
49
|
-
return [500, {"content-type" => "text/plain"}, ["An error occurred while processing the request."]]
|
|
58
|
+
return Response[500, {"content-type" => "text/plain"}, ["An error occurred while processing the request."]]
|
|
50
59
|
end
|
|
51
60
|
end
|
|
52
61
|
end
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
require "net/smtp"
|
|
7
7
|
require "mail"
|
|
8
8
|
|
|
9
|
+
require_relative "../middleware"
|
|
10
|
+
require_relative "../request"
|
|
11
|
+
require_relative "handler"
|
|
12
|
+
|
|
9
13
|
module Utopia
|
|
10
14
|
module Exceptions
|
|
11
|
-
# A middleware which catches
|
|
12
|
-
class Mailer
|
|
15
|
+
# A middleware which catches application exceptions and sends an email containing the exception, backtrace, request, and application state.
|
|
16
|
+
class Mailer < Protocol::HTTP::Middleware
|
|
13
17
|
# A basic local non-authenticated SMTP server.
|
|
14
18
|
LOCAL_SMTP = [:smtp, {
|
|
15
19
|
:address => "localhost",
|
|
@@ -24,9 +28,9 @@ module Utopia
|
|
|
24
28
|
# @param from [String] The from address for error reports.
|
|
25
29
|
# @param subject [String] The subject template which can access attributes defined by `#attributes_for`.
|
|
26
30
|
# @param delivery_method [Object] The delivery method as required by the mail gem.
|
|
27
|
-
# @param dump_environment [Boolean] Attach
|
|
31
|
+
# @param dump_environment [Boolean] Attach request attributes as `attributes.yaml` to the error report.
|
|
28
32
|
def initialize(app, to: "postmaster", from: DEFAULT_FROM, subject: DEFAULT_SUBJECT, delivery_method: LOCAL_SMTP, dump_environment: false)
|
|
29
|
-
|
|
33
|
+
super(app)
|
|
30
34
|
|
|
31
35
|
@to = to
|
|
32
36
|
@from = from
|
|
@@ -35,6 +39,8 @@ module Utopia
|
|
|
35
39
|
@dump_environment = dump_environment
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
# Freeze this object and its internal state.
|
|
43
|
+
# @returns [self] This object.
|
|
38
44
|
def freeze
|
|
39
45
|
return self if frozen?
|
|
40
46
|
|
|
@@ -47,11 +53,15 @@ module Utopia
|
|
|
47
53
|
super
|
|
48
54
|
end
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
# Report application exceptions by email before returning an error response.
|
|
57
|
+
# @parameter request [Utopia::Request] The request.
|
|
58
|
+
# @returns [Protocol::HTTP::Response] The application response or a generated error response.
|
|
59
|
+
def call(request)
|
|
51
60
|
begin
|
|
52
|
-
return @
|
|
61
|
+
return @delegate.call(request)
|
|
53
62
|
rescue => exception
|
|
54
|
-
|
|
63
|
+
request.exception = exception
|
|
64
|
+
send_notification exception, request
|
|
55
65
|
|
|
56
66
|
raise
|
|
57
67
|
end
|
|
@@ -59,28 +69,19 @@ module Utopia
|
|
|
59
69
|
|
|
60
70
|
private
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
REQUEST_ATTRIBUTES = [
|
|
73
|
+
:method,
|
|
74
|
+
:scheme,
|
|
75
|
+
:authority,
|
|
76
|
+
:protocol,
|
|
77
|
+
:version,
|
|
63
78
|
:ip,
|
|
64
79
|
:referrer,
|
|
65
80
|
:path,
|
|
81
|
+
:request_path,
|
|
66
82
|
:user_agent,
|
|
67
83
|
]
|
|
68
84
|
|
|
69
|
-
ENV_KEYS = [
|
|
70
|
-
"PATH_INFO",
|
|
71
|
-
"REQUEST_METHOD",
|
|
72
|
-
"REQUEST_PATH",
|
|
73
|
-
"REQUEST_URI",
|
|
74
|
-
"SCRIPT_NAME",
|
|
75
|
-
"QUERY_STRING",
|
|
76
|
-
"SERVER_PROTOCOL",
|
|
77
|
-
"SERVER_NAME",
|
|
78
|
-
"SERVER_PORT",
|
|
79
|
-
"REMOTE_ADDR",
|
|
80
|
-
"CONTENT_TYPE",
|
|
81
|
-
"CONTENT_LENGTH",
|
|
82
|
-
]
|
|
83
|
-
|
|
84
85
|
def generate_backtrace(io, exception, prefix: "Exception")
|
|
85
86
|
io.puts "#{prefix} #{exception.class.name}: #{exception.to_s}"
|
|
86
87
|
|
|
@@ -95,39 +96,33 @@ module Utopia
|
|
|
95
96
|
end
|
|
96
97
|
end
|
|
97
98
|
|
|
98
|
-
def generate_body(exception,
|
|
99
|
+
def generate_body(exception, request)
|
|
99
100
|
io = StringIO.new
|
|
100
101
|
|
|
101
|
-
#
|
|
102
|
-
request = Rack::Request.new(env)
|
|
102
|
+
io.puts "#{request.method} #{request.url}"
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# TODO embed `rack.input` if it's textual?
|
|
104
|
+
# TODO embed the request body if it's textual?
|
|
107
105
|
# TODO dump and embed `utopia.variables`?
|
|
108
106
|
|
|
109
107
|
io.puts
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
REQUEST_ATTRIBUTES.each do |key|
|
|
112
110
|
value = request.send(key)
|
|
113
111
|
io.puts "request.#{key}: #{value.inspect}"
|
|
114
112
|
end
|
|
115
113
|
|
|
116
|
-
request.
|
|
117
|
-
io.puts "request.
|
|
114
|
+
request.query_parameters.each do |key, value|
|
|
115
|
+
io.puts "request.query_parameters.#{key}: #{value.inspect}"
|
|
118
116
|
end
|
|
119
117
|
|
|
120
118
|
io.puts
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
io.puts "env[#{key.inspect}]: #{value.inspect}"
|
|
120
|
+
request.headers.each do |key, value|
|
|
121
|
+
io.puts "header[#{key.inspect}]: #{value.inspect}"
|
|
125
122
|
end
|
|
126
123
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
env.select{|key,_| key.start_with? "HTTP_"}.each do |key, value|
|
|
130
|
-
io.puts "#{key}: #{value.inspect}"
|
|
124
|
+
self.current_state(request).each do |key, value|
|
|
125
|
+
io.puts "state.#{key}: #{value.inspect}"
|
|
131
126
|
end
|
|
132
127
|
|
|
133
128
|
io.puts
|
|
@@ -137,7 +132,7 @@ module Utopia
|
|
|
137
132
|
return io.string
|
|
138
133
|
end
|
|
139
134
|
|
|
140
|
-
def attributes_for(exception,
|
|
135
|
+
def attributes_for(exception, request)
|
|
141
136
|
{
|
|
142
137
|
exception: exception.class.name,
|
|
143
138
|
pid: $$,
|
|
@@ -145,29 +140,29 @@ module Utopia
|
|
|
145
140
|
}
|
|
146
141
|
end
|
|
147
142
|
|
|
148
|
-
def generate_mail(exception,
|
|
143
|
+
def generate_mail(exception, request)
|
|
149
144
|
mail = Mail.new(
|
|
150
145
|
:from => @from,
|
|
151
146
|
:to => @to,
|
|
152
|
-
:subject => @subject % attributes_for(exception,
|
|
147
|
+
:subject => @subject % attributes_for(exception, request)
|
|
153
148
|
)
|
|
154
149
|
|
|
155
150
|
mail.text_part = Mail::Part.new
|
|
156
|
-
mail.text_part.body = generate_body(exception,
|
|
151
|
+
mail.text_part.body = generate_body(exception, request)
|
|
157
152
|
|
|
158
|
-
if body = extract_body(
|
|
153
|
+
if body = extract_body(request) and body.size > 0
|
|
159
154
|
mail.attachments["body.bin"] = body
|
|
160
155
|
end
|
|
161
156
|
|
|
162
157
|
if @dump_environment
|
|
163
|
-
mail.attachments["
|
|
158
|
+
mail.attachments["state.yaml"] = YAML.dump(self.current_state(request))
|
|
164
159
|
end
|
|
165
160
|
|
|
166
161
|
return mail
|
|
167
162
|
end
|
|
168
163
|
|
|
169
|
-
def send_notification(exception,
|
|
170
|
-
mail = generate_mail(exception,
|
|
164
|
+
def send_notification(exception, request)
|
|
165
|
+
mail = generate_mail(exception, request)
|
|
171
166
|
|
|
172
167
|
mail.delivery_method(*@delivery_method) if @delivery_method
|
|
173
168
|
|
|
@@ -177,10 +172,20 @@ module Utopia
|
|
|
177
172
|
$stderr.puts mail_exception.backtrace
|
|
178
173
|
end
|
|
179
174
|
|
|
180
|
-
def
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
175
|
+
def current_state(request)
|
|
176
|
+
{
|
|
177
|
+
session: request.session,
|
|
178
|
+
variables: request.variables,
|
|
179
|
+
localization: request.localization,
|
|
180
|
+
exception: request.exception,
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def extract_body(request)
|
|
185
|
+
body = request.body
|
|
186
|
+
|
|
187
|
+
if body&.rewindable? && body.rewind
|
|
188
|
+
return body.join
|
|
184
189
|
end
|
|
185
190
|
end
|
|
186
191
|
end
|
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
# Copyright, 2009-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Utopia
|
|
7
|
+
# @namespace
|
|
7
8
|
module Extensions
|
|
9
|
+
# Adds splitting helpers to arrays.
|
|
8
10
|
module ArraySplit
|
|
11
|
+
# Split the array around the first element matching the arguments or block.
|
|
12
|
+
# @parameter arguments [Array] The arguments accepted by {Array#index}.
|
|
13
|
+
# @yields {|element| ...} Each element until the block matches.
|
|
14
|
+
# @returns [Array(Array, Object, Array)] The elements before the match, the matching element, and the elements after it; the middle value is `nil` when no element matches.
|
|
9
15
|
def split_at(*arguments, &block)
|
|
10
16
|
if middle = index(*arguments, &block)
|
|
11
17
|
[self[0...middle], self[middle], self[middle+1..-1]]
|
|
@@ -9,6 +9,9 @@ module Utopia
|
|
|
9
9
|
module Extensions
|
|
10
10
|
# Provides comparison operator extensions.
|
|
11
11
|
module TimeDateComparison
|
|
12
|
+
# Compare a time with a date-like or otherwise comparable value.
|
|
13
|
+
# @parameter other [Object] The object to compare.
|
|
14
|
+
# @returns [Integer | Nil] The comparison result.
|
|
12
15
|
def <=>(other)
|
|
13
16
|
if Date === other or DateTime === other
|
|
14
17
|
self.to_datetime <=> other
|
|
@@ -22,6 +25,9 @@ module Utopia
|
|
|
22
25
|
|
|
23
26
|
# Provides comparison operator extensions.
|
|
24
27
|
module DateTimeComparison
|
|
28
|
+
# Compare a date with a time or otherwise comparable value.
|
|
29
|
+
# @parameter other [Object] The object to compare.
|
|
30
|
+
# @returns [Integer | Nil] The comparison result.
|
|
25
31
|
def <=>(other)
|
|
26
32
|
if Time === other
|
|
27
33
|
self.to_datetime <=> other.to_datetime
|
data/lib/utopia/http.rb
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2010-
|
|
4
|
+
# Copyright, 2010-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require "
|
|
7
|
-
|
|
8
|
-
require "http/accept"
|
|
6
|
+
require "protocol/http/status"
|
|
9
7
|
|
|
10
8
|
module Utopia
|
|
11
9
|
# HTTP protocol implementation.
|
|
12
10
|
module HTTP
|
|
13
|
-
# Pull in {::HTTP::Accept} for parsing.
|
|
14
|
-
Accept = ::HTTP::Accept
|
|
15
|
-
|
|
16
11
|
# A list of commonly used HTTP status codes.
|
|
17
12
|
# For help choosing the right status code, see http://racksburg.com/choosing-an-http-status-code/
|
|
18
13
|
STATUS_CODES = {
|
|
@@ -38,47 +33,16 @@ module Utopia
|
|
|
38
33
|
:unavailable => 503
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
# A list of human readable descriptions for a given status code.
|
|
42
|
-
# For a more detailed description, see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
|
|
43
|
-
STATUS_DESCRIPTIONS = {
|
|
44
|
-
200 => "OK".freeze,
|
|
45
|
-
201 => "Created".freeze,
|
|
46
|
-
202 => "Accepted".freeze,
|
|
47
|
-
203 => "Non-Authoritive Information".freeze,
|
|
48
|
-
204 => "No Content".freeze,
|
|
49
|
-
205 => "Reset Content".freeze,
|
|
50
|
-
206 => "Partial Content".freeze,
|
|
51
|
-
300 => "Multiple Choices".freeze,
|
|
52
|
-
301 => "Moved Permanently".freeze,
|
|
53
|
-
302 => "Found".freeze,
|
|
54
|
-
303 => "See Other".freeze,
|
|
55
|
-
304 => "Not Modified".freeze,
|
|
56
|
-
305 => "Use Proxy".freeze,
|
|
57
|
-
307 => "Temporary Redirect".freeze,
|
|
58
|
-
308 => "Permanent Redirect".freeze,
|
|
59
|
-
400 => "Bad Request".freeze,
|
|
60
|
-
401 => "Permission Denied".freeze,
|
|
61
|
-
402 => "Payment Required".freeze,
|
|
62
|
-
403 => "Access Forbidden".freeze,
|
|
63
|
-
404 => "Resource Not Found".freeze,
|
|
64
|
-
405 => "Unsupported Method".freeze,
|
|
65
|
-
406 => "Not Acceptable".freeze,
|
|
66
|
-
408 => "Request Timeout".freeze,
|
|
67
|
-
409 => "Request Conflict".freeze,
|
|
68
|
-
410 => "Resource Removed".freeze,
|
|
69
|
-
416 => "Byte range unsatisfiable".freeze,
|
|
70
|
-
422 => "Unprocessible Entity".freeze,
|
|
71
|
-
500 => "Internal Server Error".freeze,
|
|
72
|
-
501 => "Not Implemented".freeze,
|
|
73
|
-
503 => "Service Unavailable".freeze
|
|
74
|
-
}.merge(Rack::Utils::HTTP_STATUS_CODES)
|
|
75
|
-
|
|
76
36
|
CONTENT_TYPE = "content-type".freeze
|
|
77
37
|
LOCATION = "location".freeze
|
|
78
38
|
CACHE_CONTROL = "cache-control".freeze
|
|
79
39
|
|
|
80
40
|
# A small HTTP status wrapper that verifies the status code within a given range.
|
|
81
41
|
class Status
|
|
42
|
+
# Initialize a validated HTTP status.
|
|
43
|
+
# @parameter code [Integer | Symbol] The numeric status or a key from {STATUS_CODES}.
|
|
44
|
+
# @parameter valid_range [Range] The accepted numeric status range.
|
|
45
|
+
# @raises [ArgumentError] If the resolved status is outside `valid_range`.
|
|
82
46
|
def initialize(code, valid_range = 100...600)
|
|
83
47
|
if code.is_a? Symbol
|
|
84
48
|
code = STATUS_CODES[code]
|
|
@@ -91,17 +55,16 @@ module Utopia
|
|
|
91
55
|
@code = code
|
|
92
56
|
end
|
|
93
57
|
|
|
58
|
+
# Convert this value to an integer.
|
|
59
|
+
# @returns [Integer] The numeric status code.
|
|
94
60
|
def to_i
|
|
95
61
|
@code
|
|
96
62
|
end
|
|
97
63
|
|
|
64
|
+
# Convert this object to a string.
|
|
65
|
+
# @returns [String] The resulting string.
|
|
98
66
|
def to_s
|
|
99
|
-
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Allow to be used for rack body:
|
|
103
|
-
def each
|
|
104
|
-
yield to_s
|
|
67
|
+
return Protocol::HTTP::Status.description(@code) || @code.to_s
|
|
105
68
|
end
|
|
106
69
|
end
|
|
107
70
|
end
|
data/lib/utopia/import_map.rb
CHANGED
|
@@ -76,6 +76,11 @@ module Utopia
|
|
|
76
76
|
# end
|
|
77
77
|
# end
|
|
78
78
|
class Builder
|
|
79
|
+
# Configure an import map using a scoped builder.
|
|
80
|
+
# @parameter import_map [Utopia::ImportMap] The import map to render.
|
|
81
|
+
# @parameter options [Hash] The options.
|
|
82
|
+
# @yields {|builder| ...} The builder, when the block accepts an argument; otherwise the block is evaluated in the builder's context.
|
|
83
|
+
# @returns [Builder] The configured builder.
|
|
79
84
|
def self.build(import_map, **options, &block)
|
|
80
85
|
builder = self.new(import_map, **options)
|
|
81
86
|
|
|
@@ -88,6 +93,9 @@ module Utopia
|
|
|
88
93
|
return builder
|
|
89
94
|
end
|
|
90
95
|
|
|
96
|
+
# Initialize a scoped builder for an import map.
|
|
97
|
+
# @parameter import_map [Utopia::ImportMap] The import map to render.
|
|
98
|
+
# @parameter base [Object] The base.
|
|
91
99
|
def initialize(import_map, base: nil)
|
|
92
100
|
@import_map = import_map
|
|
93
101
|
@base = Protocol::URL[base]
|
|
@@ -100,7 +108,7 @@ module Utopia
|
|
|
100
108
|
#
|
|
101
109
|
# @parameter specifier [String] The module specifier (e.g., "react", "@myapp/utils").
|
|
102
110
|
# @parameter value [String] The URL or path to resolve to.
|
|
103
|
-
# @parameter integrity [String
|
|
111
|
+
# @parameter integrity [String | Nil] Optional subresource integrity hash.
|
|
104
112
|
# @returns [Builder] Self for method chaining.
|
|
105
113
|
#
|
|
106
114
|
# @example With base URL.
|
|
@@ -170,7 +178,7 @@ module Utopia
|
|
|
170
178
|
# The builder supports both block parameter and instance_eval styles.
|
|
171
179
|
# The returned import map is frozen to prevent accidental mutation.
|
|
172
180
|
#
|
|
173
|
-
# @parameter base [String
|
|
181
|
+
# @parameter base [String | Nil] The base URI for resolving relative paths.
|
|
174
182
|
# @yields {|builder| ...} If a block is given.
|
|
175
183
|
# @parameter builder [Builder] The import map builder, if the block takes an argument.
|
|
176
184
|
# @returns [ImportMap] A frozen import map instance.
|
|
@@ -199,7 +207,7 @@ module Utopia
|
|
|
199
207
|
# @parameter imports [Hash] The imports mapping.
|
|
200
208
|
# @parameter integrity [Hash] Integrity hashes for imports.
|
|
201
209
|
# @parameter scopes [Hash] Scoped import mappings.
|
|
202
|
-
# @parameter base [String
|
|
210
|
+
# @parameter base [String | Protocol::URL | Nil] The base URI for resolving relative paths.
|
|
203
211
|
def initialize(imports = {}, integrity = {}, scopes = {}, base: nil)
|
|
204
212
|
@imports = imports
|
|
205
213
|
@integrity = integrity
|
|
@@ -216,14 +224,14 @@ module Utopia
|
|
|
216
224
|
# @attribute [Hash(String, Hash)] Scoped import mappings.
|
|
217
225
|
attr :scopes
|
|
218
226
|
|
|
219
|
-
# @attribute [Protocol::URL::Absolute | Protocol::URL::Relative |
|
|
227
|
+
# @attribute [Protocol::URL::Absolute | Protocol::URL::Relative | Nil] The parsed base URL for efficient resolution.
|
|
220
228
|
attr :base
|
|
221
229
|
|
|
222
230
|
# Add an import mapping.
|
|
223
231
|
#
|
|
224
232
|
# @parameter specifier [String] The import specifier (e.g., "react", "@myapp/utils").
|
|
225
233
|
# @parameter value [String] The URL or path to resolve to.
|
|
226
|
-
# @parameter integrity [String
|
|
234
|
+
# @parameter integrity [String | Nil] Optional subresource integrity hash for the resource.
|
|
227
235
|
# @returns [ImportMap] Self for method chaining.
|
|
228
236
|
def import(specifier, value, integrity: nil)
|
|
229
237
|
@imports[specifier] = value
|
|
@@ -249,7 +257,7 @@ module Utopia
|
|
|
249
257
|
# Create a new import map with paths relative to the given page path.
|
|
250
258
|
# This is useful for creating page-specific import maps from a global one.
|
|
251
259
|
#
|
|
252
|
-
# @parameter path [String] The absolute page path to make imports relative to.
|
|
260
|
+
# @parameter path [String | Protocol::URL::Path] The absolute page path to make imports relative to.
|
|
253
261
|
# @returns [ImportMap] A new import map with a relative base.
|
|
254
262
|
#
|
|
255
263
|
# @example Creating page-specific import maps.
|
|
@@ -262,8 +270,8 @@ module Utopia
|
|
|
262
270
|
def relative_to(path)
|
|
263
271
|
if @base
|
|
264
272
|
# Calculate the relative path from the page to the base
|
|
265
|
-
|
|
266
|
-
resolved_base = Protocol::URL
|
|
273
|
+
relative_path = @base.path.relative(path)
|
|
274
|
+
resolved_base = Protocol::URL::Relative.new(relative_path)
|
|
267
275
|
else
|
|
268
276
|
resolved_base = nil
|
|
269
277
|
end
|
|
@@ -276,8 +284,8 @@ module Utopia
|
|
|
276
284
|
# Resolve a single import value considering base context.
|
|
277
285
|
#
|
|
278
286
|
# @parameter value [String] The import URL or path value.
|
|
279
|
-
# @parameter base [Protocol::URL
|
|
280
|
-
# @returns [Protocol::URL
|
|
287
|
+
# @parameter base [Protocol::URL | Nil] The base URL context for resolving relative paths.
|
|
288
|
+
# @returns [Protocol::URL | String] The resolved URL object or original string.
|
|
281
289
|
private def resolve_value(value, base)
|
|
282
290
|
if base
|
|
283
291
|
base + Protocol::URL[value]
|
|
@@ -289,7 +297,7 @@ module Utopia
|
|
|
289
297
|
# Resolve a hash of imports with the given base.
|
|
290
298
|
#
|
|
291
299
|
# @parameter imports [Hash] The imports hash to resolve.
|
|
292
|
-
# @parameter base [Protocol::URL
|
|
300
|
+
# @parameter base [Protocol::URL | Nil] The base URL context.
|
|
293
301
|
# @returns [Hash] The resolved imports with string values.
|
|
294
302
|
private def resolve_imports(imports, base)
|
|
295
303
|
result = {}
|