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.45k stars 120 forks source link

Sharing schemas with main application #611

Closed brysontyrrell closed 2 months ago

brysontyrrell commented 3 months ago

Question

I'm just getting started with the OpenAPI Generator and using it with an app I'm building. I'm following a pattern from a tutorial where I add a new target framework and build the client there for use in the main app.

I like this approach for the organization and keeping the namespace clean, but I did find that the types that are generated are no longer available as they're access is internal to the framework only.

I tried adopting one of the example projects where one package builds the client, another builds types, and the client is set to import that, but it resulted in errors vs having a single build.

Is there a way for me to expose the types (input and output) to the main app from the framework, or should I write additional types to return that aren't internal? Or is there some other solutions that I'm not considering?

Thanks for any support. I really like this project and the potential it had for a lot of my future projects.

timbms commented 3 months ago

Set accessModifier: public in openapi-generator-config.yml like this

generate:
  - types
  - client
accessModifier: public
czechboy0 commented 3 months ago

Hi @timbms,

if you set the access modifier to package or public, it should be available to your app's target (if it imports the framework that contains the generated code). That's one way.

Another would be to hand-write the API of the framework and only use the generated code to implement the business logic of the framework.

Both strategies should work, depends on the size and complexity of your project which one might work better.

If you hit build errors, please share more info about the error messages and your project configuration, so that we can help you debug it.

brysontyrrell commented 3 months ago

accessModifier: public did fix that, but I do have a follow-up question about the namespaces in this approach.

When I import the API client target into my main app code Components from Types becomes available, and I can get to the schema objects this way, but those are generic names. If I were building multiple API clients in one project would I end up with multiple Components, or would the generate change the names because there are now multiple clients?

simonjbeaumont commented 3 months ago

@brysontyrrell wrote:

...I do have a follow-up question about the namespaces in this approach.

When I import the API client target into my main app code Components from Types becomes available, and I can get to the schema objects this way, but those are generic names. If I were building multiple API clients in one project would I end up with multiple Components, or would the generate change the names because there are now multiple clients?

You can only run the generator once per-target and it will always produce the generically-named types, e.g. APIProtocol, Client, etc:

...users cannot configure the names of generated types, such as Client and APIProtocol, and there is no attempt to prevent namespace collisions in the target into which it is generated. Instead, users are advised to generate code into a dedicated target, and use Swift’s module system to separate the generated code from code that depends on it. — source: https://swiftpackageindex.com/apple/swift-openapi-generator/1.3.0/documentation/swift-openapi-generator/project-scope-and-goals#Principle-Reduce-complexity-of-the-generator-implementation

You will need to structure your package to have multiple targets, one of each of the APIs you want to call, and configure the generator for each, using package access modifier.

Then, in the target you need them, you can import both, and use the fully-qualified names to disambiguate:

import FooAPI
import BarAPI
...

let fooClient = FooAPI.Client(...)
let barClient = BarAPI.Client(...)
brysontyrrell commented 3 months ago

FooAPI.Client(...)

This is what to achieve for the client and components.

You will need to structure your package to have multiple targets

It is. This is the project structure. The FooAPI directory is a framework target.

App/
├── App/
│   └── ...
└── FooAPI/
    ├── FooAPI.h
    ├── FooAPI.swift
    ├── openapi-generator-config.yaml
    └── openapi.json

In FooAPI.swift I have a FooClient struct that wraps Client and handles configuration. I have a APIClientMiddleware for injecting access tokens, and a AccessTokenManager actor that is handling token requests/refreshes that the middleware uses.

Switching accessModifier to package created a slew of new errors in the generated code:

The package access level used on '...' requires a package name; set it with the compiler flag -package-name

I found the answer in the FAQ to set a name for the Package Access Identifier and fixed that.

The package access errors are gone now, but now I'm back to getting an error that I can't use the generated types as returns for the public methods on my FooClient and I can't access Componentsfrom the app:

(FooAPI/FooAPI.swift)
public struct FooAPIClient {
...
  public func getFooAPIResponse() async throws -> Components.Schemas.ResponseModel 

- - -

Method cannot be declared public because its result uses a package type
(App/ContentView.swift)
struct ContentView: View {
    @State private var fooResponse: FooAPI.Components.Schemas.ResponseModel?
    // @State private var fooResponse: Components.Schemas.ResponseModel?

- - -

No type named 'Components' in module 'FooAPI'
Cannot find type 'Components' in scope  // Commented version
simonjbeaumont commented 3 months ago

Switching accessModifier to package created a slew of new errors in the generated code:

The package access level used on '...' requires a package name; set it with the compiler flag -package-name

I found the answer in the FAQ to set a name for the Package Access Identifier and fixed that.

Yeah, glad you found that.

The package access errors are gone now, but now I'm back to getting an error that I can't use the generated types as returns for the public methods on my FooClient and I can't access Componentsfrom the app:

Right, that's what package means. Do you need thees methods on your framework to be public? Are you exposing them outside of your project? Otherwise, wouldn't using package for these too work for you? If you're exposing a framework from your project to downstreams then you'll need to be cautious about exposing the generated code.

Maybe I'm missing something and, if so, I'm happy to try and help further if you can clarify.

brysontyrrell commented 3 months ago

Thanks for taking the time. If this is a case of me holding it wrong let me know. I am still very new to Swift.

In my FooAPI module I have a wrapper to Client that handles setup.

public struct FooAPIClient {
   let client: Client

    public init(hostname: String, clientID: String, clientSecret: String) {
        self.client = Client(
            serverURL: URL(string: "https://\(hostname):443/api")!,
            configuration: Configuration(dateTranscoder: .iso8601WithFractionalSeconds),
            transport: URLSessionTransport(),

            middlewares: [
                APIClientMiddleware(
                    accessTokenManager: AccessTokenManager(
                        tokenURL: URL(string: "https://\(hostname):443/api/oauth/token")!,
                        clientId: clientID,
                        clientSecret: clientSecret
                    )
                )
            ]
        )
    }
    ...
}

Originally I was writing methods on here that wrapped the methods from the inner client, and I was returning the JSON response type.

    public func getFooAPIResponse() async throws -> Components.Schemas.ResponseModel  {
        let response = try await client.get_foo_api()
        return try response.ok.body.json
    }

I realize now I could make the client property public public let client: Client and use it directly, but in either case I still need the schemas/Components available in the main app code.

simonjbeaumont commented 3 months ago

I realize now I could make the client property public public let client: Client and use it directly, but in either case I still need the schemas/Components available in the main app code.

Well, if you used accessModifier: public then that would apply to the schemas too and they would be available in your main app.

I noticed you had a FooAPI.h there too; are you trying to integrate this into an Objective-C app? If not, it might be simplest to not try and put it in its own framework target.

Just a logistical note: I'm about to head out for a long weekend (public holiday on Monday) so might not get back to this until at least Tuesday.

brysontyrrell commented 3 months ago

Well, if you used accessModifier: public then that would apply to the schemas too and they would be available in your main app.

That was the original advice, and that works, but if I were to create multiple targets for different APIs what would happen to the namespace if I imported more than one in a file?

I noticed you had a FooAPI.h there too; are you trying to integrate this into an Objective-C app? If not, it might be simplest to not try and put it in its own framework target.

I'm not. That was put there when I created a new target and chose "Framework" in Xcode. This is the way a tutorial I watched suggested and it seemed to make sense. Do you have a different recommendation? If it's one API I need to build a client for that is probably much easier, but I am looking at integrating multiple APIs down the line.

Just a logistical note: I'm about to head out for a long weekend (public holiday on Monday) so might not get back to this until at least Tuesday.

Cheers. 🍻

simonjbeaumont commented 2 months ago

That was the original advice, and that works, but if I were to create multiple targets for different APIs what would happen to the namespace if I imported more than one in a file?

You can import multiple modules into the same file even if they declare the same type; you'll just have to disambiguate when using the types.

As a general example, if module A and module B both have expose a type T and you want to depend on both A and B in a downstream module C, then:

// C.swift
import A
import B

let t = T()  // error: Ambiguous use of type T <mumble mumble, not sure of actual error wording here> 
let tA = A.T()  // OK
let tB = B.T()  // OK

I mentioned this a few comments above:

Then, in the target you need them, you can import both, and use the fully-qualified names to disambiguate:

import FooAPI
import BarAPI
...

let fooClient = FooAPI.Client(...)
let barClient = BarAPI.Client(...)

So, concretely, I'm gonna assume the use case here can be viewed as an app that wants to call two different services, each of which is defined by an OpenAPI document. In which case, you structure your project with a distinct module for each of the service clients you would like, each containing the corresponding OpenAPI document and a config file that uses acceesModifier: package. Then you can consume both from your app's module as above.

If I'm still not grasping your problem well enough, then maybe you could put together a minimial reproducer repo/zip and share it?

brysontyrrell commented 2 months ago

@simonjbeaumont I just created a clean project and setup two clients and I think I was confusing myself.

App/
├── App/
│   └── ...
├── FooAPI/
│   ├── FooAPI.swift
│   ├── openapi-generator-config.yaml
│   └── openapi.json
└── BarAPI/
    ├── FooAPI.swift
    ├── openapi-generator-config.yaml
    └── openapi.json

Set accessModifier: public on both. You were right, I just needed to import FooAPI and BarAPI and the namespaces had everything. My confusion was when I was only working with a single client being generated. Now that I've actually tested with two I see that Xcode throws Ambiguous use errors if I don't import and use use the module names.

import FooAPI

let fooClient = FooAPI.Client(...)
var fooGreeting: FooAPI.Components.Schemas.Greeting?

I noticed you had a FooAPI.h there too; are you trying to integrate this into an Objective-C app? If not, it might be simplest to not try and put it in its own framework target.

I also went back to understand this better. I added both FooAPI and BarAPI as new framework targets so all the build settings would be in place, but you're right that I'm not using any Objective-C. I removed the header files and then changed these build settings:

Any errors or warnings are cleared.

Thank you so much for taking the time to walk me through and explain a lot of this.

simonjbeaumont commented 2 months ago

Thank you so much for taking the time to walk me through and explain a lot of this.

You are very welcome. Glad you got things working. I’ll close this issue as resolved. Good luck with your project.