Skip to content

Commit 0c1cb20

Browse files
committed
Merge branch 'develop'
2 parents d0ee257 + c0cfc8c commit 0c1cb20

File tree

5 files changed

+832
-2
lines changed

5 files changed

+832
-2
lines changed

Package.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ let package = Package(
66

77
name: "MacroExpress",
88

9+
platforms: [ .macOS(.v12), .iOS(.v15) ],
10+
911
products: [
1012
.library(name: "MacroExpress", targets: [ "MacroExpress" ]),
1113
.library(name: "express", targets: [ "express" ]),
@@ -17,7 +19,7 @@ let package = Package(
1719

1820
dependencies: [
1921
.package(url: "https://github.com/Macro-swift/Macro.git",
20-
from: "1.0.14"),
22+
from: "1.0.16"),
2123
.package(url: "https://github.com/AlwaysRightInstitute/mustache.git",
2224
from: "1.0.2")
2325
],
@@ -61,7 +63,8 @@ let package = Package(
6163
.testTarget(name: "dotenvTests", dependencies: [ "dotenv" ]),
6264
.testTarget(name: "RouteTests", dependencies: [
6365
.product(name: "MacroTestUtilities", package: "Macro"),
64-
"express"
66+
.product(name: "MacroCore", package: "Macro"),
67+
"connect", "express"
6568
])
6669
]
6770
)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
//
2+
// Middleware.swift
3+
// MacroExpress
4+
//
5+
// Created by Helge Heß on 2/28/26.
6+
// Copyright © 2026 ZeeZide GmbH. All rights reserved.
7+
//
8+
9+
import MacroCore
10+
import NIOCore
11+
12+
/**
13+
* A variant of ``Middleware`` that can itself invoke async calls.
14+
*
15+
* They take a request (``IncomingMessage``) and a response
16+
* (``ServerResponse``) object, as well as a closure to signal
17+
* whether they fully handled the request or whether the
18+
* respective "Router" (e.g. Connect) should run the next
19+
* middleware.
20+
*
21+
* Call ``Next`` when the request processing needs to continue,
22+
* just return if the request was fully processed.
23+
*
24+
* Use the ``async(_:)-1t2gx`` free function to convert an
25+
* `AsyncMiddleware` into a regular ``Middleware`` that can be
26+
* registered with a ``Route``:
27+
* ```swift
28+
* app.use(async { req, res, next in
29+
* let data = try await fetchData()
30+
* res.json(data)
31+
* })
32+
*
33+
* app.use(async { req, res, next in
34+
* let data = try await doStuff()
35+
* next() // continue
36+
* })
37+
* ```
38+
*/
39+
public typealias AsyncMiddleware =
40+
( IncomingMessage, ServerResponse, @escaping Next )
41+
async throws -> Void
42+
43+
/**
44+
* A variant of ``FinalMiddleware`` that can itself invoke async calls.
45+
*
46+
* Async final middleware are functions that deal with HTTP
47+
* transactions using Swift concurrency (`async`/`await`).
48+
* Unlike ``AsyncMiddleware``, `AsyncFinalMiddleware` always
49+
* ends the response and never calls ``Next``.
50+
*
51+
* The closures takes a request (``IncomingMessage``) and a response
52+
* (``ServerResponse``) object.
53+
*
54+
* Use the ``async(_:)-6fwgt`` free function to convert an
55+
* `AsyncFinalMiddleware` into a regular ``Middleware``:
56+
* ```swift
57+
* app.get("/hello", async { req, res in
58+
* let greeting = try await generateGreeting()
59+
* res.send(greeting)
60+
* })
61+
* ```
62+
*/
63+
public typealias AsyncFinalMiddleware =
64+
( IncomingMessage, ServerResponse ) async throws -> Void
65+
66+
/**
67+
* Convert an ``AsyncMiddleware`` into a synchronous
68+
* ``Middleware`` that can be used with Connect/Express
69+
* routing.
70+
*
71+
* The returned middleware spawns a `Task` that runs the
72+
* async closure. If the async middleware throws, the error
73+
* is forwarded to ``Next`` so that error middleware in the
74+
* chain can handle it.
75+
*
76+
* Example:
77+
* ```swift
78+
* app.use(async { req, res, next in
79+
* let user = try await db.findUser(req.params["id"])
80+
* req.extra[ObjectIdentifier(User.self)] = user
81+
* next()
82+
* })
83+
* ```
84+
*
85+
* - Note: This uses `MacroCore.shared.retain()` /
86+
* `release()` to keep the process alive while the
87+
* `Task` is running. The `next` closure is dispatched
88+
* back onto the NIO event loop.
89+
* - Parameters:
90+
* - middleware: The async middleware to wrap.
91+
* - Returns: A synchronous ``Middleware`` suitable for `route.use()`.
92+
*/
93+
public func `async`(_ middleware: @escaping AsyncMiddleware) -> Middleware {
94+
// This is not exactly cheap, but a convenient measure until we can do this
95+
// in a better way. Also requires IncomingRequest/Response to be Sendable,
96+
// which ideally would not be necessary (should be non-isolated and get sent
97+
// around).
98+
return { req, res, next in
99+
let module = MacroCore.shared.retain()
100+
101+
// Make a sendable-ish wrapper around `next`.
102+
let sendableNext: @Sendable (Any...) -> Void = { (args: Any...) in
103+
module.fallbackEventLoop().execute {
104+
switch args.count {
105+
case 0 : next() // no arguments
106+
case 1 : next(args[0]) // one argument
107+
case 2 : next(args[0], args[1]) // two arguments
108+
default : // more than two arguments
109+
// can't flatten, pass as array, but preserve error
110+
if let err = args.first as? Error {
111+
next(err, Array(args.dropFirst()))
112+
}
113+
else { next(args) }
114+
}
115+
module.release()
116+
}
117+
}
118+
119+
Task {
120+
do {
121+
try await middleware(req, res, sendableNext)
122+
}
123+
catch { // forward error via next(error)
124+
sendableNext(error)
125+
}
126+
}
127+
}
128+
}
129+
130+
/**
131+
* Convert an ``AsyncFinalMiddleware`` into a synchronous
132+
* ``Middleware`` that can be used with Connect/Express
133+
* routing.
134+
*
135+
* The returned middleware spawns a `Task` that runs the
136+
* async closure. Since final middleware never calls ``Next``,
137+
* any thrown error is forwarded to ``Next`` as an error so
138+
* that error middleware in the chain can handle it.
139+
*
140+
* Example:
141+
* ```swift
142+
* app.get("/hello", async { req, res in
143+
* let greeting = try await generateGreeting()
144+
* res.send(greeting)
145+
* })
146+
* ```
147+
*
148+
* - Note: This uses `MacroCore.shared.retain()` /
149+
* `release()` to keep the process alive while the
150+
* `Task` is running.
151+
*
152+
* - Parameters:
153+
* - middleware: The async final middleware to wrap.
154+
* - Returns: A synchronous ``Middleware`` suitable for `route.get()` etc.
155+
*/
156+
@inlinable
157+
public func `async`(_ middleware: @escaping AsyncFinalMiddleware) -> Middleware
158+
{
159+
return { req, res, next in
160+
let module = MacroCore.shared.retain()
161+
162+
Task {
163+
do {
164+
try await middleware(req, res)
165+
module.release()
166+
}
167+
catch {
168+
module.fallbackEventLoop().execute {
169+
next(error)
170+
module.release()
171+
}
172+
}
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)