apple / swift-openapi-generator

Generate Swift client and server code from an OpenAPI document.
https://swiftpackageindex.com/apple/swift-openapi-generator/documentation
Apache License 2.0
1.26k stars 92 forks source link

Support recursive types #70

Closed scsinke closed 8 months ago

scsinke commented 1 year ago

Currently the package does not support recursive types. Should this be possible? Maybe I'm doing something wrong. otherwise, I would propose to add this constraint in the docs. I have this use case due to Server Driven UI. This results in components or actions that can be embedded in each other.

Errors:

czechboy0 commented 1 year ago

Thanks for filing the issue, we'd certainly accept a contribution of support of recursive types.

Could you please add a snippet of OpenAPI that uses it in YAML, and how you imagine the generated code would change? That'll help us discuss the desired solution.

knellr commented 1 year ago

We are looking at migrating from our own hand-rolled code generation to this library, and for some of our APIs we are also encountering this issue.

Here's an example schema with recursion (involving only one model):

Recursion:
  properties:
    recursion:
      "$ref": "#/components/schemas/Recursion"
  type: object

In our existing code generator we break out of the dependency cycles by wrapping the property in a Box:

    public final class Box<Wrapped> {
        public var value: Wrapped
        public init(_ value: Wrapped) {
            self.value = value
        }
    }

    extension Box: Encodable where Wrapped: Encodable {
        public func encode(to encoder: Encoder) throws {
            try value.encode(to: encoder)
        }
    }

    extension Box: Decodable where Wrapped: Decodable {
        public convenience init(from decoder: Decoder) throws {
            let value = try Wrapped(from: decoder)
            self.init(value)
        }
    }

    public struct Recursion: Codable, Hashable {
        public let recursion: Box<Recursion>?
    }
scsinke commented 12 months ago

I don't know if this can solve the issue. But maybe we can make use of the indirect keyword? https://www.hackingwithswift.com/example-code/language/what-are-indirect-enums

czechboy0 commented 12 months ago

Seems that indirect can only be used on enums, not structs.

Before we consider the explicit Box approach, I'd like to see if there's another recommended way in Swift to solve this.

scsinke commented 12 months ago

Hereby a simplified spec we use. For the full openAPI spec we use. check this link.

{
  "openapi": "3.0.2",
  "components": {
    "schemas": {
      "Action": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/PromptAction"
          }
        ]
      },
      "PromptAction": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "PROMPT"
            ]
          },
          "prompt": {
            "$ref": "#/components/schemas/Prompt"
          }
        },
        "required": [
          "type",
          "prompt"
        ]
      },
      "Prompt": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/DialogPrompt"
          }
        ]
      },
      "DialogPrompt": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "DIALOG"
            ]
          },
          "title": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "confirmButtonTitle": {
            "type": "string"
          },
          "cancelButonTitle": {
            "type": "string"
          },
          "confirmAction": {
            "$ref": "#/components/schemas/Action"
          }
        },
        "required": [
          "type",
          "title",
          "message",
          "confirmButtonTitle",
          "cancelButonTitle",
          "confirmAction"
        ]
      }
    }
  }
}
scsinke commented 12 months ago

I think there are three options as far as I can tell.

czechboy0 commented 12 months ago

Yeah, that sounds about right. Ideally, we could constrain this as much as possible, to:

And we could use a property wrapper.

We already have the concept of a TypeUsage that represents a type being wrapped in an optional, an array, etc. I guess we could think of an "indirection box" as just another type of wrapping here, make it Sendable, Codable, etc.

It's important we limit how many places in the generator have to make explicit decisions based on this.

Might be worth for someone to prototype this and see what else might get hit.

scsinke commented 11 months ago

@czechboy0 If you can give me some pointers on how and where to start prototyping. Will try to play around with it.

czechboy0 commented 11 months ago

Hi @scsinke, sure!

TypeUsage is defined here: https://github.com/apple/swift-openapi-generator/blob/6b11135cccfb0846809f434cf2ad95134b65e945/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift#L43

The need for boxing could be represented as another case in the type usage internal enum.

The need for it would be calculated for the property schema, probably somewhere around https://github.com/apple/swift-openapi-generator/blob/6b11135cccfb0846809f434cf2ad95134b65e945/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift#L234, if the parent type and the property type are the same.

Then, we'd probably need to adjust the struct generation logic to include the extra boxing when requested: https://github.com/apple/swift-openapi-generator/blob/6b11135cccfb0846809f434cf2ad95134b65e945/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStructBlueprint.swift#L21

Would be great if the box could be represented as a property wrapper somehow.

But that's just one idea, you'll see what looks good when you're prototyping it, feel free to tag me on a PR with a proof of concept, and we can discuss more.

czechboy0 commented 10 months ago

Another way to implement this would be:

If someone wants to pick this up, it could make sense to PR these bits independently, e.g. the algorithm first, then the changes to the generator.

czechboy0 commented 8 months ago

This also blocks generating the App Store Connect OpenAPI document: https://developer.apple.com/sample-code/app-store-connect/app-store-connect-openapi-specification.zip

The schema DiagnosticLogCallStackNode is recursive there.

herrernst commented 8 months ago

We have lots of cycles in the API we want to use, so fixing this would be crucial for us. Thanks in advance.

czechboy0 commented 8 months ago

Thanks @herrernst for letting us know. Without going into any confidential specifics, what's your use case of recursive schemas? We've seen a representation of a file system hierarchy, which is inherently recursive, and in general graph representations. What's yours?

yanniks commented 8 months ago

Also very interested in this. We also have the use-case of server-driven UI.

herrernst commented 8 months ago

Thanks @herrernst for letting us know. Without going into any confidential specifics, what's your use case of recursive schemas? We've seen a representation of a file system hierarchy, which is inherently recursive, and in general graph representations. What's yours?

Just basic things like a Person having a partner that is also a Person or having friends that are Persons.

MattNewberry commented 8 months ago

In our use case, we have a data type which represents a polymorphic wrapper which encapsulates nested data types of the same wrapper. In short, we have Content which contains an array of Content.

For example:

[
    {
        "type": "box",
        "value": [ // Polymorphic envelope
            {
                "type": "text",
                "value": [ // Polymorphic envelope
                    {
                        "text": "hello world"
                    }
                ]
            }
        ]
    }
]

(We're also anxiously awaiting recursive types to be supported)

czechboy0 commented 8 months ago

Thanks for the info, everyone, helps as I'm working on the design for this.

czechboy0 commented 8 months ago

Landed in main, will get released in 0.3.1.

czechboy0 commented 8 months ago

Shipped in https://github.com/apple/swift-openapi-generator/releases/tag/0.3.1.

herrernst commented 8 months ago

Thank you, I can confirm it's working for my schema!

czechboy0 commented 8 months ago

Glad to hear that. Just FYI for anyone else following up, I did find a scenario under which the cycle detector doesn't work correctly in a complex graph and am investigating it. If you hit an issue where the generated code doesn't compile, complaining about "infinite size" structs, please let me know here.

czechboy0 commented 8 months ago

Here's the fix: https://github.com/apple/swift-openapi-generator/pull/335