From 56ea2c435435934004b2a1b58b507a5208bd0aea Mon Sep 17 00:00:00 2001 From: MadsBogeskov Date: Thu, 21 Nov 2024 09:04:38 +0100 Subject: [PATCH] :sparkles: Typed Throws --- Package.resolved | 9 --- Package.swift | 5 +- .../Models/APIRequest+Swiftable.swift | 60 +++++++++++++------ .../Models/APIRequestResponseType.swift | 57 +++++++++--------- .../RequestParameterFactory.swift | 3 +- .../Generator/Generator.swift | 29 +++++---- .../SwaggerSwiftCore/Models/ReturnType.swift | 3 +- .../DateDecodingStrategyFile.swift | 2 +- 8 files changed, 96 insertions(+), 72 deletions(-) diff --git a/Package.resolved b/Package.resolved index c6b8437..8c192d0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "swaggerswiftml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/lunarway/SwaggerSwiftML", - "state" : { - "revision" : "a973b5151c225defbe13663108d4b11c07cd3064", - "version" : "1.0.19" - } - }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index b185ce5..1f0f863 100644 --- a/Package.swift +++ b/Package.swift @@ -8,8 +8,9 @@ let package = Package( platforms: [.macOS(.v12)], products: [.executable(name: "swaggerswift", targets: ["SwaggerSwift"])], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.1.1")), - .package(url: "https://github.com/lunarway/SwaggerSwiftML", from: "1.0.19") + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.1"), +// .package(url: "https://github.com/lunarway/SwaggerSwiftML", from: "1.0.19") + .package(path: "../swaggerswiftml") ], targets: [ .executableTarget( diff --git a/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift b/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift index 5f5d102..82eb142 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift @@ -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 { @@ -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)! @@ -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() @@ -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) } } @@ -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)) + } } } @@ -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: ", "))) } """ diff --git a/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequestResponseType.swift b/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequestResponseType.swift index 33a1a60..e154ec9 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequestResponseType.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequestResponseType.swift @@ -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 @@ -58,27 +57,28 @@ 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) } """ } @@ -86,12 +86,12 @@ case \(statusCode.rawValue): 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, _): @@ -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, @@ -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, @@ -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, @@ -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, @@ -171,7 +171,7 @@ case \(statusCode.rawValue): ] ) - return .failure(.requestFailed(error: error)) + throw \(errorType).requestFailed(error: error) } """ case .array(let statusCode, let resultIsEnum, let innerType): @@ -179,10 +179,9 @@ case \(statusCode.rawValue): 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): @@ -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, @@ -206,7 +205,7 @@ case \(statusCode.rawValue): ] ) - return .failure(.requestFailed(error: error)) + throw \(errorType).requestFailed(error: error) } """ } diff --git a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift index e6a8cfb..9a14110 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift @@ -91,7 +91,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) diff --git a/Sources/SwaggerSwiftCore/Generator/Generator.swift b/Sources/SwaggerSwiftCore/Generator/Generator.swift index 13cbc6a..68c6517 100644 --- a/Sources/SwaggerSwiftCore/Generator/Generator.swift +++ b/Sources/SwaggerSwiftCore/Generator/Generator.swift @@ -46,7 +46,7 @@ public struct Generator { typealias APISpec = (APIDefinition, [ModelDefinition]) - let apiSpecs: [APISpec] = try await withThrowingTaskGroup(of: APISpec?.self) { group in + let apiSpecs: [APISpec] = await withThrowingTaskGroup(of: APISpec?.self) { group in var apiSpecs = [APISpec]() for service in services { @@ -87,8 +87,6 @@ public struct Generator { } else { log("Failed to download Swagger: \(error.localizedDescription)", error: true) } - - throw NSError(domain: "", code: 0) } return apiSpecs @@ -216,13 +214,22 @@ public struct Generator { return data } - 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, - serviceName: serviceName, - branch: branch, - swaggerPath: swaggerPath, - urlSession: urlSession) + 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, + serviceName: serviceName, + branch: branch, + swaggerPath: swaggerPath, + urlSession: urlSession + ) guard let stringValue = String(data: data, encoding: .utf8) else { throw FetchSwaggerError.invalidResponse(serviceName: serviceName) @@ -367,7 +374,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) diff --git a/Sources/SwaggerSwiftCore/Models/ReturnType.swift b/Sources/SwaggerSwiftCore/Models/ReturnType.swift index 7d31653..0d0c883 100644 --- a/Sources/SwaggerSwiftCore/Models/ReturnType.swift +++ b/Sources/SwaggerSwiftCore/Models/ReturnType.swift @@ -2,5 +2,6 @@ import Foundation struct ReturnType { let description: String - let typeName: TypeType + let successType: TypeType + let failureType: TypeType } diff --git a/Sources/SwaggerSwiftCore/Static Files/DateDecodingStrategyFile.swift b/Sources/SwaggerSwiftCore/Static Files/DateDecodingStrategyFile.swift index 763f3ca..398f4ad 100644 --- a/Sources/SwaggerSwiftCore/Static Files/DateDecodingStrategyFile.swift +++ b/Sources/SwaggerSwiftCore/Static Files/DateDecodingStrategyFile.swift @@ -1,7 +1,7 @@ let dateDecodingStrategy = """ import Foundation -@Sendable internal func dateDecodingStrategy(_ decoder: Decoder) throws -> Date { +@Sendable package func dateDecodingStrategy(_ decoder: Decoder) throws -> Date { let container = try decoder.singleValueContainer() let stringValue = try container.decode(String.self)