swift-openapi-generator icon indicating copy to clipboard operation
swift-openapi-generator copied to clipboard

Generate types for the path string

Open atacan opened this issue 1 year ago • 2 comments

Motivation

paths:
  /greet:
    get:
      operationId: getGreeting
  /count:
    get:
      operationId: getCount

The actual path of the endpoints that we define is only used in the comment or documentation of the operations. It would be useful to have the string of the path, /greet and /count in the above example, available in the Swift code as well.

One use case that I encountered was when I was making a request to a third-party API. I provide a webhook URL, meaning when the third party finishes, they will call back my endpoint that I defined in my OpenAPI YAML file for the webhook requests. In this case, I need to pass this path to this third-party provider. It would be nice to have it type-safe from changes in the OpenAPI path namings.

Proposed solution

For the simple paths this can be straightforward a constant in the Operations enum. But for the paths that have parameters in them, we might use a simple function with path parameters as its arguments.

public enum Operations {
    public enum greet {
       public static let path = "/greet"
// ...
    }
}

If we have a yaml like

paths:
  /users/{userId}:
    get:
      operationId: getUser

then

public enum Operations {
    public enum getUser {
       public static let path: @Sendable (String) -> String = { "/users/\($0)" }
// ...
    }
}

we can make use of the type of the path parameter

paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            minimum: 1
public enum Operations {
    public enum getUser {
       public static let path: @Sendable (Int) -> String = { "/users/\($0)" }
// ...
    }
}

Alternatives considered

No response

Additional information

No response

atacan avatar Nov 10 '24 20:11 atacan

Hi @atacan,

this is an interesting use case, I agree that for webhooks, this could be useful.

I wonder if a function wouldn't work better here, as it'd allow us to express all the parameters to the path, including future defaulted ones, consistently:

public enum Operations {
    public enum getUser {
        public static func renderedPath(userId: String) -> String { 
            "/users/\(userId)" 
        }
    }
}

Now, the problem with the above is that it won't work correctly for all strings and other types, we have this method to correctly render paths from parameters: https://github.com/apple/swift-openapi-runtime/blob/daa2fb54fe4a7f5187d7286047d5144c8cb97477/Sources/OpenAPIRuntime/Conversion/Converter%2BClient.swift#L37

But to call to that method, we'd need an instance of the Converter, which we only have in the generated client and server code.

So maybe we could constrain this feature to the server, and instead of on Types.swift, it'd be somewhere in Server.swift? Not sure, do you have ideas? https://github.com/apple/swift-openapi-generator/blob/main/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift

czechboy0 avatar Nov 11 '24 08:11 czechboy0

If the converter is needed, and it's a stored property of UniversalServer struct, that rules out to have a static function, right?

This came to my mind:

fileprivate extension UniversalServer where APIHandler: APIProtocol {

    enum Path {
        case getUser(id: Int)
    }

    func path(_ path: Path) throws -> String {
        switch path {
        case .getUser(id: let id):
            return try converter.renderedPath(template: "/users/{id}", parameters: [id])
        }
    }

// ...
}

but it is fileprivate and UniversalServer is not directly used. We simply conform to APIProtocol, and registerHandlers of APIProtocol has an instance of UniversalServer.

atacan avatar Nov 12 '24 21:11 atacan