apple / apple/swift-openapi-generator

A way to group features or services to allow middleware to be applied to groups

Open
#551 7 comments 2 reactions 0 assignees View on GitHub
kind/feature status/triage
Dominant language
Swift
Stars
2k
Forks
182
Avg merge
13h 28m
Merged PRs (30d)
5

Description

### Motivation

Using the OpenAPI generator loses a lot of expressivity on the server side. Once you're in generated-code-land, you lose access to the functionality you would be able to have with vanilla routes e.g. applying middleware to groups.
```swift
/// Before
final class AdminMiddleware: AsyncMiddleware
{
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
if request.auth.has(User.self) {
if try await (UserService(request.db).user(from: request.auth).isAdmin {
return try await next.respond(to: request)
}
}
throw Abort(.forbidden, reason: "Admins only, fools")
}
}
let adminProtected = app.grouped(AdminMiddleware())
adminProtected.group("api", configure: apiController)
func apiController(api: RoutesBuilder) {
api.get("users") { req async throws -> [User] in
try await UserService(req.db).allUsers
}
api.delete("user", ":name") { req async throws -> User in
if let username = req.parameters.get("name") {
try await UserService(req.db).deleteUser(username)
} else { throw Abort(.badRequest) }
}
}

/// After
struct APIProtocolImpl: APIProtocol {
// I am using a middleware with swift-dependency to have access to the request
@Dependency(\.request) var req

func getUsers(_ input: Operations.getUsers.Input) async throws -> Operations.getUsers.Output {
// I would need to copy this block for every single route that would be behind AdminMiddleware
guard try await req.auth.has(User.self), (UserService(req.db).user(from: req.auth).isAdmin else {
return .forbidden(.init("Admins only, fools"))
}
return try await .ok(.init(body: .json(
.init(UserService(req.db).allUsers)
)))
}
}
```
Repeating the middleware logic for every single route that should be behind it can quickly grow completely untenable if you have middleware that is supposed to be in front of dozens or hundreds of endpoints.
However you *can* apply middleware to the routesBuilder you pass to the transport:
```swift
let transport = VaporTransport(
routesBuilder: app.grouped(OpenAPIRequestInjectionMiddleware()).grouped(AdminMiddleware())
)
```
But we're artificially limited to one document, which means I can only apply a middleware if I'm willing to put it in front of *all* routes. I've heard that this project intends to implement authorization as part of the API spec, which would alleviate the problem in this specific example, but this isn't the only situation I would want to arbitrarily group routes for a middleware.

### Proposed solution

Leverage the `openapi-generator-config.yaml` configuration file to generate code from multiple documents rather than just one. Let us supply a list of document names that will each generate a protocol with associated types. I envision it looking something like this:
```yaml
generate:
- types
- server
documents:
- api/admin.yaml
- api/public.yaml
```
This config would look for an `api/admin.yaml` and an `api/public.yaml` document in the target. It would then generate two protocols: `AdminAPIProtocol` and `PublicAPIProtocol`. We could then do something like:
```swift
let requestAttached = app.grouped(OpenAPIRequestInjectionMiddleware())

let adminTransport = VaporTransport(routesBuilder: requestAttached.grouped(AdminMiddleware()))
let publicTransport = VaporTransport(routesBuilder: requestAttached)

let adminHandler = AdminAPIProtocolImpl()
let publicHandler = PublicAPIProtocolImpl()

try adminHandler.registerHandlers(on: adminTransport, serverURL: Servers.server1())
try publicHandler.registerHandlers(on: publicTransport, serverURL: Servers.server1())

try await app.execute()
```

If the `documents` list isn't specified in the config, we could default to the normal behavior of looking for an `openapi.yaml` document and generating a single protocol from that.

### Alternatives considered

Ideally, we'd be able to integrate with some unified serverside middleware framework to let us specify middleware in generated-code-land. SSWG is supposed to be working on one, but the repo isn't even public, much less finished. In the meantime, the requirement to run middleware operations on every single route is extremely onerous and this seems like a major blocker to migrating large applications to use it.

### Additional information

_No response_

Contributor guide

Open the contributing guide

Research direction

Start by tracing how openapi-generator-config.yaml is read and how the current single document produces one protocol. Compare that flow with the proposed documents list and generated AdminAPIProtocol/PublicAPIProtocol examples. Done means multiple documents can generate separately registerable protocols while preserving the existing single-document behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
openapi, swift
Domain
backend-api-design, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.