Closed brysontyrrell closed 2 months ago
Set accessModifier: public
in openapi-generator-config.yml
like this
generate:
- types
- client
accessModifier: public
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.
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?
@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
fromTypes
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 multipleComponents
, 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(...)
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 Components
from 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
Switching
accessModifier
topackage
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 accessComponents
from 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.
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.
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.
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. 🍻
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?
@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:
Build Options / Build Libraries for Distribution -> No
Packaging / Defines Modules -> No
Any errors or warnings are cleared.
Thank you so much for taking the time to walk me through and explain a lot of this.
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.
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.