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
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 =>
|
|
137
|
+
return Utopia::Response[307, {HTTP::LOCATION => location}, []]
|
|
116
138
|
end
|
|
117
139
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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)
|
|
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-
|
|
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
|
data/lib/utopia/content/node.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2009-
|
|
4
|
+
# Copyright, 2009-2026, by Samuel Williams.
|
|
5
5
|
# 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
|
-
|
|
110
|
-
|
|
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)
|
|
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
|
-
|
|
38
|
-
|
|
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.
|
data/lib/utopia/content.rb
CHANGED
|
@@ -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
|
|
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.
|
|
35
|
+
@user = User.find(request.query_parameters["id"])
|
|
36
36
|
|
|
37
37
|
if request.post?
|
|
38
|
-
@user.update_attributes(request
|
|
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.
|
|
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!
|
|
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
|
-
#
|
|
54
|
-
# @
|
|
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|
|