From 45466a03ba93ba8587e21e9283214ebe9aa9aa97 Mon Sep 17 00:00:00 2001 From: MadsBogeskov Date: Wed, 24 Jan 2024 10:49:16 +0100 Subject: [PATCH 1/3] :sparkles: Add header override --- .../API Factory/APIFactory.swift | 2 +- .../APIRequestFactory.swift | 54 +++++-- .../Models/APIRequest+Swiftable.swift | 35 ++++- .../RequestParameterFactory.swift | 141 ++++++++++++------ .../Extensions/Parameter.swift | 7 + .../Generator/Generator.swift | 18 ++- .../Generator/Models/GlobalHeadersModel.swift | 4 +- .../Models/SwaggerFile.swift | 15 +- 8 files changed, 195 insertions(+), 81 deletions(-) diff --git a/Sources/SwaggerSwiftCore/API Factory/APIFactory.swift b/Sources/SwaggerSwiftCore/API Factory/APIFactory.swift index b221c8b..c5f43a3 100644 --- a/Sources/SwaggerSwiftCore/API Factory/APIFactory.swift +++ b/Sources/SwaggerSwiftCore/API Factory/APIFactory.swift @@ -201,7 +201,7 @@ struct APIFactory { defaultValue: nil) ] - let hasGlobalHeaders = (swaggerFile.globalHeaders ?? []).count > 0 + let hasGlobalHeaders = swaggerFile.globalHeaders.count > 0 if hasGlobalHeaders { fields.append(APIDefinitionField(name: "headerProvider", diff --git a/Sources/SwaggerSwiftCore/API Request Factory/APIRequestFactory.swift b/Sources/SwaggerSwiftCore/API Request Factory/APIRequestFactory.swift index ce411bb..8282461 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/APIRequestFactory.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/APIRequestFactory.swift @@ -18,9 +18,11 @@ public struct APIRequestFactory { } func generateRequest(for operation: SwaggerSwiftML.Operation, httpMethod: HTTPMethod, servicePath: String, swagger: Swagger, swaggerFile: SwaggerFile, pathParameters: [Parameter]) throws -> (APIRequest, [ModelDefinition]) { - let functionName = resolveFunctionName(httpMethod: httpMethod.rawValue, - servicePath: servicePath, - operationId: operation.operationId) + let functionName = resolveFunctionName( + httpMethod: httpMethod.rawValue, + servicePath: servicePath, + operationId: operation.operationId + ) var inlineResponseModels = [ModelDefinition]() @@ -66,21 +68,36 @@ public struct APIRequestFactory { swagger.findParameter(node: $0) } + pathParameters - let headers = allParameters.compactMap { param -> APIRequestHeaderField? in - if case ParameterLocation.header = param.location { - return APIRequestHeaderField(headerName: param.name, - isRequired: param.required) - } else { - return nil + let requestSpecificHeaders = allParameters + .compactMap { param -> APIRequestHeaderField? in + if case ParameterLocation.header = param.location { + + let isRequired: Bool + if swaggerFile.globalHeaders.contains(param.name) { + isRequired = false // the header will be provided by global headers + } else { + isRequired = param.required + } + + return APIRequestHeaderField( + headerName: param.name, + isRequired: isRequired + ) + } else { + return nil + } } - } + + let headers = requestSpecificHeaders let queryItems = resolveQueries(parameters: allParameters, swagger: swagger) - let apiResponseTypes = apiResponseTypeFactory.make(forResponses: responses, - forHTTPMethod: httpMethod, - at: servicePath, - swagger: swagger) + let apiResponseTypes = apiResponseTypeFactory.make( + forResponses: responses, + forHTTPMethod: httpMethod, + at: servicePath, + swagger: swagger + ) inlineResponseModels.append(contentsOf: inlineModels) @@ -88,13 +105,18 @@ public struct APIRequestFactory { description: operation.description, functionName: functionName, parameters: functionParameters, - consumes: try consumeMimeType(forOperation: operation, swagger: swagger, httpMethod: httpMethod.rawValue, servicePath: servicePath), + consumes: try consumeMimeType( + forOperation: operation, + swagger: swagger, + httpMethod: httpMethod.rawValue, + servicePath: servicePath + ), isInternalOnly: operation.isInternalOnly, isDeprecated: operation.deprecated, httpMethod: httpMethod, servicePath: servicePath, queries: queryItems, - headers: headers, + headers: headers.unique(on: \.swiftyName), responseTypes: apiResponseTypes, returnType: returnType ) diff --git a/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift b/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift index 80ac533..d075c10 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/Models/APIRequest+Swiftable.swift @@ -5,7 +5,17 @@ extension APIRequest { /// The arguments of the function, e.g. "myValue: String, myOtherValue: Int" private var functionArguments: String { parameters.map { - "\($0.name.variableNameFormatted): \($0.typeName.toString(required: $0.required))\($0.required ? "" : " = nil")" + let paramName = $0.name.variableNameFormatted + let typeName = $0.typeName.toString(required: $0.required) + + let defaultValue: String + if !$0.required { + defaultValue = " = nil" + } else { + defaultValue = "" + } + + return "\(paramName): \(typeName)\(defaultValue)" }.joined(separator: ", ") } @@ -28,26 +38,35 @@ extension APIRequest { }.joined(separator: "/") var globalHeaders = [String]() - if let globalHeaderFields = swaggerFile.globalHeaders, globalHeaderFields.count > 0 { + if swaggerFile.globalHeaders.count > 0 { globalHeaders.append("let globalHeaders = self.headerProvider()") globalHeaders.append("globalHeaders.add(to: &request)") } - var headerStatements: [String] = headers + let uniquelyGlobalHeaders = swaggerFile.globalHeaders.filter { globalHeaderName in headers.contains(where: { $0.fullHeaderName == globalHeaderName }) == false } + + let allHeaders = headers + uniquelyGlobalHeaders.map { + APIRequestHeaderField(headerName: $0, isRequired: false) // not required as they are default from the global header provider + } + + let headersName = headers.filter { $0.isRequired }.count > 0 ? "headers" : "headers?" + + let setHeaderValues: [String] = allHeaders .sorted(by: { $0.swiftyName < $1.swiftyName }) - .filter { !(swaggerFile.globalHeaders ?? []).map { $0.lowercased() }.contains($0.fullHeaderName.lowercased()) } .map { if $0.isRequired { - return "request.addValue(headers.\($0.swiftyName), forHTTPHeaderField: \"\($0.fullHeaderName)\")" + return "request.addValue(\(headersName).\($0.swiftyName), forHTTPHeaderField: \"\($0.fullHeaderName)\")" } else { return """ -if let \(($0.swiftyName)) = headers.\($0.swiftyName) { - request.addValue(\($0.swiftyName), forHTTPHeaderField: \"\($0.fullHeaderName)\") - } +if let \(($0.swiftyName)) = \(headersName).\($0.swiftyName) { + request.addValue(\($0.swiftyName), forHTTPHeaderField: \"\($0.fullHeaderName)\") +} """ } } + var headerStatements = setHeaderValues + var bodyInjection: String = "" if let body = parameters.first(where: { $0.in == .body }) { bodyInjection += """ diff --git a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift index 1d4bcd7..4bbb751 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift @@ -30,11 +30,13 @@ public struct RequestParameterFactory { .map { $0.capitalizingFirstLetter() } .joined() - if let (headerParameter, headerModels) = try resolveInputHeadersToApi(parameters: parameters, - typePrefix: typeName, - isInternalOnly: operation.isInternalOnly, - swagger: swagger, - swaggerFile: swaggerFile) { + if let (headerParameter, headerModels) = try resolveInputHeadersToApi( + parameters: parameters, + typePrefix: typeName, + isInternalOnly: operation.isInternalOnly, + swagger: swagger, + swaggerFile: swaggerFile + ) { resolvedParameters.append(headerParameter) resolvedModelDefinitions.append(contentsOf: headerModels) } @@ -113,60 +115,91 @@ public struct RequestParameterFactory { } private func resolveInputHeadersToApi(parameters: [SwaggerSwiftML.Parameter], typePrefix: String, isInternalOnly: Bool, swagger: Swagger, swaggerFile: SwaggerFile) throws -> (FunctionParameter, [ModelDefinition])? { - let headerParameters = parameters.parameters(of: .header) - - let globalHeaderFieldNames = (swaggerFile.globalHeaders ?? []).map(convertApiHeader) + let requestSpecificParameters = parameters.compactMap { + if case let .header(type) = $0.location { + return HeaderParameter( + type: type, + name: $0.name, + required: $0.required, + description: $0.description + ) + } else { + return nil + } + }.unique(on: { $0.name }) // this is necessary in the case where copies of the same header are sent in at the same request + + let globalHeaderParams = swaggerFile.globalHeaders + .filter { header in requestSpecificParameters.contains(where: { $0.name.lowercased() == header.lowercased() }) == false } + .map { + HeaderParameter( + type: .string( + format: nil, + enumValues: nil, + maxLength: nil, + minLength: nil, + pattern: nil + ), + name: $0, + required: true, + description: nil + ) + } var modelDefinitions = [ModelDefinition]() - - let headerFields: [ModelField] = try headerParameters.compactMap { param, type, _ in - let (type, inlineModelDefinitions) = try type.toType( + let requestSpecificHeaderFields: [ModelField] = try requestSpecificParameters.map { headerParam in + let (type, inlineModelDefinitions) = try headerParam.type.toType( typePrefix: typePrefix, - description: param.description, + description: headerParam.description, swagger: swagger ) - let name = convertApiHeader(param.name) + modelDefinitions.append(contentsOf: inlineModelDefinitions) - // we should not add fields for the global headers - if globalHeaderFieldNames.contains(name) { - return nil + let isRequired: Bool + if swaggerFile.globalHeaders.contains(where: { $0.lowercased() == headerParam.name.lowercased() }) { + isRequired = false // this is overriden by the global headers so it will be set by that, and is therefor not required here + } else { + isRequired = headerParam.required } - modelDefinitions.append(contentsOf: inlineModelDefinitions) + return ModelField( + description: nil, + type: type, + name: convertApiHeader(headerParam.name), + isRequired: isRequired + ) + } - return ModelField(description: nil, - type: type, - name: name, - isRequired: param.required) - }.sorted(by: { $0.argumentLabel < $1.argumentLabel }) - - if headerFields.count > 0 { - let typeName = "\(typePrefix)Headers" - - let model = Model(description: "A collection of the header fields required for the request", - typeName: typeName, - fields: headerFields, - inheritsFrom: [], - isInternalOnly: isInternalOnly, - embeddedDefinitions: [], - isCodable: false) - - if model.fields.count > 0 { - let headerModelDefinition = ModelDefinition.object(model) - modelDefinitions.append(headerModelDefinition) - let functionParameter = FunctionParameter(description: nil, - name: "headers", - typeName: .object(typeName: typeName), - required: true, - in: .headers, - isEnum: false) - - return (functionParameter, modelDefinitions) + let globalHeaderFields = globalHeaderParams + .map { + ModelField(description: nil, type: .string(defaultValue: nil), name: convertApiHeader($0.name), isRequired: false) } - } - return nil + let allHeaderFields = (requestSpecificHeaderFields + globalHeaderFields) + .sorted(by: { $0.argumentLabel < $1.argumentLabel }) + + guard allHeaderFields.count > 0 else { return nil } + + let model = Model( + description: "A collection of the header fields required for the request", + typeName: "\(typePrefix)Headers", + fields: allHeaderFields, + isInternalOnly: isInternalOnly, + isCodable: false + ) + + modelDefinitions.append(ModelDefinition.object(model)) + + let functionParameter = FunctionParameter( + description: nil, + name: "headers", + typeName: .object(typeName: model.typeName), + required: requestSpecificHeaderFields.count > 0 && requestSpecificHeaderFields.contains(where: { $0.isRequired }), + in: .headers, + isEnum: false + ) + + return (functionParameter, modelDefinitions) } private func resolvePathParameters(parameters: [SwaggerSwiftML.Parameter], typePrefix: String, swagger: Swagger) throws -> ([FunctionParameter], [ModelDefinition]) { @@ -357,3 +390,17 @@ public struct RequestParameterFactory { return (functionParameters, modelDefinitions) } } + +extension Sequence { + func unique(on block: (Iterator.Element) -> T) -> [Self.Element] { + var result = [Iterator.Element]() + + for item in self { + if result.contains(where: { r in block(item) == block(r) }) == false { + result.append(item) + } + } + + return result + } +} diff --git a/Sources/SwaggerSwiftCore/Extensions/Parameter.swift b/Sources/SwaggerSwiftCore/Extensions/Parameter.swift index 9702423..18e001f 100644 --- a/Sources/SwaggerSwiftCore/Extensions/Parameter.swift +++ b/Sources/SwaggerSwiftCore/Extensions/Parameter.swift @@ -9,6 +9,13 @@ enum ParamLocation { case body } +struct HeaderParameter { + let type: ParameterType + let name: String + let required: Bool + let description: String? +} + extension Sequence where Element == SwaggerSwiftML.Parameter { /// Convenience function that searches a set of SwaggerSwiftML.Parameter's to find those that are used in a specific location, based on `ParamLocation` /// - Parameter type: the type of api parameter that is needed diff --git a/Sources/SwaggerSwiftCore/Generator/Generator.swift b/Sources/SwaggerSwiftCore/Generator/Generator.swift index 511beaa..3b8b032 100644 --- a/Sources/SwaggerSwiftCore/Generator/Generator.swift +++ b/Sources/SwaggerSwiftCore/Generator/Generator.swift @@ -97,7 +97,7 @@ public struct Generator { log("Creating Swift Project at \(destination)") - let globalHeadersModel = GlobalHeadersModel(headerFields: swaggerFile.globalHeaders ?? []) + let globalHeadersModel = GlobalHeadersModel(headerFields: swaggerFile.globalHeaders) let commonLibraryName = "\(swaggerFile.projectName)Shared" if swaggerFile.createSwiftPackage { @@ -226,7 +226,17 @@ public struct Generator { } } - private func write(apiDefinition: APIDefinition, modelDefinitions: [ModelDefinition], swaggerFile: SwaggerFile, rootDirectory: String, commonLibraryName: String?, accessControl: APIAccessControl, globalHeadersModel: GlobalHeadersModel, fileManager: FileManager, dummyMode: Bool) throws { + private func write( + apiDefinition: APIDefinition, + modelDefinitions: [ModelDefinition], + swaggerFile: SwaggerFile, + rootDirectory: String, + commonLibraryName: String?, + accessControl: APIAccessControl, + globalHeadersModel: GlobalHeadersModel, + fileManager: FileManager, + dummyMode: Bool + ) throws { log("Parsing contents of Swagger: \(apiDefinition.serviceName)") let apiDirectory = rootDirectory + "/" + apiDefinition.serviceName @@ -300,8 +310,8 @@ public struct Generator { try globalHeadersDefinitions.write(toFile: globalHeaderExtensionsPath) } - if let globalHeaderFields = swaggerFile.globalHeaders { - let globalHeadersModel = GlobalHeadersModel(headerFields: globalHeaderFields) + if swaggerFile.globalHeaders.count > 0 { + let globalHeadersModel = GlobalHeadersModel(headerFields: swaggerFile.globalHeaders) let globalHeadersFileContents = globalHeadersModel.toSwift(swaggerFile: swaggerFile, accessControl: accessControl) try globalHeadersFileContents.write(toFile: "\(targetPath)/GlobalHeaders.swift") } diff --git a/Sources/SwaggerSwiftCore/Generator/Models/GlobalHeadersModel.swift b/Sources/SwaggerSwiftCore/Generator/Models/GlobalHeadersModel.swift index 8aa888a..2059471 100644 --- a/Sources/SwaggerSwiftCore/Generator/Models/GlobalHeadersModel.swift +++ b/Sources/SwaggerSwiftCore/Generator/Models/GlobalHeadersModel.swift @@ -54,7 +54,7 @@ struct GlobalHeadersModel { return model } - func addToRequestFunction() -> String { + private func addToRequestFunction() -> String { let fields = headerFields.map { APIRequestHeaderField( headerName: $0, @@ -83,7 +83,7 @@ if let \($0.swiftyName) = \($0.swiftyName) { return function } - func asDictionaryFunction() -> String { + private func asDictionaryFunction() -> String { let fields = headerFields.map { APIRequestHeaderField( headerName: $0, diff --git a/Sources/SwaggerSwiftCore/SwaggerFile Parser/Models/SwaggerFile.swift b/Sources/SwaggerSwiftCore/SwaggerFile Parser/Models/SwaggerFile.swift index 86a3d76..dac5216 100644 --- a/Sources/SwaggerSwiftCore/SwaggerFile Parser/Models/SwaggerFile.swift +++ b/Sources/SwaggerSwiftCore/SwaggerFile Parser/Models/SwaggerFile.swift @@ -8,7 +8,7 @@ struct SwaggerFile: Decodable { let path: String let organisation: String let services: [String: Service] - let globalHeaders: [String]? + let globalHeaders: [String] let createSwiftPackage: Bool /// What is the name of the directory the files will be placed in? This is also the name as the project in the Swift Package (if created) let projectName: String @@ -27,7 +27,16 @@ struct SwaggerFile: Decodable { case destination } - init(path: String, organisation: String, services: [String: Service], globalHeaders: [String]?, createSwiftPackage: Bool = true, accessControl: APIAccessControl = .public, destination: String = "./", projectName: String = "Services") { + init( + path: String, + organisation: String, + services: [String: Service], + globalHeaders: [String] = [], + createSwiftPackage: Bool = true, + accessControl: APIAccessControl = .public, + destination: String = "./", + projectName: String = "Services" + ) { self.path = path self.organisation = organisation self.services = services @@ -43,7 +52,7 @@ struct SwaggerFile: Decodable { self.path = try container.decode(String.self, forKey: .path) self.organisation = try container.decode(String.self, forKey: .organisation) self.services = try container.decode([String: Service].self, forKey: .services) - self.globalHeaders = try container.decodeIfPresent([String].self, forKey: .globalHeaders) + self.globalHeaders = (try container.decodeIfPresent([String].self, forKey: .globalHeaders)) ?? [] self.accessControl = try container.decodeIfPresent(APIAccessControl.self, forKey: .accessControl) ?? .public self.createSwiftPackage = try container.decodeIfPresent(Bool.self, forKey: .createSwiftPackage) ?? true self.projectName = try container.decodeIfPresent(String.self, forKey: .projectName) ?? "Services" From 5b7677ef665410ad6c468de7c721db7d3e03b1e0 Mon Sep 17 00:00:00 2001 From: MadsBogeskov Date: Wed, 24 Jan 2024 10:54:26 +0100 Subject: [PATCH 2/3] :truck: Update SwaggerSwift --- .../API Request Factory/RequestParameterFactory.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift index 4bbb751..0465613 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift @@ -184,7 +184,9 @@ public struct RequestParameterFactory { description: "A collection of the header fields required for the request", typeName: "\(typePrefix)Headers", fields: allHeaderFields, + inheritsFrom: [], isInternalOnly: isInternalOnly, + embeddedDefinitions: [],d s isCodable: false ) From 6d47a0f11a4f12a54f988723f1ab24397cec1099 Mon Sep 17 00:00:00 2001 From: MadsBogeskov Date: Wed, 24 Jan 2024 10:54:56 +0100 Subject: [PATCH 3/3] :truck: Update SwaggerSwift --- .../API Request Factory/RequestParameterFactory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift index 0465613..f645ec7 100644 --- a/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift +++ b/Sources/SwaggerSwiftCore/API Request Factory/RequestParameterFactory.swift @@ -186,7 +186,7 @@ public struct RequestParameterFactory { fields: allHeaderFields, inheritsFrom: [], isInternalOnly: isInternalOnly, - embeddedDefinitions: [],d s + embeddedDefinitions: [], isCodable: false )