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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/utopia/environment.rb +6 -6
  4. data/bake/utopia/server.rb +4 -1
  5. data/bake/utopia/site.rb +14 -14
  6. data/bake/utopia/static.rb +26 -13
  7. data/bake/utopia.rb +1 -1
  8. data/context/getting-started.md +7 -5
  9. data/context/index.yaml +1 -2
  10. data/context/middleware.md +42 -13
  11. data/lib/utopia/application.rb +83 -0
  12. data/lib/utopia/content/builder.rb +25 -0
  13. data/lib/utopia/content/document.rb +64 -6
  14. data/lib/utopia/content/link.rb +50 -5
  15. data/lib/utopia/content/links.rb +44 -7
  16. data/lib/utopia/content/markup.rb +74 -0
  17. data/lib/utopia/content/middleware.rb +48 -20
  18. data/lib/utopia/content/namespace.rb +10 -1
  19. data/lib/utopia/content/node.rb +66 -4
  20. data/lib/utopia/content/response.rb +12 -3
  21. data/lib/utopia/content.rb +3 -0
  22. data/lib/utopia/controller/actions.md +4 -4
  23. data/lib/utopia/controller/actions.rb +51 -3
  24. data/lib/utopia/controller/base.rb +72 -29
  25. data/lib/utopia/controller/middleware.rb +20 -15
  26. data/lib/utopia/controller/respond.rb +36 -32
  27. data/lib/utopia/controller/responder.rb +106 -45
  28. data/lib/utopia/controller/result.rb +11 -0
  29. data/lib/utopia/controller/rewrite.rb +39 -0
  30. data/lib/utopia/controller/variables.rb +15 -3
  31. data/lib/utopia/controller.rb +2 -0
  32. data/lib/utopia/exceptions/handler.rb +20 -11
  33. data/lib/utopia/exceptions/mailer.rb +56 -51
  34. data/lib/utopia/extensions/array_split.rb +6 -0
  35. data/lib/utopia/extensions/date_comparisons.rb +6 -0
  36. data/lib/utopia/http.rb +11 -48
  37. data/lib/utopia/import_map.rb +19 -11
  38. data/lib/utopia/localization/locales.rb +60 -0
  39. data/lib/utopia/localization/middleware.rb +107 -73
  40. data/lib/utopia/localization/preferences.rb +76 -0
  41. data/lib/utopia/localization/resolver.rb +47 -0
  42. data/lib/utopia/localization.rb +6 -0
  43. data/lib/utopia/middleware.rb +3 -4
  44. data/lib/utopia/path/matcher.rb +15 -0
  45. data/lib/utopia/path.rb +146 -10
  46. data/lib/utopia/redirection/client_redirect.rb +87 -0
  47. data/lib/utopia/redirection/directory_index.rb +41 -0
  48. data/lib/utopia/redirection/errors.rb +78 -0
  49. data/lib/utopia/redirection/moved.rb +52 -0
  50. data/lib/utopia/redirection/request_failure.rb +26 -0
  51. data/lib/utopia/redirection/rewrite.rb +42 -0
  52. data/lib/utopia/redirection.rb +7 -165
  53. data/lib/utopia/request.rb +202 -0
  54. data/lib/utopia/response.rb +73 -0
  55. data/lib/utopia/session/lazy_hash.rb +27 -1
  56. data/lib/utopia/session/middleware.rb +103 -30
  57. data/lib/utopia/session/serialization.rb +8 -0
  58. data/lib/utopia/session.rb +5 -0
  59. data/lib/utopia/setup.rb +23 -3
  60. data/lib/utopia/shell.rb +28 -7
  61. data/lib/utopia/static/local_file.rb +84 -61
  62. data/lib/utopia/static/middleware.rb +65 -33
  63. data/lib/utopia/static/mime_types.rb +38 -29
  64. data/lib/utopia/static.rb +2 -0
  65. data/lib/utopia/version.rb +3 -2
  66. data/lib/utopia.rb +1 -1
  67. data/license.md +1 -1
  68. data/readme.md +33 -4
  69. data/releases.md +15 -0
  70. data/setup/site/bake.rb +1 -1
  71. data/setup/site/config/application.rb +51 -0
  72. data/setup/site/config/serve.rb +8 -0
  73. data/setup/site/falcon.rb +17 -4
  74. data/setup/site/fixtures/website.rb +27 -11
  75. data/setup/site/gems.rb +1 -3
  76. data/setup/site/lib/readme.txt +1 -1
  77. data/setup/site/pages/welcome/index.xnode +3 -3
  78. data/setup/site/readme.md +0 -3
  79. data/setup/site/test/website.rb +2 -2
  80. data.tar.gz.sig +0 -0
  81. metadata +60 -34
  82. metadata.gz.sig +0 -0
  83. data/lib/utopia/localization/wrapper.rb +0 -52
  84. data/setup/site/Guardfile +0 -12
  85. 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.env[VARIABLES_KEY]
80
+ request.variables
69
81
  end
70
82
  end
71
83
  end
@@ -7,6 +7,8 @@ require_relative "controller/middleware"
7
7
 
8
8
  module Utopia
9
9
  module Controller
10
+ # Construct controller middleware.
11
+ # @returns [Controller::Middleware] The controller middleware.
10
12
  def self.new(...)
11
13
  Middleware.new(...)
12
14
  end
@@ -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
- @app = app
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
- def call(env)
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 @app.call(env)
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 = env.merge(
37
- Rack::PATH_INFO => @location,
38
- Rack::REQUEST_METHOD => Rack::GET,
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 = @app.call(error_request)
43
- error_response[0] = 500
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 all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
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 `env` as `environment.yaml` to the error report.
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
- @app = app
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
- def call(env)
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 @app.call(env)
61
+ return @delegate.call(request)
53
62
  rescue => exception
54
- send_notification exception, env
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
- REQUEST_KEYS = [
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, env)
99
+ def generate_body(exception, request)
99
100
  io = StringIO.new
100
101
 
101
- # Dump out useful rack environment variables:
102
- request = Rack::Request.new(env)
102
+ io.puts "#{request.method} #{request.url}"
103
103
 
104
- io.puts "#{request.request_method} #{request.url}"
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
- REQUEST_KEYS.each do |key|
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.params.each do |key, value|
117
- io.puts "request.params.#{key}: #{value.inspect}"
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
- ENV_KEYS.each do |key|
123
- value = env[key]
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
- io.puts
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, env)
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, env)
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, env)
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, env)
151
+ mail.text_part.body = generate_body(exception, request)
157
152
 
158
- if body = extract_body(env) and body.size > 0
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["environment.yaml"] = YAML.dump(env)
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, env)
170
- mail = generate_mail(exception, env)
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 extract_body(env)
181
- if io = env["rack.input"]
182
- io.rewind if io.respond_to?(:rewind)
183
- io.read
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-2025, by Samuel Williams.
4
+ # Copyright, 2010-2026, by Samuel Williams.
5
5
 
6
- require "rack"
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
- STATUS_DESCRIPTIONS[@code] || @code.to_s
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
@@ -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, nil] Optional subresource integrity hash.
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, nil] The base URI for resolving relative paths.
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, Protocol::URL, nil] The base URI for resolving relative paths.
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 | nil] The parsed base URL for efficient resolution.
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, nil] Optional subresource integrity hash for the resource.
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
- relative_base = Protocol::URL::Path.relative(@base.path, path)
266
- resolved_base = Protocol::URL[relative_base]
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, nil] The base URL context for resolving relative paths.
280
- # @returns [Protocol::URL, String] The resolved URL object or original string.
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, nil] The base URL context.
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 = {}