diff --git a/Sources/OpenAPIRuntime/Conversion/ServerVariable.swift b/Sources/OpenAPIRuntime/Conversion/ServerVariable.swift new file mode 100644 index 00000000..4a22853c --- /dev/null +++ b/Sources/OpenAPIRuntime/Conversion/ServerVariable.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension URL { + /// Returns a validated server URL created from the URL template, or + /// throws an error. + /// - Parameter + /// - string: A URL string. + /// - variables: A map of variable values to substitute into the URL + /// template. + /// - Throws: If the provided string doesn't convert to URL. + @_spi(Generated) + public init( + validatingOpenAPIServerURL string: String, + variables: [ServerVariable] + ) throws { + var urlString = string + for variable in variables { + let name = variable.name + let value = variable.value + if let allowedValues = variable.allowedValues { + guard allowedValues.contains(value) else { + throw RuntimeError.invalidServerVariableValue( + name: name, + value: value, + allowedValues: allowedValues + ) + } + } + urlString = urlString.replacingOccurrences(of: "{\(name)}", with: value) + } + guard let url = Self(string: urlString) else { + throw RuntimeError.invalidServerURL(urlString) + } + self = url + } +} + +/// A variable of a server URL template in the OpenAPI document. +@_spi(Generated) +public struct ServerVariable: Sendable, Hashable { + + /// The name of the variable. + public var name: String + + /// The value to be substituted into the URL template. + public var value: String + + /// A list of allowed values from the OpenAPI document. + /// + /// Nil means that any value is allowed. + public var allowedValues: [String]? + + /// Creates a new server variable. + /// - Parameters: + /// - name: The name of the variable. + /// - value: The value to be substituted into the URL template. + /// - allowedValues: A list of allowed values from the OpenAPI document. + public init(name: String, value: String, allowedValues: [String]? = nil) { + self.name = name + self.value = value + self.allowedValues = allowedValues + } +} diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 74eb1ef3..f7b4e93f 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -19,6 +19,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Miscs case invalidServerURL(String) + case invalidServerVariableValue(name: String, value: String, allowedValues: [String]) case invalidExpectedContentType(String) case invalidHeaderFieldName(String) case invalidBase64String(String) @@ -70,6 +71,9 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret switch self { case .invalidServerURL(let string): return "Invalid server URL: \(string)" + case .invalidServerVariableValue(name: let name, value: let value, allowedValues: let allowedValues): + return + "Invalid server variable named: '\(name)', which has the value: '\(value)', but the only allowed values are: \(allowedValues.map { "'\($0)'" }.joined(separator: ", "))" case .invalidExpectedContentType(let string): return "Invalid expected content type: '\(string)'" case .invalidHeaderFieldName(let name): diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_ServerVariable.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_ServerVariable.swift new file mode 100644 index 00000000..0b134b2e --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_ServerVariable.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime + +final class Test_ServerVariable: Test_Runtime { + + func testOnlyConstants() throws { + XCTAssertEqual( + try URL( + validatingOpenAPIServerURL: "https://example.com", + variables: [] + ) + .absoluteString, + "https://example.com" + ) + XCTAssertEqual( + try URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + .absoluteString, + "https://example.com/api" + ) + XCTAssertEqual( + try URL( + validatingOpenAPIServerURL: "/api", + variables: [] + ) + .absoluteString, + "/api" + ) + } + + func testVariables() throws { + XCTAssertEqual( + try URL( + validatingOpenAPIServerURL: "https://{subdomain}.example.com:{port}/{baseURL}", + variables: [ + .init(name: "subdomain", value: "test"), + .init(name: "port", value: "443", allowedValues: ["443", "8443"]), + .init(name: "baseURL", value: "v1"), + ] + ) + .absoluteString, + "https://test.example.com:443/v1" + ) + XCTAssertThrowsError( + try URL( + validatingOpenAPIServerURL: "https://{subdomain}.example.com:{port}/{baseURL}", + variables: [ + .init(name: "subdomain", value: "test"), + .init(name: "port", value: "foo", allowedValues: ["443", "8443"]), + .init(name: "baseURL", value: "v1"), + ] + ), + "Should have thrown an error", + { error in + guard + case let .invalidServerVariableValue(name: name, value: value, allowedValues: allowedValues) = error + as? RuntimeError + else { + XCTFail("Expected error, but not this: \(error)") + return + } + XCTAssertEqual(name, "port") + XCTAssertEqual(value, "foo") + XCTAssertEqual(allowedValues, ["443", "8443"]) + } + ) + } +}