dankinsoid / VaporToOpenAPI

OpenAPI specification generator for Vapor based Swift projects.
MIT License
98 stars 8 forks source link

Is there a limitation / workaround to recursive Codable types being exposed by VaporToOpenAPI? #17

Closed heckj closed 9 months ago

heckj commented 9 months ago

I'm doing a bit of debugging on an open source project (Swift Package Index) that's leveraging VaporToOpenAPI to present an OpenAPI file for its API. One particular section isn't rendering correctly - a single route that has a nested layer of Codable structures for its data transfer types.

The route reads with this .openAPI() extension:

.openAPI(
    summary: "/api/versions/{id}/build-report",
    description: "Send a build report.",
    body: .type(of: API.PostBuildReportDTO.example),
    response: .type(HTTPStatus.self),
    responseContentType: .application(.json)
)

The type that's not getting converted into spec correctly reads as:

public struct ProductDependency: Codable, Equatable {
    public var identity: String
    public var name: String
    public var url: String
    public var dependencies: [ProductDependency]

    public init(identity: String, name: String, url: String, dependencies: [ProductDependency]) {
        self.identity = identity
        self.name = name
        self.url = url
        self.dependencies = dependencies
    }
}

(This particular type also happens to be in an external module, but I suspect that wouldn't likely be an issue - but just in case, I'll mention it)

The resulting schema that's exposed for this type/schema is:

            "ProductDependency": {
                "properties": {
                    "identity": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "dependencies": {
                        "$ref": "#/components/schemas/ArrayProductDependency"
                    },
                    "url": {
                        "type": "string"
                    }
                },
                "type": "object",
                "required": [
                    "dependencies",
                    "identity",
                    "name",
                    "url"
                ]
            },

The issue being that ArrayProductDependency should be to an array of ProductDependency, but it looks like Array was tacked on in front, generating a new type to represent an array of ProductDependency, but not including it in the schema that was output. I'm not 100% confident on how to represent this in OpenAPI 3.0.1 today - so I'm not sure if another type would make sense for an "Array of ProductDependency" or if the dependencies property should be a type: "array" to a reference to #/components/schemas/ProductDependency.

I wasn't sure if this was a known limitation of VaporToOpenAPI - so I wanted to ask that first, and then inquire about the escape hatches and what might be useful for a resolution. I saw the documentation notes in the README about potentionally conforming that type to OpenAPIDescriptable to either generate the type through comments, or provide a manual structure for the schema itself. Or possibly conforming to OpenAPIType to provide an alternate schema if that would make more sense.

Any suggestions for how this would be ideally resolved?

heckj commented 9 months ago

I fiddled around in an OpenAPI editor for a bit, and I think the correct spec for this type should be something like:

            "ProductDependency": {
                "properties": {
                    "identity": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "dependencies": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/ProductDependency"
                        }
                    },
                    "url": {
                        "type": "string"
                    }
                },
                "type": "object",
                "required": [
                    "dependencies",
                    "identity",
                    "name",
                    "url"
                ]
            },
dankinsoid commented 9 months ago

@heckj There can be some troubles with recursive types. I'll try to improve this, but now you can try to update your API.PostBuildReportDTO.example var. This example must contain all possible values, including recursive values:

ProductDependency(
        identity: "0",
        name: "name",
        url: "http://vapor.com",
        dependencies: [
            ProductDependency(
                identity: "1",
                name: "name",
                url: "http://vapor.com",
                dependencies: []
            )
        ]
    )
dankinsoid commented 9 months ago

@heckj I fixed it in 4.4.3

heckj commented 9 months ago

Thank you! I've tried adding the stanza, and bumping the version, in a branch of SPI - but it doesn't appear to be doing the trick. I've applied the updates in a branch for SwiftPackageIndex (and in a PR, if you're inclined to look: https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/pull/2756) - but it's still generating the ArrayProductDependency reference without an associated component.