Skip to content

Commit 51bdb07

Browse files
authored
[Runtime] Generate server variables (#64)
[Runtime] Generate server variables ### Motivation The runtime side of apple/swift-openapi-generator#24. ### Modifications Added an SPI `ServerVariable` type and a variant of the `URL.init(validatingOpenAPIServerURL:variables:)` method, which takes the template and variables and returns a fully formed concrete URL. ### Result Unblocked the generator half of supporting server variables. ### Test Plan Added unit tests. Reviewed by: glbrntt Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #64
1 parent 82edf0e commit 51bdb07

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
17+
extension URL {
18+
/// Returns a validated server URL created from the URL template, or
19+
/// throws an error.
20+
/// - Parameter
21+
/// - string: A URL string.
22+
/// - variables: A map of variable values to substitute into the URL
23+
/// template.
24+
/// - Throws: If the provided string doesn't convert to URL.
25+
@_spi(Generated)
26+
public init(
27+
validatingOpenAPIServerURL string: String,
28+
variables: [ServerVariable]
29+
) throws {
30+
var urlString = string
31+
for variable in variables {
32+
let name = variable.name
33+
let value = variable.value
34+
if let allowedValues = variable.allowedValues {
35+
guard allowedValues.contains(value) else {
36+
throw RuntimeError.invalidServerVariableValue(
37+
name: name,
38+
value: value,
39+
allowedValues: allowedValues
40+
)
41+
}
42+
}
43+
urlString = urlString.replacingOccurrences(of: "{\(name)}", with: value)
44+
}
45+
guard let url = Self(string: urlString) else {
46+
throw RuntimeError.invalidServerURL(urlString)
47+
}
48+
self = url
49+
}
50+
}
51+
52+
/// A variable of a server URL template in the OpenAPI document.
53+
@_spi(Generated)
54+
public struct ServerVariable: Sendable, Hashable {
55+
56+
/// The name of the variable.
57+
public var name: String
58+
59+
/// The value to be substituted into the URL template.
60+
public var value: String
61+
62+
/// A list of allowed values from the OpenAPI document.
63+
///
64+
/// Nil means that any value is allowed.
65+
public var allowedValues: [String]?
66+
67+
/// Creates a new server variable.
68+
/// - Parameters:
69+
/// - name: The name of the variable.
70+
/// - value: The value to be substituted into the URL template.
71+
/// - allowedValues: A list of allowed values from the OpenAPI document.
72+
public init(name: String, value: String, allowedValues: [String]? = nil) {
73+
self.name = name
74+
self.value = value
75+
self.allowedValues = allowedValues
76+
}
77+
}

Sources/OpenAPIRuntime/Errors/RuntimeError.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
1919

2020
// Miscs
2121
case invalidServerURL(String)
22+
case invalidServerVariableValue(name: String, value: String, allowedValues: [String])
2223
case invalidExpectedContentType(String)
2324
case invalidHeaderFieldName(String)
2425
case invalidBase64String(String)
@@ -70,6 +71,9 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
7071
switch self {
7172
case .invalidServerURL(let string):
7273
return "Invalid server URL: \(string)"
74+
case .invalidServerVariableValue(name: let name, value: let value, allowedValues: let allowedValues):
75+
return
76+
"Invalid server variable named: '\(name)', which has the value: '\(value)', but the only allowed values are: \(allowedValues.map { "'\($0)'" }.joined(separator: ", "))"
7377
case .invalidExpectedContentType(let string):
7478
return "Invalid expected content type: '\(string)'"
7579
case .invalidHeaderFieldName(let name):
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import XCTest
15+
@_spi(Generated) @testable import OpenAPIRuntime
16+
17+
final class Test_ServerVariable: Test_Runtime {
18+
19+
func testOnlyConstants() throws {
20+
XCTAssertEqual(
21+
try URL(
22+
validatingOpenAPIServerURL: "https://example.com",
23+
variables: []
24+
)
25+
.absoluteString,
26+
"https://example.com"
27+
)
28+
XCTAssertEqual(
29+
try URL(
30+
validatingOpenAPIServerURL: "https://example.com/api",
31+
variables: []
32+
)
33+
.absoluteString,
34+
"https://example.com/api"
35+
)
36+
XCTAssertEqual(
37+
try URL(
38+
validatingOpenAPIServerURL: "/api",
39+
variables: []
40+
)
41+
.absoluteString,
42+
"/api"
43+
)
44+
}
45+
46+
func testVariables() throws {
47+
XCTAssertEqual(
48+
try URL(
49+
validatingOpenAPIServerURL: "https://{subdomain}.example.com:{port}/{baseURL}",
50+
variables: [
51+
.init(name: "subdomain", value: "test"),
52+
.init(name: "port", value: "443", allowedValues: ["443", "8443"]),
53+
.init(name: "baseURL", value: "v1"),
54+
]
55+
)
56+
.absoluteString,
57+
"https://test.example.com:443/v1"
58+
)
59+
XCTAssertThrowsError(
60+
try URL(
61+
validatingOpenAPIServerURL: "https://{subdomain}.example.com:{port}/{baseURL}",
62+
variables: [
63+
.init(name: "subdomain", value: "test"),
64+
.init(name: "port", value: "foo", allowedValues: ["443", "8443"]),
65+
.init(name: "baseURL", value: "v1"),
66+
]
67+
),
68+
"Should have thrown an error",
69+
{ error in
70+
guard
71+
case let .invalidServerVariableValue(name: name, value: value, allowedValues: allowedValues) = error
72+
as? RuntimeError
73+
else {
74+
XCTFail("Expected error, but not this: \(error)")
75+
return
76+
}
77+
XCTAssertEqual(name, "port")
78+
XCTAssertEqual(value, "foo")
79+
XCTAssertEqual(allowedValues, ["443", "8443"])
80+
}
81+
)
82+
}
83+
}

0 commit comments

Comments
 (0)