Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift5 Vapor 4 client library #9583

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions bin/configs/swift5-vapor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
generatorName: swift5
outputDir: samples/client/petstore/swift5/vaporLibrary
library: vapor
inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore-with-fake-endpoints-models-for-testing.yaml
templateDir: modules/openapi-generator/src/main/resources/swift5
generateAliasAsModel: true
additionalProperties:
projectName: PetstoreClient
useSPMFileStructure: true
useClasses: true
useBacktickEscapes: true
mapFileBinaryToData: true
3 changes: 2 additions & 1 deletion docs/generators/swift5.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |true|
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C#have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.</dd></dl>|true|
|lenientTypeCast|Accept and cast values for simple types (string-&gt;bool, string-&gt;int, int-&gt;string)| |false|
|library|Library template (sub-template) to use|<dl><dt>**urlsession**</dt><dd>[DEFAULT] HTTP client: URLSession</dd><dt>**alamofire**</dt><dd>HTTP client: Alamofire</dd></dl>|urlsession|
|library|Library template (sub-template) to use|<dl><dt>**urlsession**</dt><dd>[DEFAULT] HTTP client: URLSession</dd><dt>**alamofire**</dt><dd>HTTP client: Alamofire</dd><dt>**vapor**</dt><dd>HTTP client: Vapor</dd></dl>|urlsession|
|mapFileBinaryToData|Map File and Binary to Data (default: false)| |false|
|nonPublicApi|Generates code with reduced access modifiers; allows embedding elsewhere without exposing non-public API calls to consumers.(default: false)| |null|
|objcCompatible|Add additional properties and methods for Objective-C compatibility (default: false)| |null|
Expand All @@ -39,6 +39,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|swiftPackagePath|Set a custom source path instead of OpenAPIClient/Classes/OpenAPIs.| |null|
|swiftUseApiNamespace|Flag to make all the API classes inner-class of {{projectName}}API| |null|
|useBacktickEscapes|Escape reserved words using backticks (default: false)| |false|
|useClasses|Use final classes for models instead of structs (default: false)| |false|
|useSPMFileStructure|Use SPM file structure and set the source path to Sources/{{projectName}} (default: false).| |null|

## IMPORT MAPPING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ public class Swift5ClientCodegen extends DefaultCodegen implements CodegenConfig
public static final String LENIENT_TYPE_CAST = "lenientTypeCast";
public static final String USE_SPM_FILE_STRUCTURE = "useSPMFileStructure";
public static final String SWIFT_PACKAGE_PATH = "swiftPackagePath";
public static final String USE_CLASSES = "useClasses";
public static final String USE_BACKTICK_ESCAPES = "useBacktickEscapes";
public static final String GENERATE_MODEL_ADDITIONAL_PROPERTIES = "generateModelAdditionalProperties";
public static final String HASHABLE_MODELS = "hashableModels";
public static final String MAP_FILE_BINARY_TO_DATA = "mapFileBinaryToData";
protected static final String LIBRARY_ALAMOFIRE = "alamofire";
protected static final String LIBRARY_URLSESSION = "urlsession";
protected static final String LIBRARY_VAPOR = "vapor";
protected static final String RESPONSE_LIBRARY_PROMISE_KIT = "PromiseKit";
protected static final String RESPONSE_LIBRARY_RX_SWIFT = "RxSwift";
protected static final String RESPONSE_LIBRARY_RESULT = "Result";
Expand All @@ -82,6 +84,7 @@ public class Swift5ClientCodegen extends DefaultCodegen implements CodegenConfig
protected boolean swiftUseApiNamespace = false;
protected boolean useSPMFileStructure = false;
protected String swiftPackagePath = "Classes" + File.separator + "OpenAPIs";
protected boolean useClasses = false;
protected boolean useBacktickEscapes = false;
protected boolean generateModelAdditionalProperties = true;
protected boolean hashableModels = true;
Expand Down Expand Up @@ -282,6 +285,8 @@ public Swift5ClientCodegen() {
+ " and set the source path to Sources" + File.separator + "{{projectName}} (default: false)."));
cliOptions.add(new CliOption(SWIFT_PACKAGE_PATH, "Set a custom source path instead of "
+ projectName + File.separator + "Classes" + File.separator + "OpenAPIs" + "."));
cliOptions.add(new CliOption(USE_CLASSES, "Use final classes for models instead of structs (default: false)")
.defaultValue(Boolean.FALSE.toString()));

cliOptions.add(new CliOption(HASHABLE_MODELS,
"Make hashable models (default: true)")
Expand All @@ -293,6 +298,7 @@ public Swift5ClientCodegen() {

supportedLibraries.put(LIBRARY_URLSESSION, "[DEFAULT] HTTP client: URLSession");
supportedLibraries.put(LIBRARY_ALAMOFIRE, "HTTP client: Alamofire");
supportedLibraries.put(LIBRARY_VAPOR, "HTTP client: Vapor");

CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "Library template (sub-template) to use");
libraryOption.setEnum(supportedLibraries);
Expand Down Expand Up @@ -442,7 +448,7 @@ public void processOpts() {
additionalProperties.put(READONLY_PROPERTIES, readonlyProperties);

// Setup swiftUseApiNamespace option, which makes all the API
// classes inner-class of {{projectName}}API
// classes inner-class of {{projectName}}
if (additionalProperties.containsKey(SWIFT_USE_API_NAMESPACE)) {
setSwiftUseApiNamespace(convertPropertyToBooleanAndWriteBack(SWIFT_USE_API_NAMESPACE));
}
Expand Down Expand Up @@ -484,63 +490,69 @@ public void processOpts() {
typeMapping.put("binary", "Data");
}

if (additionalProperties.containsKey(USE_CLASSES)) {
setUseClasses(convertPropertyToBooleanAndWriteBack(USE_CLASSES));
}

setLenientTypeCast(convertPropertyToBooleanAndWriteBack(LENIENT_TYPE_CAST));

// make api and model doc path available in mustache template
additionalProperties.put("apiDocPath", apiDocPath);
additionalProperties.put("modelDocPath", modelDocPath);

supportingFiles.add(new SupportingFile("Podspec.mustache",
"",
projectName + ".podspec"));
supportingFiles.add(new SupportingFile("Cartfile.mustache",
"",
"Cartfile"));
if (!getLibrary().equals(LIBRARY_VAPOR)) {
supportingFiles.add(new SupportingFile("Podspec.mustache",
"",
projectName + ".podspec"));
supportingFiles.add(new SupportingFile("Cartfile.mustache",
"",
"Cartfile"));
supportingFiles.add(new SupportingFile("CodableHelper.mustache",
sourceFolder,
"CodableHelper.swift"));
supportingFiles.add(new SupportingFile("OpenISO8601DateFormatter.mustache",
sourceFolder,
"OpenISO8601DateFormatter.swift"));
supportingFiles.add(new SupportingFile("JSONDataEncoding.mustache",
sourceFolder,
"JSONDataEncoding.swift"));
supportingFiles.add(new SupportingFile("JSONEncodingHelper.mustache",
sourceFolder,
"JSONEncodingHelper.swift"));
supportingFiles.add(new SupportingFile("git_push.sh.mustache",
"",
"git_push.sh"));
supportingFiles.add(new SupportingFile("SynchronizedDictionary.mustache",
sourceFolder,
"SynchronizedDictionary.swift"));
supportingFiles.add(new SupportingFile("XcodeGen.mustache",
"",
"project.yml"));
supportingFiles.add(new SupportingFile("APIHelper.mustache",
sourceFolder,
"APIHelper.swift"));
supportingFiles.add(new SupportingFile("Models.mustache",
sourceFolder,
"Models.swift"));
}
supportingFiles.add(new SupportingFile("Package.swift.mustache",
"",
"Package.swift"));
supportingFiles.add(new SupportingFile("APIHelper.mustache",
sourceFolder,
"APIHelper.swift"));
supportingFiles.add(new SupportingFile("Configuration.mustache",
sourceFolder,
"Configuration.swift"));
supportingFiles.add(new SupportingFile("Extensions.mustache",
sourceFolder,
"Extensions.swift"));
supportingFiles.add(new SupportingFile("Models.mustache",
sourceFolder,
"Models.swift"));
supportingFiles.add(new SupportingFile("APIs.mustache",
sourceFolder,
"APIs.swift"));
supportingFiles.add(new SupportingFile("CodableHelper.mustache",
sourceFolder,
"CodableHelper.swift"));
supportingFiles.add(new SupportingFile("OpenISO8601DateFormatter.mustache",
sourceFolder,
"OpenISO8601DateFormatter.swift"));
supportingFiles.add(new SupportingFile("JSONDataEncoding.mustache",
sourceFolder,
"JSONDataEncoding.swift"));
supportingFiles.add(new SupportingFile("JSONEncodingHelper.mustache",
sourceFolder,
"JSONEncodingHelper.swift"));
supportingFiles.add(new SupportingFile("git_push.sh.mustache",
"",
"git_push.sh"));
supportingFiles.add(new SupportingFile("SynchronizedDictionary.mustache",
sourceFolder,
"SynchronizedDictionary.swift"));
supportingFiles.add(new SupportingFile("gitignore.mustache",
"",
".gitignore"));
supportingFiles.add(new SupportingFile("README.mustache",
"",
"README.md"));
supportingFiles.add(new SupportingFile("XcodeGen.mustache",
"",
"project.yml"));

switch (getLibrary()) {
case LIBRARY_ALAMOFIRE:
Expand All @@ -555,6 +567,9 @@ public void processOpts() {
sourceFolder,
"URLSessionImplementations.swift"));
break;
case LIBRARY_VAPOR:
additionalProperties.put("useVapor", true);
break;
default:
break;
}
Expand Down Expand Up @@ -904,6 +919,10 @@ public void setSwiftPackagePath(String swiftPackagePath) {
this.swiftPackagePath = swiftPackagePath;
}

public void setUseClasses(boolean useClasses) {
this.useClasses = useClasses;
}

public void setUseBacktickEscapes(boolean useBacktickEscapes) {
this.useBacktickEscapes = useBacktickEscapes;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// https://openapi-generator.tech
//

import Foundation
import Foundation{{#useVapor}}
import Vapor{{/useVapor}}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} struct APIHelper {
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static func rejectNil(_ source: [String: Any?]) -> [String: Any]? {
Expand Down
23 changes: 16 additions & 7 deletions modules/openapi-generator/src/main/resources/swift5/APIs.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
//

import Foundation
{{#useVapor}}
import Vapor
{{/useVapor}}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class {{projectName}}API {
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class {{projectName}} {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aymanbagabas this is a breaking change that affects a lot of file, and what I remember from the other PR is that this change was only related to naming, so I suggest to revert it please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's only related to naming. However, it doesn't make sense to forcefully append "API" to the project name. The user should have a choice of having that in the name. Simply when desired, the user can append "API" to the project name and it would give the old behavior back.

I'll make a separate PR for that one. Should I base it on top of the 5.2.x branch?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I don't think this is an option worth to create.
It will add extra complexity and we won't add nothing new or extra functionality to the project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aesthetics matter, having this suffix in the name is extra complexity. By merging it to the 5.2.x branch, we will introduce a breaking change with fallback and we don't have to introduce any flags or options since the fallback is to have API in the project name.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that would break every project that uses OpenAPI Swift Generator with compilation errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but for a generated library, I would rather have the project name matches the library class name since there is only one class. This also matches the SPM file structure for example the PlayingCard library

example-package-playingcard
├── Sources
│   └── PlayingCard
│       ├── PlayingCard.swift
│       ├── Rank.swift
│       └── Suit.swift
└── Package.swift

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point.
My main issue here is creating an extra burden on the user side.
To try to make this transition as smooth as possible, we could do something like this.

@available(*, deprecated, renamed: "PlayingCard")
public typealias PlayingCardAPI = PlayingCard

open class PlayingCard {
    ...
}

This way the old name PlayingCardAPI would still work, and the user could migrate when he wanted it.
Plus the IDE would presente a warning in a quick fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a yes? Should I create a PR for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You last comment made sense to me, so for me it's a yes.
Could you please open a PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@4brunu I'm also thinking about doing a similar thing for Configuration.mustache and model.mustache when using the swiftUseApiNamespace option. I believe when users enable swiftUseApiNamespace, the Configuration class and models should also be under the API namespace like what api.mustache has. Because it doesn't make sense to do so for the API classes and not for the other classes. For example:

@available(*, deprecated, renamed: "PlayingCard.Configuration")
public typealias Configuration = PlayingCard.Configuration

extension PlayingCard {
    open class Configuration {
        ...
    }
}

and

@available(*, deprecated, renamed: "PlayingCard.Rank")
public typealias Rank = PlayingCard.Rank

extension PlayingCard {
    enum Rank: Int {}
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var basePath = "{{{basePath}}}"
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var credential: URLCredential?
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var customHeaders: [String: String] = [:]{{#useAlamofire}}
{{#useVapor}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var customHeaders: HTTPHeaders = [:]
{{/useVapor}}
{{^useVapor}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var customHeaders: [String: String] = [:]
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var credential: URLCredential?{{#useAlamofire}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var requestBuilderFactory: RequestBuilderFactory = AlamofireRequestBuilderFactory(){{/useAlamofire}}{{#useURLSession}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(){{/useURLSession}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var apiResponseQueue: DispatchQueue = .main
{{/useVapor}}
}

{{#useVapor}}{{/useVapor}}{{^useVapor}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class RequestBuilder<T> {
var credential: URLCredential?
var headers: [String: String]
Expand All @@ -33,7 +42,7 @@ import Foundation
self.parameters = parameters
self.headers = headers

addHeaders({{projectName}}API.customHeaders)
addHeaders({{projectName}}.customHeaders)
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func addHeaders(_ aHeaders: [String: String]) {
Expand All @@ -42,7 +51,7 @@ import Foundation
}
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func execute(_ apiResponseQueue: DispatchQueue = {{projectName}}API.apiResponseQueue, _ completion: @escaping (_ result: Swift.Result<Response<T>, Error>) -> Void) { }
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func execute(_ apiResponseQueue: DispatchQueue = {{projectName}}.apiResponseQueue, _ completion: @escaping (_ result: Swift.Result<Response<T>, Error>) -> Void) { }

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} func addHeader(name: String, value: String) -> Self {
if !value.isEmpty {
Expand All @@ -52,12 +61,12 @@ import Foundation
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func addCredential() -> Self {
credential = {{projectName}}API.credential
credential = {{projectName}}.credential
return self
}
}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol RequestBuilderFactory {
func getNonDecodableBuilder<T>() -> RequestBuilder<T>.Type
func getBuilder<T: Decodable>() -> RequestBuilder<T>.Type
}
}{{/useVapor}}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
//

import Foundation
{{#useVapor}}import Vapor{{/useVapor}}

{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class Configuration {
{{#useVapor}}{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var apiClient: Vapor.Client? = nil
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var apiWrapper: (inout Vapor.ClientRequest) throws -> () = { _ in }
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var contentConfiguration = ContentConfiguration.default(){{/useVapor}}{{^useVapor}}
// This value is used to configure the date formatter that is used to serialize dates into JSON format.
// You must set it prior to encoding any dates, and it will only be read once.
@available(*, unavailable, message: "To set a different date format, use CodableHelper.dateFormatter instead.")
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static var dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"{{/useVapor}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import Foundation
#if canImport(AnyCodable)
import AnyCodable
#endif{{#usePromiseKit}}
import PromiseKit{{/usePromiseKit}}
import PromiseKit{{/usePromiseKit}}{{#useVapor}}
import Vapor{{/useVapor}}

{{^useVapor}}
extension Bool: JSONEncodable {
func encodeToJSON() -> Any { return self as Any }
}
Expand Down Expand Up @@ -94,7 +96,7 @@ extension UUID: JSONEncodable {
func encodeToJSON() -> Any {
return self.uuidString
}
}{{#generateModelAdditionalProperties}}
}{{/useVapor}}{{#generateModelAdditionalProperties}}

extension String: CodingKey {

Expand Down Expand Up @@ -180,13 +182,13 @@ extension KeyedDecodingContainerProtocol {
return map
}

}{{/generateModelAdditionalProperties}}
}{{/generateModelAdditionalProperties}}{{^useVapor}}

extension HTTPURLResponse {
var isStatusCodeSuccessful: Bool {
return Array(200 ..< 300).contains(statusCode)
}
}{{#usePromiseKit}}
}{{/useVapor}}{{#usePromiseKit}}

extension RequestBuilder {
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} func execute() -> Promise<Response<T>> {
Expand All @@ -202,6 +204,41 @@ extension RequestBuilder {
return deferred.promise
}
}{{/usePromiseKit}}
{{#useVapor}}

extension UUID: Content { }

extension URL: Content { }

extension Bool: Content { }

extension Set: ResponseEncodable where Element: Content {
public func encodeResponse(for request: Vapor.Request) -> EventLoopFuture<Vapor.Response> {
let response = Vapor.Response()
do {
try response.content.encode(Array(self))
} catch {
return request.eventLoop.makeFailedFuture(error)
}
return request.eventLoop.makeSucceededFuture(response)
}
}

extension Set: RequestDecodable where Element: Content {
public static func decodeRequest(_ request: Vapor.Request) -> EventLoopFuture<Self> {
do {
let content = try request.content.decode([Element].self)
return request.eventLoop.makeSucceededFuture(Set(content))
} catch {
return request.eventLoop.makeFailedFuture(error)
}
}
}

extension Set: Content where Element: Content { }

extension AnyCodable: Content {}
{{/useVapor}}

#if canImport(AnyCodable)
extension AnyCodable: Hashable {
Expand Down
Loading