utopia 2.30.2 → 2.31.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/server.rb +1 -1
- data/bake/utopia/site.rb +3 -3
- data/context/getting-started.md +93 -0
- data/context/index.yaml +32 -0
- data/context/integrating-with-javascript.md +75 -0
- data/context/middleware.md +157 -0
- data/context/server-setup.md +116 -0
- data/context/updating-utopia.md +69 -0
- data/context/what-is-xnode.md +41 -0
- data/lib/utopia/content/document.rb +39 -37
- data/lib/utopia/content/link.rb +1 -2
- data/lib/utopia/content/links.rb +2 -2
- data/lib/utopia/content/markup.rb +10 -10
- data/lib/utopia/content/middleware.rb +195 -0
- data/lib/utopia/content/namespace.rb +1 -1
- data/lib/utopia/content/node.rb +1 -1
- data/lib/utopia/content/response.rb +1 -1
- data/lib/utopia/content/tags.rb +1 -1
- data/lib/utopia/content.rb +4 -186
- data/lib/utopia/controller/actions.md +8 -8
- data/lib/utopia/controller/actions.rb +1 -1
- data/lib/utopia/controller/base.rb +4 -4
- data/lib/utopia/controller/middleware.rb +133 -0
- data/lib/utopia/controller/respond.rb +2 -46
- data/lib/utopia/controller/responder.rb +103 -0
- data/lib/utopia/controller/rewrite.md +2 -2
- data/lib/utopia/controller/rewrite.rb +1 -1
- data/lib/utopia/controller/variables.rb +11 -5
- data/lib/utopia/controller.rb +4 -126
- data/lib/utopia/exceptions/mailer.rb +4 -4
- data/lib/utopia/extensions/array_split.rb +2 -2
- data/lib/utopia/extensions/date_comparisons.rb +3 -3
- data/lib/utopia/import_map.rb +374 -0
- data/lib/utopia/localization/middleware.rb +173 -0
- data/lib/utopia/localization/wrapper.rb +52 -0
- data/lib/utopia/localization.rb +4 -202
- data/lib/utopia/path.rb +26 -11
- data/lib/utopia/redirection.rb +2 -2
- data/lib/utopia/session/lazy_hash.rb +1 -1
- data/lib/utopia/session/middleware.rb +218 -0
- data/lib/utopia/session/serialization.rb +1 -1
- data/lib/utopia/session.rb +4 -205
- data/lib/utopia/static/local_file.rb +19 -19
- data/lib/utopia/static/middleware.rb +120 -0
- data/lib/utopia/static/mime_types.rb +1 -1
- data/lib/utopia/static.rb +4 -108
- data/lib/utopia/version.rb +1 -1
- data/lib/utopia.rb +1 -0
- data/readme.md +7 -0
- data/releases.md +7 -0
- data/setup/site/config.ru +1 -1
- data.tar.gz.sig +0 -0
- metadata +31 -4
- metadata.gz.sig +0 -0
- data/lib/utopia/locale.rb +0 -29
- data/lib/utopia/responder.rb +0 -59
data/lib/utopia/content.rb
CHANGED
|
@@ -3,194 +3,12 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2009-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative "middleware"
|
|
7
|
-
require_relative "localization"
|
|
8
|
-
|
|
9
|
-
require_relative "content/links"
|
|
10
|
-
require_relative "content/node"
|
|
11
|
-
require_relative "content/markup"
|
|
12
|
-
require_relative "content/tags"
|
|
13
|
-
|
|
14
|
-
require "xrb/template"
|
|
15
|
-
|
|
16
|
-
require "concurrent/map"
|
|
17
|
-
|
|
18
|
-
require "traces/provider"
|
|
6
|
+
require_relative "content/middleware"
|
|
19
7
|
|
|
20
8
|
module Utopia
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
UTOPIA_NAMESPACE = "utopia".freeze
|
|
25
|
-
DEFERRED_TAG_NAME = "utopia:deferred".freeze
|
|
26
|
-
CONTENT_TAG_NAME = "utopia:content".freeze
|
|
27
|
-
|
|
28
|
-
# @param root [String] The content root where pages will be generated from.
|
|
29
|
-
# @param namespaces [Hash<String,Library>] Tag namespaces for dynamic tag lookup.
|
|
30
|
-
def initialize(app, root: Utopia::default_root, namespaces: {})
|
|
31
|
-
@app = app
|
|
32
|
-
@root = root
|
|
33
|
-
|
|
34
|
-
@template_cache = Concurrent::Map.new
|
|
35
|
-
@node_cache = Concurrent::Map.new
|
|
36
|
-
|
|
37
|
-
@links = Links.new(@root)
|
|
38
|
-
|
|
39
|
-
@namespaces = namespaces
|
|
40
|
-
|
|
41
|
-
# Default content namespace for dynamic path based lookup:
|
|
42
|
-
@namespaces[CONTENT_NAMESPACE] ||= self.method(:content_tag)
|
|
43
|
-
|
|
44
|
-
# The core namespace for utopia specific functionality:
|
|
45
|
-
@namespaces[UTOPIA_NAMESPACE] ||= Tags
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def freeze
|
|
49
|
-
return self if frozen?
|
|
50
|
-
|
|
51
|
-
@root.freeze
|
|
52
|
-
@namespaces.values.each(&:freeze)
|
|
53
|
-
@namespaces.freeze
|
|
54
|
-
|
|
55
|
-
super
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
attr :root
|
|
59
|
-
|
|
60
|
-
# TODO we should remove this method and expose `@links` directly.
|
|
61
|
-
def links(path, **options)
|
|
62
|
-
@links.index(path, **options)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def fetch_template(path)
|
|
66
|
-
@template_cache.fetch_or_store(path.to_s) do
|
|
67
|
-
XRB::Template.load_file(path)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Look up a named tag such as `<entry />` or `<content:page>...`
|
|
72
|
-
def lookup_tag(qualified_name, node)
|
|
73
|
-
namespace, name = XRB::Tag.split(qualified_name)
|
|
74
|
-
|
|
75
|
-
if library = @namespaces[namespace]
|
|
76
|
-
library.call(name, node)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# @param path [Path] the request path is an absolute uri path, e.g. `/foo/bar`. If an xnode file exists on disk for this exact path, it is instantiated, otherwise nil.
|
|
81
|
-
def lookup_node(path, locale = nil)
|
|
82
|
-
resolve_link(
|
|
83
|
-
@links.for(path, locale)
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def resolve_link(link)
|
|
88
|
-
if full_path = link&.full_path(@root)
|
|
89
|
-
if File.exist?(full_path)
|
|
90
|
-
return Node.new(self, link.path, link.path, full_path)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def respond(link, request)
|
|
96
|
-
if node = resolve_link(link)
|
|
97
|
-
attributes = request.env.fetch(VARIABLES_KEY, {}).to_hash
|
|
98
|
-
|
|
99
|
-
return node.process!(request, attributes)
|
|
100
|
-
elsif redirect_uri = link[:uri]
|
|
101
|
-
return [307, {HTTP::LOCATION => redirect_uri}, []]
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def call(env)
|
|
106
|
-
request = Rack::Request.new(env)
|
|
107
|
-
path = Path.create(request.path_info)
|
|
108
|
-
|
|
109
|
-
# Check if the request is to a non-specific index. This only works for requests with a given name:
|
|
110
|
-
basename = path.basename
|
|
111
|
-
directory_path = File.join(@root, path.dirname.components, basename)
|
|
112
|
-
|
|
113
|
-
# If the request for /foo/bar is actually a directory, rewrite it to /foo/bar/index:
|
|
114
|
-
if File.directory? directory_path
|
|
115
|
-
index_path = [basename, INDEX]
|
|
116
|
-
|
|
117
|
-
return [307, {HTTP::LOCATION => path.dirname.join(index_path).to_s}, []]
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
locale = env[Localization::CURRENT_LOCALE_KEY]
|
|
121
|
-
if link = @links.for(path, locale)
|
|
122
|
-
if response = self.respond(link, request)
|
|
123
|
-
return response
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
return @app.call(env)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
private
|
|
131
|
-
|
|
132
|
-
def lookup_content(name, parent_path)
|
|
133
|
-
if String === name && name.index("/")
|
|
134
|
-
name = Path.create(name)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
if Path === name
|
|
138
|
-
name = parent_path + name
|
|
139
|
-
name_path = name.components.dup
|
|
140
|
-
name_path[-1] += XNODE_EXTENSION
|
|
141
|
-
else
|
|
142
|
-
name_path = name + XNODE_EXTENSION
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
components = parent_path.components.dup
|
|
146
|
-
|
|
147
|
-
while components.any?
|
|
148
|
-
tag_path = File.join(@root, components, name_path)
|
|
149
|
-
|
|
150
|
-
if File.exist? tag_path
|
|
151
|
-
return Node.new(self, Path[components] + name, parent_path + name, tag_path)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
if String === name_path
|
|
155
|
-
tag_path = File.join(@root, components, "_" + name_path)
|
|
156
|
-
|
|
157
|
-
if File.exist? tag_path
|
|
158
|
-
return Node.new(self, Path[components] + name, parent_path + name, tag_path)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
components.pop
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
return nil
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def content_tag(name, node)
|
|
169
|
-
full_path = node.parent_path + name
|
|
170
|
-
|
|
171
|
-
name = full_path.pop
|
|
172
|
-
|
|
173
|
-
# If the current node is called 'foo', we can't lookup 'foo' in the current directory or we will have infinite recursion.
|
|
174
|
-
while full_path.last == name
|
|
175
|
-
full_path.pop
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
cache_key = full_path + name
|
|
179
|
-
|
|
180
|
-
@node_cache.fetch_or_store(cache_key) do
|
|
181
|
-
lookup_content(name, full_path)
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
Traces::Provider(Content) do
|
|
187
|
-
def respond(link, request)
|
|
188
|
-
attributes = {
|
|
189
|
-
"link.key" => link.key,
|
|
190
|
-
"link.href" => link.href
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
Traces.trace("utopia.content.respond", attributes: attributes) {super}
|
|
9
|
+
module Content
|
|
10
|
+
def self.new(...)
|
|
11
|
+
Middleware.new(...)
|
|
194
12
|
end
|
|
195
13
|
end
|
|
196
14
|
end
|
|
@@ -17,32 +17,32 @@ A simple CRUD controller might look like:
|
|
|
17
17
|
```ruby
|
|
18
18
|
prepend Actions
|
|
19
19
|
|
|
20
|
-
on
|
|
20
|
+
on "index" do
|
|
21
21
|
@users = User.all
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
on
|
|
24
|
+
on "new" do |request|
|
|
25
25
|
@user = User.new
|
|
26
26
|
|
|
27
27
|
if request.post?
|
|
28
|
-
@user.update_attributes(request.params[
|
|
28
|
+
@user.update_attributes(request.params["user"])
|
|
29
29
|
|
|
30
30
|
redirect! "index"
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
on
|
|
35
|
-
@user = User.find(request.params[
|
|
34
|
+
on "edit" do |request|
|
|
35
|
+
@user = User.find(request.params["id"])
|
|
36
36
|
|
|
37
37
|
if request.post?
|
|
38
|
-
@user.update_attributes(request.params[
|
|
38
|
+
@user.update_attributes(request.params["user"])
|
|
39
39
|
|
|
40
40
|
redirect! "index"
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
on
|
|
45
|
-
User.find(request.params[
|
|
44
|
+
on "delete" do |request|
|
|
45
|
+
User.find(request.params["id"]).destroy
|
|
46
46
|
|
|
47
47
|
redirect! "index"
|
|
48
48
|
end
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
require_relative "../http"
|
|
7
7
|
|
|
8
8
|
module Utopia
|
|
9
|
-
|
|
9
|
+
module Controller
|
|
10
10
|
CONTENT_TYPE = HTTP::CONTENT_TYPE
|
|
11
11
|
|
|
12
12
|
# The base implementation of a controller class.
|
|
@@ -71,14 +71,14 @@ module Utopia
|
|
|
71
71
|
def process!(request, relative_path)
|
|
72
72
|
return nil
|
|
73
73
|
end
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
# Copy the instance variables from the previous controller to the next controller (usually only a few). This allows controllers to share effectively the same instance variables while still being separate classes/instances.
|
|
76
76
|
def copy_instance_variables(from)
|
|
77
77
|
from.instance_variables.each do |name|
|
|
78
78
|
self.instance_variable_set(name, from.instance_variable_get(name))
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
# Call into the next app as defined by rack.
|
|
83
83
|
def call(env)
|
|
84
84
|
self.class.controller.app.call(env)
|
|
@@ -98,7 +98,7 @@ module Utopia
|
|
|
98
98
|
def ignore!
|
|
99
99
|
throw :response, nil
|
|
100
100
|
end
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
# Request relative redirect. Respond with a redirect to the given target.
|
|
103
103
|
def redirect!(target, status = 302)
|
|
104
104
|
status = HTTP::Status.new(status, 300...400)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2009-2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../path"
|
|
7
|
+
require_relative "../middleware"
|
|
8
|
+
|
|
9
|
+
require_relative "variables"
|
|
10
|
+
require_relative "base"
|
|
11
|
+
require_relative "rewrite"
|
|
12
|
+
require_relative "respond"
|
|
13
|
+
require_relative "actions"
|
|
14
|
+
|
|
15
|
+
require "concurrent/map"
|
|
16
|
+
|
|
17
|
+
module Utopia
|
|
18
|
+
# A middleware which loads controller classes and invokes functionality based on the requested path.
|
|
19
|
+
module Controller
|
|
20
|
+
class Middleware
|
|
21
|
+
# The controller filename.
|
|
22
|
+
CONTROLLER_RB = "controller.rb".freeze
|
|
23
|
+
|
|
24
|
+
# @param root [String] The content root where controllers will be loaded from.
|
|
25
|
+
# @param base [Class] The base class for controllers.
|
|
26
|
+
def initialize(app, root: Utopia::default_root, base: Controller::Base)
|
|
27
|
+
@app = app
|
|
28
|
+
@root = root
|
|
29
|
+
|
|
30
|
+
@controller_cache = Concurrent::Map.new
|
|
31
|
+
|
|
32
|
+
@base = base
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr :app
|
|
36
|
+
|
|
37
|
+
def freeze
|
|
38
|
+
return self if frozen?
|
|
39
|
+
|
|
40
|
+
@root.freeze
|
|
41
|
+
@base.freeze
|
|
42
|
+
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Fetch the controller for the given relative path. May be cached.
|
|
47
|
+
def lookup_controller(path)
|
|
48
|
+
@controller_cache.fetch_or_store(path.to_s) do
|
|
49
|
+
load_controller_file(path)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Loads the controller file for the given relative url_path.
|
|
54
|
+
def load_controller_file(uri_path)
|
|
55
|
+
base_path = File.join(@root, uri_path.components)
|
|
56
|
+
|
|
57
|
+
controller_path = File.join(base_path, CONTROLLER_RB)
|
|
58
|
+
# puts "load_controller_file(#{path.inspect}) => #{controller_path}"
|
|
59
|
+
|
|
60
|
+
if File.exist?(controller_path)
|
|
61
|
+
klass = Class.new(@base)
|
|
62
|
+
|
|
63
|
+
# base_path is expected to be a string representing a filesystem path:
|
|
64
|
+
klass.const_set(:BASE_PATH, base_path.freeze)
|
|
65
|
+
|
|
66
|
+
# uri_path is expected to be an instance of Path:
|
|
67
|
+
klass.const_set(:URI_PATH, uri_path.dup.freeze)
|
|
68
|
+
|
|
69
|
+
klass.const_set(:CONTROLLER, self)
|
|
70
|
+
|
|
71
|
+
klass.class_eval(File.read(controller_path), controller_path)
|
|
72
|
+
|
|
73
|
+
# We lock down the controller class to prevent unsafe modifications:
|
|
74
|
+
klass.freeze
|
|
75
|
+
|
|
76
|
+
# Create an instance of the controller:
|
|
77
|
+
return klass.new
|
|
78
|
+
else
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Invoke the controller layer for a given request. The request path may be rewritten.
|
|
84
|
+
def invoke_controllers(request)
|
|
85
|
+
request_path = Path.from_string(request.path_info)
|
|
86
|
+
|
|
87
|
+
# The request path must be absolute. We could handle this internally but it is probably better for this to be an error:
|
|
88
|
+
raise ArgumentError.new("Invalid request path #{request_path}") unless request_path.absolute?
|
|
89
|
+
|
|
90
|
+
# The controller path contains the current complete path being evaluated:
|
|
91
|
+
controller_path = Path.new
|
|
92
|
+
|
|
93
|
+
# Controller instance variables which eventually get processed by the view:
|
|
94
|
+
variables = request.env[VARIABLES_KEY]
|
|
95
|
+
|
|
96
|
+
while request_path.components.any?
|
|
97
|
+
# We copy one path component from the relative path to the controller path at a time. The controller, when invoked, can modify the relative path (by assigning to relative_path.components). This allows for controller-relative rewrites, but only the remaining path postfix can be modified.
|
|
98
|
+
controller_path.components << request_path.components.shift
|
|
99
|
+
|
|
100
|
+
if controller = lookup_controller(controller_path)
|
|
101
|
+
# Don't modify the original controller:
|
|
102
|
+
controller = controller.clone
|
|
103
|
+
|
|
104
|
+
# Append the controller to the set of controller variables, updates the controller with all current instance variables.
|
|
105
|
+
variables << controller
|
|
106
|
+
|
|
107
|
+
if result = controller.process!(request, request_path)
|
|
108
|
+
return result
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Controllers can directly modify relative_path, which is copied into controller_path. The controllers may have rewriten the path so we update the path info:
|
|
114
|
+
request.env[Rack::PATH_INFO] = controller_path.to_s
|
|
115
|
+
|
|
116
|
+
# No controller gave a useful result:
|
|
117
|
+
return nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def call(env)
|
|
121
|
+
env[VARIABLES_KEY] ||= Variables.new
|
|
122
|
+
|
|
123
|
+
request = Rack::Request.new(env)
|
|
124
|
+
|
|
125
|
+
if result = invoke_controllers(request)
|
|
126
|
+
return result
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return @app.call(env)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -4,60 +4,16 @@
|
|
|
4
4
|
# Copyright, 2016-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "../http"
|
|
7
|
-
require_relative "
|
|
7
|
+
require_relative "responder"
|
|
8
8
|
|
|
9
9
|
module Utopia
|
|
10
|
-
|
|
10
|
+
module Controller
|
|
11
11
|
# A controller layer which provides a convenient way to respond to different requested content types. The order in which you add converters matters, as it determines how the incoming Accept: header is mapped, e.g. the first converter is also defined as matching the media range */*.
|
|
12
12
|
module Respond
|
|
13
13
|
def self.prepended(base)
|
|
14
14
|
base.extend(ClassMethods)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
module Handlers
|
|
18
|
-
module JSON
|
|
19
|
-
APPLICATION_JSON = HTTP::Accept::ContentType.new("application", "json").freeze
|
|
20
|
-
|
|
21
|
-
def self.split(*arguments)
|
|
22
|
-
APPLICATION_JSON.split(*arguments)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.call(context, request, media_range, object, **options)
|
|
26
|
-
if version = media_range.parameters["version"]
|
|
27
|
-
options[:version] = version.to_s
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
context.succeed! content: object.to_json(options), type: APPLICATION_JSON
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
module Passthrough
|
|
35
|
-
WILDCARD = HTTP::Accept::MediaTypes::MediaRange.new("*", "*").freeze
|
|
36
|
-
|
|
37
|
-
def self.split(*arguments)
|
|
38
|
-
WILDCARD.split(*arguments)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.call(context, request, media_range, object, **options)
|
|
42
|
-
# Do nothing.
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
class Responder < Utopia::Responder
|
|
48
|
-
def with_json
|
|
49
|
-
@handlers << Handlers::JSON
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def with_passthrough
|
|
53
|
-
@handlers << Handlers::Passthrough
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def with(content_type, &block)
|
|
57
|
-
handle(content_type, &block)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
17
|
module ClassMethods
|
|
62
18
|
def responds
|
|
63
19
|
@responder ||= Responder.new
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "middleware"
|
|
7
|
+
|
|
8
|
+
module Utopia
|
|
9
|
+
module Controller
|
|
10
|
+
module Handlers
|
|
11
|
+
module JSON
|
|
12
|
+
APPLICATION_JSON = HTTP::Accept::ContentType.new("application", "json").freeze
|
|
13
|
+
|
|
14
|
+
def self.split(*arguments)
|
|
15
|
+
APPLICATION_JSON.split(*arguments)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.call(context, request, media_range, object, **options)
|
|
19
|
+
if version = media_range.parameters["version"]
|
|
20
|
+
options[:version] = version.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context.succeed! content: object.to_json(options), type: APPLICATION_JSON
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module Passthrough
|
|
28
|
+
WILDCARD = HTTP::Accept::MediaTypes::MediaRange.new("*", "*").freeze
|
|
29
|
+
|
|
30
|
+
def self.split(*arguments)
|
|
31
|
+
WILDCARD.split(*arguments)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.call(context, request, media_range, object, **options)
|
|
35
|
+
# Do nothing.
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Responder
|
|
41
|
+
Handler = Struct.new(:content_type, :block) do
|
|
42
|
+
def split(*arguments)
|
|
43
|
+
self.content_type.split(*arguments)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call(context, request, media_range, *arguments, **options)
|
|
47
|
+
context.instance_exec(media_range, *arguments, **options, &self.block)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Responds = Struct.new(:responder, :context, :request) do
|
|
52
|
+
# @todo Refactor `object` -> `*arguments`...
|
|
53
|
+
def with(object, **options)
|
|
54
|
+
responder.call(context, request, object, **options)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize
|
|
59
|
+
@handlers = HTTP::Accept::MediaTypes::Map.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr :handlers
|
|
63
|
+
|
|
64
|
+
def freeze
|
|
65
|
+
@handlers.freeze
|
|
66
|
+
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def call(context, request, *arguments, **options)
|
|
71
|
+
# Parse the list of browser preferred content types and return ordered by priority:
|
|
72
|
+
media_types = HTTP::Accept::MediaTypes.browser_preferred_media_types(request.env)
|
|
73
|
+
|
|
74
|
+
handler, media_range = @handlers.for(media_types)
|
|
75
|
+
|
|
76
|
+
if handler
|
|
77
|
+
handler.call(context, request, media_range, *arguments, **options)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Add a converter for the specified content type. Call the block with the response content if the request accepts the specified content_type.
|
|
82
|
+
def handle(content_type, &block)
|
|
83
|
+
@handlers << Handler.new(content_type, block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def respond_to(context, request)
|
|
87
|
+
Responds.new(self, context, request)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def with_json
|
|
91
|
+
@handlers << Handlers::JSON
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def with_passthrough
|
|
95
|
+
@handlers << Handlers::Passthrough
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def with(content_type, &block)
|
|
99
|
+
handle(content_type, &block)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -19,14 +19,14 @@ rewrite.extract_prefix permalink: /(?<id>\d+)-(?<title>.*)/ do |request, path, m
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
on
|
|
22
|
+
on "post" do
|
|
23
23
|
# You can do further processing here.
|
|
24
24
|
fail! unless @post.published?
|
|
25
25
|
|
|
26
26
|
@comments = @post.comments.first(5)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
on
|
|
29
|
+
on "edit" do
|
|
30
30
|
# You can do further processing here.
|
|
31
31
|
fail! unless @current_user&.editor?
|
|
32
32
|
end
|
|
@@ -7,7 +7,7 @@ require_relative "../http"
|
|
|
7
7
|
require_relative "../path/matcher"
|
|
8
8
|
|
|
9
9
|
module Utopia
|
|
10
|
-
|
|
10
|
+
module Controller
|
|
11
11
|
# This controller layer rewrites the path before executing controller actions. When the rule matches, the supplied block is executed.
|
|
12
12
|
# @example
|
|
13
13
|
# prepend Rewrite
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2014-
|
|
4
|
+
# Copyright, 2014-2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../middleware"
|
|
5
7
|
|
|
6
8
|
module Utopia
|
|
7
|
-
|
|
9
|
+
module Controller
|
|
8
10
|
# Provides a stack-based instance variable lookup mechanism. It can flatten a stack of controllers into a single hash.
|
|
9
11
|
class Variables
|
|
10
12
|
def initialize
|
|
@@ -14,7 +16,7 @@ module Utopia
|
|
|
14
16
|
def top
|
|
15
17
|
@controllers.last
|
|
16
18
|
end
|
|
17
|
-
|
|
19
|
+
|
|
18
20
|
def << controller
|
|
19
21
|
if top = self.top
|
|
20
22
|
# This ensures that most variables will be at the top and controllers can naturally interactive with instance variables:
|
|
@@ -42,7 +44,7 @@ module Utopia
|
|
|
42
44
|
raise KeyError.new(key)
|
|
43
45
|
end
|
|
44
46
|
end
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
def to_hash
|
|
47
49
|
attributes = {}
|
|
48
50
|
|
|
@@ -56,10 +58,14 @@ module Utopia
|
|
|
56
58
|
|
|
57
59
|
return attributes
|
|
58
60
|
end
|
|
59
|
-
|
|
61
|
+
|
|
60
62
|
def [] key
|
|
61
63
|
fetch("@#{key}".to_sym, nil)
|
|
62
64
|
end
|
|
63
65
|
end
|
|
66
|
+
|
|
67
|
+
def self.[] request
|
|
68
|
+
request.env[VARIABLES_KEY]
|
|
69
|
+
end
|
|
64
70
|
end
|
|
65
71
|
end
|