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

Major ✨ Typed Throws #149

Merged
merged 3 commits into from
Nov 22, 2024
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ extension APIRequest {
}.joined(separator: ", ")
}

private var functionReturnType: String {
returnType.typeName.toString(required: true)
}

private func makeRequestFunction(serviceName: String?, swaggerFile: SwaggerFile) -> String {
let servicePath = self.servicePath.split(separator: "/")
.map {
Expand Down Expand Up @@ -155,11 +151,11 @@ if let \(($0.swiftyName)) = \(headersName).\($0.swiftyName) {
.addNewlinesIfNonEmpty()

let responseTypes = self.responseTypes
.map { $0.print(apiName: serviceName ?? "") }
.map { $0.print(apiName: serviceName ?? "", errorType: returnType.failureType.toString(required: true)) }
.joined(separator: "\n")

return """
private func _\(functionName)(\(functionArguments)) async -> \(functionReturnType) {
private func _\(functionName)(\(functionArguments)) async throws(\(returnType.failureType.toString(required: true))) -> \(returnType.successType.toString(required: true)) {
let endpointUrl = baseUrlProvider().appendingPathComponent("\(servicePath)")

\(queries.count > 0 ? "var" : "let") urlComponents = URLComponents(url: endpointUrl, resolvingAgainstBaseURL: true)!
Expand All @@ -175,20 +171,24 @@ private func _\(functionName)(\(functionArguments)) async -> \(functionReturnTyp
do {
(data, response) = try await urlSession().\(urlSessionMethodName)
} catch {
return .failure(.requestFailed(error: error))
throw .requestFailed(error: error)
}

if let interceptor {
do {
try await interceptor.networkDidPerformRequest(urlRequest: request, urlResponse: response, data: data, error: nil)
try await interceptor.networkDidPerformRequest(
urlRequest: request,
urlResponse: response,
data: data,
error: nil
)
} catch {
return .failure(.requestFailed(error: error))
throw .requestFailed(error: error)
}
}

guard let httpResponse = response as? HTTPURLResponse else {
let error = NSError(domain: "\(serviceName ?? "Generic")", code: 0, userInfo: [NSLocalizedDescriptionKey: "Returned response object wasnt a HTTP URL Response as expected, but was instead a \\(String(describing: response))"])
return .failure(.requestFailed(error: error))
fatalError("The response must be a URL response")
}

let decoder = JSONDecoder()
Expand All @@ -199,7 +199,7 @@ private func _\(functionName)(\(functionArguments)) async -> \(functionReturnTyp
default:
let result = String(data: data, encoding: .utf8) ?? ""
let error = NSError(domain: "\(serviceName ?? "Generic")", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: result])
return .failure(.requestFailed(error: error))
throw .requestFailed(error: error)
}
}

Expand Down Expand Up @@ -238,10 +238,31 @@ private func _\(functionName)(\(functionArguments)) async -> \(functionReturnTyp
body += "\n"

body += """
\(accessControl) func \(functionName)(\(functionArguments)\(functionArguments.isEmpty ? "" : ", ")completion: @Sendable @escaping (\(functionReturnType)) -> Void = { _ in }) {
\(accessControl) func \(functionName)(\(functionArguments)\(functionArguments.isEmpty ? "" : ", ")completion: @Sendable @escaping (Result<\(returnType.successType.toString(required: true)), \(returnType.failureType.toString(required: true))>) -> Void = { _ in }) {
_Concurrency.Task {
let result = await _\(functionName)(\(parameters.map { "\($0.name.variableNameFormatted): \($0.name.variableNameFormatted)" }.joined(separator: ", ")))
completion(result)
do {

"""

if returnType.successType.toString(required: true) == "Void" {
body += """
try await _\(functionName)(\(parameters.map { "\($0.name.variableNameFormatted): \($0.name.variableNameFormatted)" }.joined(separator: ", ")))
completion(.success(()))

"""
} else {
body += """
let result = try await _\(functionName)(\(parameters.map { "\($0.name.variableNameFormatted): \($0.name.variableNameFormatted)" }.joined(separator: ", ")))
completion(.success(result))

"""
}

body += """
} catch let error {
let error = error as! \(returnType.failureType.toString(required: true))
completion(.failure(error))
}
}
}

Expand All @@ -256,11 +277,14 @@ private func _\(functionName)(\(functionArguments)) async -> \(functionReturnTyp

body += "\n"

if returnType.successType.toString(required: true) != "Void" {
body += "@discardableResult\n"
}

body +=
"""
@discardableResult
\(accessControl) func \(functionName)(\(functionArguments)) async -> \(functionReturnType) {
await _\(functionName)(\(parameters.map { "\($0.name.variableNameFormatted): \($0.name.variableNameFormatted)" }.joined(separator: ", ")))
\(accessControl) func \(functionName)(\(functionArguments)) async throws(\(returnType.failureType.toString(required: true))) -> \(returnType.successType.toString(required: true)) {
try await _\(functionName)(\(parameters.map { "\($0.name.variableNameFormatted): \($0.name.variableNameFormatted)" }.joined(separator: ", ")))
}

"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,16 @@ enum APIRequestResponseType {
}
}

func print(apiName: String) -> String {
func print(apiName: String, errorType: String) -> String {
let failed = !statusCode.isSuccess
let swiftResult = failed ? "failure" : "success"

let resultType: (String, Bool) -> String = { resultType, enumBased -> String in
let resultBlock = resultType.count == 0 ? "" : "(\(resultType))"
if failed {
if enumBased {
return ".backendError(error: .\(statusCode.name)\(resultBlock))"
return "\(errorType).backendError(error: .\(statusCode.name)\(resultBlock))"
} else {
return ".backendError(error: \(resultType))"
return "\(errorType).backendError(error: \(resultType))"
}
} else {
return enumBased ? ".\(statusCode.name)\(resultBlock)" : resultType
Expand All @@ -58,40 +57,41 @@ enum APIRequestResponseType {
return """
case \(statusCode.rawValue):
let result = String(data: data, encoding: .utf8) ?? ""
return .\(swiftResult)(\(resultType("result", resultIsEnum)))
\(failed ? "throw" : "return") \(resultType("result", resultIsEnum))
"""
case .object(let statusCode, let resultIsEnum, let responseType):
if responseType == "Data" {
return """
case \(statusCode.rawValue):
return .\(swiftResult)(data)
\(failed ? "throw" : "return") data
"""
} else {
return """
case \(statusCode.rawValue):
do {
let result = try decoder.decode(\(responseType.modelNamed).self, from: data)

return .\(swiftResult)(\(resultType("result", resultIsEnum)))
\(failed ? "throw" : "return") \(resultType("result", resultIsEnum))
} catch let error {
interceptor?.networkFailedToParseObject(urlRequest: request,
urlResponse: response,
data: data,
error: error)
return .failure(.requestFailed(error: error))
interceptor?.networkFailedToParseObject(
urlRequest: request,
urlResponse: response,
data: data,
error: error
)
throw \(errorType).requestFailed(error: error)
}
"""
}
case .void(let statusCode, let resultIsEnum):
if resultIsEnum {
return """
case \(statusCode.rawValue):
return .\(swiftResult)(\(resultType("", resultIsEnum)))
\(failed ? "throw" : "return") \(resultType("", resultIsEnum))
"""
} else {
return """
case \(statusCode.rawValue):
return .\(swiftResult)(\(resultType("()", resultIsEnum)))
\(failed ? "throw" : "return") \(resultType("()", resultIsEnum))
"""
}
case .int(let statusCode, _):
Expand All @@ -107,14 +107,14 @@ case \(statusCode.rawValue):
]
)

return .failure(.requestFailed(error: error))
throw \(errorType).requestFailed(error: error)
}
"""
case .double(let statusCode, _):
return """
case \(statusCode.rawValue):
if let stringValue = String(data: data, encoding: .utf8), let value = Double(stringValue) {
return .success(value)
return value
} else {
let error = NSError(domain: "\(apiName)",
code: 0,
Expand All @@ -123,14 +123,14 @@ case \(statusCode.rawValue):
]
)

return .failure(.requestFailed(error: error))
throw \(errorType).requestFailed(error: error)
}
"""
case .float(let statusCode, _):
return """
case \(statusCode.rawValue):
if let stringValue = String(data: data, encoding: .utf8), let value = Float(stringValue) {
return .success(value)
return value
} else {
let error = NSError(domain: "\(apiName)",
code: 0,
Expand All @@ -139,14 +139,14 @@ case \(statusCode.rawValue):
]
)

return .failure(.requestFailed(error: error))
throw \(errorType).requestFailed(error: error)
}
"""
case .boolean(let statusCode, _):
return """
case \(statusCode.rawValue):
if let stringValue = String(data: data, encoding: .utf8), let value = Bool(stringValue) {
return .success(value)
return value
} else {
let error = NSError(domain: "\(apiName)",
code: 0,
Expand All @@ -155,14 +155,14 @@ case \(statusCode.rawValue):
]
)

return .failure(.requestFailed(error: error))
throw \(errorType).requestFailed(error: error)
}
"""
case .int64(let statusCode, _):
return """
case \(statusCode.rawValue):
if let stringValue = String(data: data, encoding: .utf8), let value = Int64(stringValue) {
return .success(value)
return value
} else {
let error = NSError(domain: "\(apiName)",
code: 0,
Expand All @@ -171,18 +171,17 @@ case \(statusCode.rawValue):
]
)

return .failure(.requestFailed(error: error))
throw \(errorType).requestFailed(error: error)
}
"""
case .array(let statusCode, let resultIsEnum, let innerType):
return """
case \(statusCode.rawValue):
do {
let result = try decoder.decode([\(innerType)].self, from: data)

return .\(swiftResult)(\(resultType("result", resultIsEnum)))
\(failed ? "throw" : "return") \(resultType("result", resultIsEnum))
} catch let error {
return .failure(.requestFailed(error: error))
throw \(errorType).requestFailed(error: error))
}
"""
case .enumeration(let statusCode, let resultIsEnum, let responseType):
Expand All @@ -197,7 +196,7 @@ case \(statusCode.rawValue):
.trimmingCharacters(in: CharacterSet(charactersIn: "\\""))

let enumValue = \(responseType)(rawValue: cleanedStringValue)
return .\(swiftResult)(\(resultType("enumValue", resultIsEnum)))
\(failed ? "throw" : "return") \(resultType("enumValue", resultIsEnum))
} else {
let error = NSError(domain: "\(apiName)",
code: 0,
Expand All @@ -206,7 +205,7 @@ case \(statusCode.rawValue):
]
)

return .failure(.requestFailed(error: error))
throw \(errorType).requestFailed(error: error)
}
"""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ public struct RequestParameterFactory {

let returnType = ReturnType(
description: "The completion handler of the function returns as soon as the request completes",
typeName: .object(typeName: "Result<\(successTypeName), ServiceError<\(failureTypeName)>>")
successType: .object(typeName: successTypeName),
failureType: .object(typeName: "ServiceError<\(failureTypeName)>")
)

return (resolvedParameters, resolvedModelDefinitions, returnType)
Expand Down
57 changes: 25 additions & 32 deletions Sources/SwaggerSwiftCore/Generator/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,34 +46,24 @@ public struct Generator {

typealias APISpec = (APIDefinition, [ModelDefinition])

let apiSpecs: [APISpec] = try await withThrowingTaskGroup(of: APISpec?.self) { group in
var apiSpecs = [APISpec]()

for service in services {
group.addTask {
async let swagger = try await downloadSwagger(
githubToken: githubToken,
organisation: swaggerFile.organisation,
serviceName: service.key,
branch: service.value.branch ?? "master",
swaggerPath: service.value.path ?? swaggerFile.path
)

let apiSpec = try await APIFactory(
apiRequestFactory: apiRequestFactory,
modelTypeResolver: modelTypeResolver
).generate(for: swagger, withSwaggerFile: swaggerFile)

return apiSpec
}
}
var apiSpecs = [APISpec]()

for service in services {
do {
for try await spec in group {
if let spec = spec {
apiSpecs.append(spec)
}
}
async let swagger = try await downloadSwagger(
githubToken: githubToken,
organisation: swaggerFile.organisation,
serviceName: service.key,
branch: service.value.branch ?? "master",
swaggerPath: service.value.path ?? swaggerFile.path
)

let apiSpec = try await APIFactory(
apiRequestFactory: apiRequestFactory,
modelTypeResolver: modelTypeResolver
).generate(for: swagger, withSwaggerFile: swaggerFile)

apiSpecs.append(apiSpec)
} catch {
if let error = error as? FetchSwaggerError {
error.logError()
Expand All @@ -87,11 +77,7 @@ public struct Generator {
} else {
log("Failed to download Swagger: \(error.localizedDescription)", error: true)
}

throw NSError(domain: "", code: 0)
}

return apiSpecs
}

let destination = URL(fileURLWithPath: swaggerFilePath).deletingLastPathComponent().appendingPathComponent(swaggerFile.destination).path
Expand Down Expand Up @@ -216,7 +202,14 @@ public struct Generator {
return data
}

private func downloadSwagger(githubToken: String, organisation: String, serviceName: String, branch: String, swaggerPath: String, urlSession: URLSession = .shared) async throws -> Swagger {
private func downloadSwagger(
githubToken: String,
organisation: String,
serviceName: String,
branch: String,
swaggerPath: String,
urlSession: URLSession = .shared
) async throws -> Swagger {
let data = try await download(
githubToken: githubToken,
organisation: organisation,
Expand Down Expand Up @@ -369,7 +362,7 @@ public struct Generator {
extension String {
func write(toFile path: String, addHeader: Bool = true) throws {
if addHeader {
let file = "// Autogenerated with ❤️ by SwaggerSwift\n// Do not modify this file manually 🙅\n// swiftlint:disable all\n\n" + self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + "\n\n// swiftlint:enable all\n"
let file = "// Autogenerated with ❤️ by SwaggerSwift\n// Do not modify this file manually 🙅\n\n" + self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + "\n"
try file.write(toFile: path, atomically: true, encoding: .utf8)
} else {
try self.write(toFile: path, atomically: true, encoding: .utf8)
Expand Down
Loading
Loading