Skip to content

Commit 50cf5ac

Browse files
authored
[Bug] Fix mixed path components (#491)
### Motivation Fixes #490. ### Modifications Refactors the logic for composing path parameters on the client to support mixed components, which contain both a constant and a variable part (or two variable parts, etc). ### Result Support more OpenAPI docs. ### Test Plan Expanded a snippet test to cover this.
1 parent 9ba2457 commit 50cf5ac

File tree

2 files changed

+52
-16
lines changed

2 files changed

+52
-16
lines changed

Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414
import OpenAPIKit
15+
import Foundation
1516

1617
/// A wrapper of an OpenAPI operation that includes the information
1718
/// about the parent containers of the operation, such as its path
@@ -244,6 +245,11 @@ extension OperationDescription {
244245
)
245246
}
246247

248+
/// The regular expression for parsing subcomponents of path components.
249+
///
250+
/// Either a parameter `{foo}` or a constant value `foo`.
251+
private static let pathParameterRegex = try! NSRegularExpression(pattern: #"(\{[a-zA-Z0-9_]+\})|([^{}]+)"#)
252+
247253
/// Returns a string that contains the template to be generated for
248254
/// the client that fills in path parameters, and an array expression
249255
/// with the parameter values.
@@ -257,19 +263,35 @@ extension OperationDescription {
257263
// in which the parameters are used.
258264
var newComponents: [String] = []
259265
for component in path.components {
260-
guard component.hasPrefix("{") && component.hasSuffix("}") else {
261-
newComponents.append(component)
262-
continue
263-
}
264-
let componentName = String(component.dropFirst().dropLast())
265-
guard pathParameterNames.contains(componentName) else {
266-
throw GenericError(
267-
message:
268-
"Parameter '\(componentName)' used in the path '\(self.path.rawValue)', but not found in the defined list of path parameters."
269-
)
266+
let matches = Self.pathParameterRegex.matches(
267+
in: component,
268+
options: [],
269+
range: NSRange(location: 0, length: component.utf16.count)
270+
)
271+
var subcomponents: [String] = []
272+
for match in matches {
273+
for i in 1..<match.numberOfRanges {
274+
let range = match.range(at: i)
275+
guard range.location != NSNotFound, let swiftRange = Range(range, in: component) else {
276+
continue
277+
}
278+
let value = component[swiftRange]
279+
if value.hasPrefix("{") && value.hasSuffix("}") {
280+
let componentName = String(value.dropFirst().dropLast())
281+
guard pathParameterNames.contains(componentName) else {
282+
throw GenericError(
283+
message:
284+
"Parameter '\(componentName)' used in the path '\(self.path.rawValue)', but not found in the defined list of path parameters."
285+
)
286+
}
287+
orderedPathParameters.append(componentName)
288+
subcomponents.append("{}")
289+
} else {
290+
subcomponents.append(String(value))
291+
}
292+
}
270293
}
271-
orderedPathParameters.append(componentName)
272-
newComponents.append("{}")
294+
newComponents.append(subcomponents.joined())
273295
}
274296
let newPath = OpenAPI.Path(newComponents, trailingSlash: path.trailingSlash)
275297
let names: [Expression] = orderedPathParameters.map { param in

Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,7 +2349,7 @@ final class SnippetBasedReferenceTests: XCTestCase {
23492349
func testRequestWithPathParams() throws {
23502350
try self.assertRequestInTypesClientServerTranslation(
23512351
"""
2352-
/foo/a/{a}/b/{b}:
2352+
/foo/a/{a}/b/{b}/c{num}:
23532353
get:
23542354
parameters:
23552355
- name: b
@@ -2362,6 +2362,11 @@ final class SnippetBasedReferenceTests: XCTestCase {
23622362
required: true
23632363
schema:
23642364
type: string
2365+
- name: num
2366+
in: path
2367+
required: true
2368+
schema:
2369+
type: integer
23652370
operationId: getFoo
23662371
responses:
23672372
default:
@@ -2372,12 +2377,15 @@ final class SnippetBasedReferenceTests: XCTestCase {
23722377
public struct Path: Sendable, Hashable {
23732378
public var b: Swift.String
23742379
public var a: Swift.String
2380+
public var num: Swift.Int
23752381
public init(
23762382
b: Swift.String,
2377-
a: Swift.String
2383+
a: Swift.String,
2384+
num: Swift.Int
23782385
) {
23792386
self.b = b
23802387
self.a = a
2388+
self.num = num
23812389
}
23822390
}
23832391
public var path: Operations.getFoo.Input.Path
@@ -2389,10 +2397,11 @@ final class SnippetBasedReferenceTests: XCTestCase {
23892397
client: """
23902398
{ input in
23912399
let path = try converter.renderedPath(
2392-
template: "/foo/a/{}/b/{}",
2400+
template: "/foo/a/{}/b/{}/c{}",
23932401
parameters: [
23942402
input.path.a,
2395-
input.path.b
2403+
input.path.b,
2404+
input.path.num
23962405
]
23972406
)
23982407
var request: HTTPTypes.HTTPRequest = .init(
@@ -2415,6 +2424,11 @@ final class SnippetBasedReferenceTests: XCTestCase {
24152424
in: metadata.pathParameters,
24162425
name: "a",
24172426
as: Swift.String.self
2427+
),
2428+
num: try converter.getPathParameterAsURI(
2429+
in: metadata.pathParameters,
2430+
name: "num",
2431+
as: Swift.Int.self
24182432
)
24192433
)
24202434
return Operations.getFoo.Input(path: path)

0 commit comments

Comments
 (0)