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
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2009-2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "../middleware"
7
- require_relative "../localization"
7
+ require_relative "../localization/resolver"
8
+ require_relative "../request"
9
+ require_relative "../response"
10
+ require_relative "../controller/variables"
8
11
 
9
12
  require_relative "links"
10
13
  require_relative "node"
@@ -18,7 +21,9 @@ require "traces/provider"
18
21
  module Utopia
19
22
  module Content
20
23
  # A middleware which serves dynamically generated content based on markup files.
21
- class Middleware
24
+ class Middleware < Protocol::HTTP::Middleware
25
+ include Localization::Resolver
26
+
22
27
  CONTENT_NAMESPACE = "content".freeze
23
28
  UTOPIA_NAMESPACE = "utopia".freeze
24
29
  CONTENT_TAG_NAME = "utopia:content".freeze
@@ -26,7 +31,8 @@ module Utopia
26
31
  # @param root [String] The content root where pages will be generated from.
27
32
  # @param namespaces [Hash<String,Library>] Tag namespaces for dynamic tag lookup.
28
33
  def initialize(app, root: Utopia::default_root, namespaces: {})
29
- @app = app
34
+ super(app)
35
+
30
36
  @root = root
31
37
 
32
38
  @template_cache = Concurrent::Map.new
@@ -43,6 +49,8 @@ module Utopia
43
49
  @namespaces[UTOPIA_NAMESPACE] ||= Tags
44
50
  end
45
51
 
52
+ # Freeze this object and its internal state.
53
+ # @returns [self] This object.
46
54
  def freeze
47
55
  return self if frozen?
48
56
 
@@ -60,6 +68,9 @@ module Utopia
60
68
  @links.index(path, **options)
61
69
  end
62
70
 
71
+ # Load and cache a content template.
72
+ # @parameter path [Utopia::Path | String] The path.
73
+ # @returns [XRB::Template] The parsed template.
63
74
  def fetch_template(path)
64
75
  @template_cache.fetch_or_store(path.to_s) do
65
76
  XRB::Template.load_file(path)
@@ -82,6 +93,9 @@ module Utopia
82
93
  )
83
94
  end
84
95
 
96
+ # Resolve a link to an existing content node.
97
+ # @parameter link [Utopia::Content::Link] The content link.
98
+ # @returns [Node | Nil] The content node when its backing file exists.
85
99
  def resolve_link(link)
86
100
  if full_path = link&.full_path(@root)
87
101
  if File.exist?(full_path)
@@ -90,19 +104,26 @@ module Utopia
90
104
  end
91
105
  end
92
106
 
93
- def respond(link, request)
107
+ # Respond.
108
+ # @parameter link [Utopia::Content::Link] The content link.
109
+ # @parameter request [Utopia::Request] The application request.
110
+ # @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
111
+ # @returns [Protocol::HTTP::Response] The response.
112
+ def respond(link, request, localization: request.localization)
94
113
  if node = resolve_link(link)
95
- attributes = request.env.fetch(VARIABLES_KEY, {}).to_hash
114
+ attributes = request.variables&.to_hash || {}
96
115
 
97
- return node.process!(request, attributes)
116
+ return node.process!(request, attributes, localization: localization)
98
117
  elsif redirect_uri = link[:uri]
99
- return [307, {HTTP::LOCATION => redirect_uri}, []]
118
+ return Utopia::Response[307, {HTTP::LOCATION => redirect_uri}, []]
100
119
  end
101
120
  end
102
121
 
103
- def call(env)
104
- request = Rack::Request.new(env)
105
- path = Path.create(request.path_info)
122
+ # Serve or redirect filesystem-backed content, otherwise invoke the application.
123
+ # @parameter request [Utopia::Request] The request.
124
+ # @returns [Protocol::HTTP::Response] The content, redirect, or downstream response.
125
+ def call(request)
126
+ path = request.path
106
127
 
107
128
  # Check if the request is to a non-specific index. This only works for requests with a given name:
108
129
  basename = path.basename
@@ -111,18 +132,24 @@ module Utopia
111
132
  # If the request for /foo/bar is actually a directory, rewrite it to /foo/bar/index:
112
133
  if File.directory? directory_path
113
134
  index_path = [basename, INDEX]
135
+ location = path.dirname.join(index_path).to_url_path.encoded
114
136
 
115
- return [307, {HTTP::LOCATION => path.dirname.join(index_path).to_s}, []]
137
+ return Utopia::Response[307, {HTTP::LOCATION => location}, []]
116
138
  end
117
139
 
118
- locale = env[Localization::CURRENT_LOCALE_KEY]
119
- if link = @links.for(path, locale)
120
- if response = self.respond(link, request)
121
- return response
140
+ response = resolve_localized(request) do |localization|
141
+ locale = localization&.locale
142
+
143
+ if link = @links.for(path, locale, fallback: false)
144
+ self.respond(link, request, localization: localization)
122
145
  end
123
146
  end
124
147
 
125
- return @app.call(env)
148
+ if response
149
+ return response
150
+ end
151
+
152
+ return @delegate.call(request)
126
153
  end
127
154
 
128
155
  private
@@ -182,13 +209,14 @@ module Utopia
182
209
  end
183
210
 
184
211
  Traces::Provider(Middleware) do
185
- def respond(link, request)
212
+ def respond(link, request, localization: request.localization)
186
213
  attributes = {
187
214
  "link.key" => link.key,
188
- "link.href" => link.href
215
+ "link.href" => link.href,
216
+ "link.locale" => localization&.locale,
189
217
  }
190
218
 
191
- Traces.trace("utopia.content.middleware.respond", attributes: attributes) {super}
219
+ Traces.trace("utopia.content.middleware.respond", attributes: attributes){super}
192
220
  end
193
221
  end
194
222
  end
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2022, by Samuel Williams.
4
+ # Copyright, 2017-2026, by Samuel Williams.
5
5
 
6
6
  module Utopia
7
7
  module Content
8
8
  # A namespace which contains tags which can be rendered within a {Document}.
9
9
  module Namespace
10
+ # Initialize tag mappings on an extended namespace.
11
+ # @parameter other [Module] The namespace being extended.
12
+ # @returns [Hash] The initialized tag mappings.
10
13
  def self.extended(other)
11
14
  other.class_exec do
12
15
  @named = {}
@@ -15,6 +18,8 @@ module Utopia
15
18
 
16
19
  attr :named
17
20
 
21
+ # Freeze this object and its internal state.
22
+ # @returns [self] This object.
18
23
  def freeze
19
24
  return self if frozen?
20
25
 
@@ -24,6 +29,10 @@ module Utopia
24
29
  super
25
30
  end
26
31
 
32
+ # Tag.
33
+ # @parameter name [String] The name.
34
+ # @parameter klass [Class] The class to configure.
35
+ # @returns [Class | Proc] The registered tag implementation.
27
36
  def tag(name, klass = nil, &block)
28
37
  @named[name] = klass || block
29
38
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2009-2025, by Samuel Williams.
4
+ # Copyright, 2009-2026, by Samuel Williams.
5
5
  # Copyright, 2015, by Huba Nagy.
6
6
 
7
7
  require_relative "markup"
@@ -15,6 +15,11 @@ module Utopia
15
15
  module Content
16
16
  # Represents an immutable node within the content hierarchy.
17
17
  class Node
18
+ # Initialize a node within the filesystem-backed content hierarchy.
19
+ # @parameter controller [Utopia::Controller::Base] The controller instance.
20
+ # @parameter uri_path [Utopia::Path | String] The uri path.
21
+ # @parameter request_path [Utopia::Path | String] The request path.
22
+ # @parameter file_path [String] The filesystem path.
18
23
  def initialize(controller, uri_path, request_path, file_path)
19
24
  @controller = controller
20
25
 
@@ -27,14 +32,22 @@ module Utopia
27
32
  attr :uri_path
28
33
  attr :file_path
29
34
 
35
+ # Return the node's URI basename.
36
+ # @returns [String] The node name.
30
37
  def name
31
38
  @uri_path.basename
32
39
  end
33
40
 
41
+ # Resolve another node relative to this node's parent.
42
+ # @parameter path [Utopia::Path | String] The path.
43
+ # @returns [Node | Nil] The resolved node.
34
44
  def lookup_node(path)
35
45
  @controller.lookup_node(parent_path + Path[path])
36
46
  end
37
47
 
48
+ # Resolve a content path beneath the controller root.
49
+ # @parameter path [Utopia::Path | String] The path.
50
+ # @returns [Pathname] The local filesystem path.
38
51
  def local_path(path = ".", base = nil)
39
52
  path = Path[path]
40
53
 
@@ -48,6 +61,9 @@ module Utopia
48
61
  end
49
62
  end
50
63
 
64
+ # Resolve a path relative to this node's containing URI path.
65
+ # @parameter path [Utopia::Path | String] The path.
66
+ # @returns [Path] The resolved content path.
51
67
  def relative_path(path = ".")
52
68
  path = Path[path]
53
69
  base = uri_path.dirname
@@ -55,10 +71,16 @@ module Utopia
55
71
  return base + path
56
72
  end
57
73
 
74
+ # Return this node's containing URI path.
75
+ # @returns [Path] The parent URI path.
58
76
  def parent_path
59
77
  @uri_path.dirname
60
78
  end
61
79
 
80
+ # Enumerate or return links relative to this node.
81
+ # @parameter path [Utopia::Path | String] The path.
82
+ # @yields {|link| ...} Each matching link when a block is given.
83
+ # @returns [Array(Link)] The matching links.
62
84
  def links(path = ".", **options, &block)
63
85
  path = uri_path.dirname + Path[path]
64
86
 
@@ -71,10 +93,14 @@ module Utopia
71
93
  end
72
94
  end
73
95
 
96
+ # Return localized and indexed variants related to this node.
97
+ # @returns [Array(Link)] The related links.
74
98
  def related_links
75
99
  @controller.links(@uri_path.dirname, name: @uri_path.basename, indices: true)
76
100
  end
77
101
 
102
+ # Return the directory whose links are siblings of this node.
103
+ # @returns [Path] The sibling directory path.
78
104
  def siblings_path
79
105
  if @uri_path.basename == INDEX
80
106
  @uri_path.dirname(2)
@@ -83,6 +109,9 @@ module Utopia
83
109
  end
84
110
  end
85
111
 
112
+ # Return links that are siblings of this node.
113
+ # @parameter options [Hash] The options.
114
+ # @returns [Array(Link)] The sibling links.
86
115
  def sibling_links(**options)
87
116
  return @controller.links(siblings_path, **options)
88
117
  end
@@ -106,12 +135,21 @@ module Utopia
106
135
  document.parse_markup(markup)
107
136
  end
108
137
 
109
- def process!(request, attributes = {})
110
- Document.render(self, request, attributes).to_a
138
+ # Process the request and return the resulting response.
139
+ # @parameter request [Utopia::Request] The application request.
140
+ # @parameter attributes [Hash] The attributes.
141
+ # @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
142
+ # @returns [Protocol::HTTP::Response] The response.
143
+ def process!(request, attributes = {}, localization: request&.localization)
144
+ Document.render(self, request, attributes, localization: localization).to_response
111
145
  end
112
146
 
113
147
  # This is a special context in which a limited set of well defined methods are exposed in the content view.
114
148
  Context = Struct.new(:document, :state) do
149
+ # Render or defer a partial content block.
150
+ # @parameter arguments [Array] The arguments.
151
+ # @yields {|document| ...} Deferred content, when a block is given.
152
+ # @returns [String] The deferred-content marker.
115
153
  def partial(*arguments, &block)
116
154
  if block_given?
117
155
  state.defer(&block)
@@ -124,44 +162,68 @@ module Utopia
124
162
 
125
163
  alias deferred_tag partial
126
164
 
165
+ # Return the controller associated with the document.
166
+ # @returns [Controller::Variables | Nil] The current controller variables.
127
167
  def controller
128
168
  document.controller
129
169
  end
130
170
 
171
+ # Return the document's localization preferences.
172
+ # @returns [Localization::Preferences | Nil] The localization preferences.
131
173
  def localization
132
174
  document.localization
133
175
  end
134
176
 
177
+ # Return the application request being rendered.
178
+ # @returns [Utopia::Request] The request.
135
179
  def request
136
180
  document.request
137
181
  end
138
182
 
183
+ # Return the document response being rendered.
184
+ # @returns [Document] The document response.
139
185
  def response
140
186
  document
141
187
  end
142
188
 
189
+ # Return attributes for the current rendering state.
190
+ # @returns [Hash] The current attributes.
143
191
  def attributes
144
192
  state.attributes
145
193
  end
146
194
 
195
+ # Fetch an attribute from the current state or document defaults.
196
+ # @parameter key [String | Symbol] The lookup key.
197
+ # @returns [Object | Nil] The state attribute, falling back to the document attribute.
147
198
  def [] key
148
- state.attributes.fetch(key) {document.attributes[key]}
199
+ state.attributes.fetch(key){document.attributes[key]}
149
200
  end
150
201
 
151
202
  alias current state
152
203
 
204
+ # Return the captured content of the current node.
205
+ # @returns [String] The captured content.
153
206
  def content
154
207
  document.content
155
208
  end
156
209
 
210
+ # Return the enclosing rendering state.
211
+ # @returns [Builder | Nil] The parent rendering state.
157
212
  def parent
158
213
  document.parent
159
214
  end
160
215
 
216
+ # Return the first rendering state in the document.
217
+ # @returns [Builder | Nil] The first rendering state.
161
218
  def first
162
219
  document.first
163
220
  end
164
221
 
222
+ # Return or enumerate links relative to the current node.
223
+ # @parameter arguments [Array] The arguments.
224
+ # @parameter options [Hash] The options.
225
+ # @yields {|link| ...} Each matching link when a block is given.
226
+ # @returns [Array(Link)] The matching links.
165
227
  def links(*arguments, **options, &block)
166
228
  state.node.links(*arguments, **options, &block)
167
229
  end
@@ -3,9 +3,10 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2010-2025, by Samuel Williams.
5
5
 
6
+ require_relative "../response"
7
+
6
8
  module Utopia
7
9
  module Content
8
- # Compatibility with older versions of rack:
9
10
  EXPIRES = "expires".freeze
10
11
  CACHE_CONTROL = "cache-control".freeze
11
12
  CONTENT_TYPE = "content-type".freeze
@@ -13,6 +14,7 @@ module Utopia
13
14
 
14
15
  # A basic content response, including useful defaults for typical HTML5 content.
15
16
  class Response
17
+ # Initialize an empty successful HTML response.
16
18
  def initialize
17
19
  @status = 200
18
20
  @headers = {}
@@ -26,16 +28,23 @@ module Utopia
26
28
  attr :headers
27
29
  attr :body
28
30
 
31
+ # Join the response body into rendered content.
32
+ # @returns [String] The rendered content.
29
33
  def content
30
34
  @body.join
31
35
  end
32
36
 
37
+ # Decline tag lookup by default.
38
+ # @parameter tag [Object] The tag.
39
+ # @returns [Nil] No tag is resolved.
33
40
  def lookup(tag)
34
41
  return nil
35
42
  end
36
43
 
37
- def to_a
38
- [@status, @headers, @body]
44
+ # Convert this value to a protocol HTTP response.
45
+ # @returns [Protocol::HTTP::Response] The response.
46
+ def to_response
47
+ Utopia::Response[@status, @headers, @body]
39
48
  end
40
49
 
41
50
  # Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
@@ -6,7 +6,10 @@
6
6
  require_relative "content/middleware"
7
7
 
8
8
  module Utopia
9
+ # Builds middleware for serving filesystem-backed dynamic content.
9
10
  module Content
11
+ # Construct content middleware.
12
+ # @returns [Content::Middleware] The content middleware.
10
13
  def self.new(...)
11
14
  Middleware.new(...)
12
15
  end
@@ -25,24 +25,24 @@ on "new" do |request|
25
25
  @user = User.new
26
26
 
27
27
  if request.post?
28
- @user.update_attributes(request.params["user"])
28
+ @user.update_attributes(parse_body(request)["user"])
29
29
 
30
30
  redirect! "index"
31
31
  end
32
32
  end
33
33
 
34
34
  on "edit" do |request|
35
- @user = User.find(request.params["id"])
35
+ @user = User.find(request.query_parameters["id"])
36
36
 
37
37
  if request.post?
38
- @user.update_attributes(request.params["user"])
38
+ @user.update_attributes(parse_body(request)["user"])
39
39
 
40
40
  redirect! "index"
41
41
  end
42
42
  end
43
43
 
44
44
  on "delete" do |request|
45
- User.find(request.params["id"]).destroy
45
+ User.find(request.query_parameters["id"]).destroy
46
46
 
47
47
  redirect! "index"
48
48
  end
@@ -10,15 +10,21 @@ module Utopia
10
10
  # A controller layer which invokes functinality based on the request path.
11
11
  # @example
12
12
  # on '*' do |request, path|
13
- # succeed! content: 'Hello World'
13
+ # succeed! 'Hello World'
14
14
  # end
15
15
  module Actions
16
+ # Extend a controller class with the action-definition DSL.
17
+ # @parameter base [Class] The controller class.
18
+ # @returns [Class] The extended controller class.
16
19
  def self.prepended(base)
17
20
  base.extend(ClassMethods)
18
21
  end
19
22
 
20
23
  # A nested action lookup hash table.
21
24
  class Action < Hash
25
+ # Initialize an action lookup node.
26
+ # @parameter options [Hash] Metadata associated with this action.
27
+ # @yields The action body when this node matches.
22
28
  def initialize(options = {}, &block)
23
29
  @options = options
24
30
  @callback = block
@@ -28,18 +34,28 @@ module Utopia
28
34
 
29
35
  attr_accessor :callback, :options
30
36
 
37
+ # Check whether this action has a callback.
38
+ # @returns [Boolean] Whether this action has a callback.
31
39
  def callback?
32
40
  @callback != nil
33
41
  end
34
42
 
43
+ # Check whether this object is equivalent to another object.
44
+ # @parameter other [Object] The object to compare.
45
+ # @returns [Boolean] Whether the action mappings, callback, and options are equal.
35
46
  def eql? other
36
47
  super and @callback.eql? other.callback and @options.eql? other.options
37
48
  end
38
49
 
50
+ # Compute the hash value for this object.
51
+ # @returns [Integer] The resulting integer.
39
52
  def hash
40
53
  [super, @callback, @options].hash
41
54
  end
42
55
 
56
+ # Compare this object with another object.
57
+ # @parameter other [Object] The object to compare.
58
+ # @returns [Boolean] Whether the action mappings, callback, and options are equal.
43
59
  def == other
44
60
  super and @callback == other.callback and @options == other.options
45
61
  end
@@ -50,8 +66,11 @@ module Utopia
50
66
  # Matches any 1 path component.
51
67
  WILDCARD = "*".freeze
52
68
 
53
- # Given a path, iterate over all actions that match. Actions match from most specific to most general.
54
- # @return nil if nothing matched, or true if something matched.
69
+ # Yield all actions matching a path, from most specific to most general.
70
+ # @parameter path [Array(String)] The path components.
71
+ # @parameter index [Integer] The component index currently being matched.
72
+ # @yields {|action| ...} Each matching action.
73
+ # @returns [Boolean | Nil] `true` if any action matched, otherwise `nil`.
55
74
  def apply(path, index = -1, &block)
56
75
  # ** is greedy, it always matches if possible and matches all remaining input.
57
76
  if match_all = self[WILDCARD_GREEDY] and match_all.callback?
@@ -79,10 +98,18 @@ module Utopia
79
98
  return matched
80
99
  end
81
100
 
101
+ # Collect the actions matching the given path.
102
+ # @parameter path [Utopia::Path | String] The path.
103
+ # @returns [Array(Action)] The matching actions.
82
104
  def matching(path, &block)
83
105
  to_enum(:apply, path).to_a
84
106
  end
85
107
 
108
+ # Define an action at the given path.
109
+ # @parameter path [Array(String)] The path components in reverse matching order.
110
+ # @parameter options [Hash] Metadata associated with the action.
111
+ # @yields The action body.
112
+ # @returns [Action] The defined action.
86
113
  def define(path, **options, &callback)
87
114
  # puts "Defining path: #{path.inspect}"
88
115
  current = self
@@ -97,6 +124,8 @@ module Utopia
97
124
  return current
98
125
  end
99
126
 
127
+ # Generate a debug representation of this object.
128
+ # @returns [String] The resulting string.
100
129
  def inspect
101
130
  if callback?
102
131
  "<action " + super + ":#{callback.source_location}(#{options})>"
@@ -108,6 +137,9 @@ module Utopia
108
137
 
109
138
  # Exposed to the controller class.
110
139
  module ClassMethods
140
+ # Initialize class-level state when this module is extended.
141
+ # @parameter klass [Class] The class to configure.
142
+ # @returns [Class] The configured controller class.
111
143
  def self.extended(klass)
112
144
  klass.instance_eval do
113
145
  @actions = nil
@@ -115,10 +147,18 @@ module Utopia
115
147
  end
116
148
  end
117
149
 
150
+ # Return the root of the action lookup tree.
151
+ # @returns [Action] The root action.
118
152
  def actions
119
153
  @actions ||= Action.new
120
154
  end
121
155
 
156
+ # Define an action for the given request path.
157
+ # @parameter first [Path | String | Symbol | Array] The first path pattern or named suffix.
158
+ # @parameter path [Array(String)] Additional path components.
159
+ # @parameter options [Hash] Metadata associated with the action.
160
+ # @yields The action body.
161
+ # @returns [Action] The defined action.
122
162
  def on(first, *path, **options, &block)
123
163
  if first.is_a? Symbol
124
164
  first = ["**", first.to_s]
@@ -127,10 +167,18 @@ module Utopia
127
167
  actions.define(Path.split(first) + path, **options, &block)
128
168
  end
129
169
 
170
+ # Define the fallback action.
171
+ # @yields The fallback action body.
172
+ # @returns [Proc] The fallback action body.
130
173
  def otherwise(&block)
131
174
  @otherwise = block
132
175
  end
133
176
 
177
+ # Dispatch the request to the first matching action.
178
+ # @parameter controller [Utopia::Controller::Base] The controller instance.
179
+ # @parameter request [Utopia::Request] The request.
180
+ # @parameter path [Utopia::Path | String] The path.
181
+ # @returns [Object | Nil] The result of the final matching action or fallback action.
134
182
  def dispatch(controller, request, path)
135
183
  if @actions
136
184
  matched = @actions.apply(path.components) do |action|