Skip to content

Commit 8b9c564

Browse files
authored
[Generator] Choose the serialization method based on content type (#48)
[Generator] Choose the serialization method based on content type ### Motivation Fixes #43. Depends on apple/swift-openapi-runtime#12 landing first and tagging a release. ### Modifications Builds on top of the changes to the runtime library. ### Result We now always specify a coding strategy with a Swift type, leading to deterministic and understandable conversion logic. ### Test Plan Updated unit and integration tests, added a `500` case to one of the operations to explicitly test the missing plain text response body. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #48
1 parent 7186fd5 commit 8b9c564

File tree

26 files changed

+518
-255
lines changed

26 files changed

+518
-255
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ let package = Package(
6161
),
6262

6363
// Tests-only: Runtime library linked by generated code
64-
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.0")),
64+
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.1")),
6565

6666
// Build and preview docs
6767
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),

Sources/_OpenAPIGeneratorCore/Extensions/Foundation.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,7 @@ extension Data {
2121
/// - Throws: When data is not valid UTF-8.
2222
var swiftFormatted: Data {
2323
get throws {
24-
struct FormattingError: Error, LocalizedError, CustomStringConvertible {
25-
var description: String {
26-
"Invalid UTF-8 data"
27-
}
28-
var errorDescription: String? {
29-
description
30-
}
31-
}
32-
guard let string = String(data: self, encoding: .utf8) else {
33-
throw FormattingError()
34-
}
24+
let string = String(decoding: self, as: UTF8.self)
3525
return try Self(string.swiftFormatted.utf8)
3626
}
3727
}

Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ struct TextBasedRenderer: RendererProtocol {
3333

3434
/// Renders the specified Swift file.
3535
func renderFile(_ description: FileDescription) -> Data {
36-
renderedFile(description).data(using: .utf8)!
36+
Data(renderedFile(description).utf8)
3737
}
3838

3939
/// Renders the specified comment.

Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,28 @@ extension ClientFileTranslator {
2222
_ description: OperationDescription
2323
) throws -> Expression {
2424

25-
let clientPathTemplate = try translatePathParameterInClient(
25+
let (pathTemplate, pathParamsArrayExpr) = try translatePathParameterInClient(
2626
description: description
2727
)
28+
let pathDecl: Declaration = .variable(
29+
kind: .let,
30+
left: "path",
31+
right: .try(
32+
.identifier("converter")
33+
.dot("renderedRequestPath")
34+
.call([
35+
.init(label: "template", expression: .literal(pathTemplate)),
36+
.init(label: "parameters", expression: pathParamsArrayExpr),
37+
])
38+
)
39+
)
2840
let requestDecl: Declaration = .variable(
2941
kind: .var,
3042
left: "request",
3143
type: TypeName.request.fullyQualifiedSwiftName,
3244
right: .dot("init")
3345
.call([
34-
.init(label: "path", expression: .literal(clientPathTemplate)),
46+
.init(label: "path", expression: .identifier("path")),
3547
.init(label: "method", expression: .dot(description.httpMethodLowercased)),
3648
])
3749
)
@@ -65,9 +77,12 @@ extension ClientFileTranslator {
6577
.map(\.headerValueForValidation)
6678
.joined(separator: ", ")
6779
let addAcceptHeaderExpr: Expression = .try(
68-
.identifier("converter").dot("headerFieldAdd")
80+
.identifier("converter").dot("setHeaderFieldAsText")
6981
.call([
70-
.init(label: "in", expression: .inOut(.identifier("request").dot("headerFields"))),
82+
.init(
83+
label: "in",
84+
expression: .inOut(.identifier("request").dot("headerFields"))
85+
),
7186
.init(label: "name", expression: "accept"),
7287
.init(label: "value", expression: .literal(acceptValue)),
7388
])
@@ -91,6 +106,7 @@ extension ClientFileTranslator {
91106
"input"
92107
],
93108
body: [
109+
.declaration(pathDecl),
94110
.declaration(requestDecl),
95111
.expression(requestDecl.suppressMutabilityWarningExpr),
96112
] + requestExprs.map { .expression($0) } + [

Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,19 @@ enum Constants {
336336
]
337337
}
338338

339+
/// Constants related to the coding strategy.
340+
enum CodingStrategy {
341+
342+
/// The substring used in method names for the JSON coding strategy.
343+
static let json: String = "JSON"
344+
345+
/// The substring used in method names for the text coding strategy.
346+
static let text: String = "Text"
347+
348+
/// The substring used in method names for the binary coding strategy.
349+
static let binary: String = "Binary"
350+
}
351+
339352
/// Constants related to types used in many components.
340353
enum Global {
341354

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
/// Describes the underlying coding strategy.
16+
enum CodingStrategy: String, Equatable, Hashable, Sendable {
17+
18+
/// A strategy using JSONEncoder/JSONDecoder.
19+
case json
20+
21+
/// A strategy using LosslessStringConvertible.
22+
case text
23+
24+
/// A strategy that passes through the data unmodified.
25+
case binary
26+
27+
/// The name of the coding strategy in the runtime library.
28+
var runtimeName: String {
29+
switch self {
30+
case .json:
31+
return Constants.CodingStrategy.json
32+
case .text:
33+
return Constants.CodingStrategy.text
34+
case .binary:
35+
return Constants.CodingStrategy.binary
36+
}
37+
}
38+
}

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ enum ContentType: Hashable {
8282
}
8383
}
8484

85+
/// The coding strategy appropriate for this content type.
86+
var codingStrategy: CodingStrategy {
87+
switch self {
88+
case .json:
89+
return .json
90+
case .text:
91+
return .text
92+
case .binary:
93+
return .binary
94+
}
95+
}
96+
8597
/// A Boolean value that indicates whether the content type
8698
/// is a type of JSON.
8799
var isJSON: Bool {

Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -232,23 +232,28 @@ extension OperationDescription {
232232
}
233233

234234
/// Returns a string that contains the template to be generated for
235-
/// the client that fills in path parameters.
235+
/// the client that fills in path parameters, and an array expression
236+
/// with the parameter values.
236237
///
237-
/// For example, `/cats/\(input.catId)`.
238-
var templatedPathForClient: String {
238+
/// For example, `/cats/{}` and `[input.catId]`.
239+
var templatedPathForClient: (String, Expression) {
239240
get throws {
240241
let path = self.path.rawValue
241242
let pathParameters = try allResolvedParameters.filter { $0.location == .path }
242-
guard !pathParameters.isEmpty else {
243-
return path
244-
}
245-
// replace "{foo}" with "\(input.foo)" for each parameter
246-
return pathParameters.reduce(into: path) { partialResult, parameter in
243+
// replace "{foo}" with "{}" for each parameter
244+
let template = pathParameters.reduce(into: path) { partialResult, parameter in
247245
partialResult = partialResult.replacingOccurrences(
248246
of: "{\(parameter.name)}",
249-
with: "\\(input.path.\(parameter.name.asSwiftSafeName))"
247+
with: "{}"
250248
)
251249
}
250+
let names: [Expression] =
251+
pathParameters
252+
.map { param in
253+
.identifier("input.path.\(param.name.asSwiftSafeName)")
254+
}
255+
let arrayExpr: Expression = .literal(.array(names))
256+
return (template, arrayExpr)
252257
}
253258
}
254259

Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ struct TypedParameter {
2424

2525
/// The computed type usage.
2626
var typeUsage: TypeUsage
27+
28+
/// The coding strategy appropriate for this parameter.
29+
var codingStrategy: CodingStrategy
2730
}
2831

2932
extension TypedParameter: CustomStringConvertible {
@@ -126,9 +129,11 @@ extension FileTranslator {
126129
let foundIn = "\(locationTypeName.description)/\(parameter.name)"
127130

128131
let schema: Either<JSONReference<JSONSchema>, JSONSchema>
132+
let codingStrategy: CodingStrategy
129133
switch parameter.schemaOrContent {
130134
case let .a(schemaContext):
131135
schema = schemaContext.schema
136+
codingStrategy = .text
132137

133138
// Check supported exploded/style types
134139
let location = parameter.location
@@ -175,6 +180,11 @@ extension FileTranslator {
175180
return nil
176181
}
177182
schema = typedContent.content.schema ?? .b(.fragment)
183+
codingStrategy =
184+
typedContent
185+
.content
186+
.contentType
187+
.codingStrategy
178188
}
179189

180190
// Check if the underlying schema is supported
@@ -207,7 +217,8 @@ extension FileTranslator {
207217
return .init(
208218
parameter: parameter,
209219
schema: schema,
210-
typeUsage: usage
220+
typeUsage: usage,
221+
codingStrategy: codingStrategy
211222
)
212223
}
213224
}

Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,12 @@ extension TypesFileTranslator {
9191
extension ClientFileTranslator {
9292

9393
/// Returns a templated string that includes all path parameters in
94-
/// the specified operation.
94+
/// the specified operation, and an expression of an array literal
95+
/// with all those parameters.
9596
/// - Parameter description: The OpenAPI operation.
9697
func translatePathParameterInClient(
9798
description: OperationDescription
98-
) throws -> String {
99+
) throws -> (String, Expression) {
99100
try description.templatedPathForClient
100101
}
101102

@@ -115,10 +116,10 @@ extension ClientFileTranslator {
115116
let containerExpr: Expression
116117
switch parameter.location {
117118
case .header:
118-
methodPrefix = "headerField"
119+
methodPrefix = "HeaderField"
119120
containerExpr = .identifier(requestVariableName).dot("headerFields")
120121
case .query:
121-
methodPrefix = "query"
122+
methodPrefix = "QueryItem"
122123
containerExpr = .identifier(requestVariableName)
123124
default:
124125
diagnostics.emitUnsupported(
@@ -129,20 +130,22 @@ extension ClientFileTranslator {
129130
}
130131
return .try(
131132
.identifier("converter")
132-
.dot("\(methodPrefix)Add")
133-
.call([
134-
.init(
135-
label: "in",
136-
expression: .inOut(containerExpr)
137-
),
138-
.init(label: "name", expression: .literal(parameter.name)),
139-
.init(
140-
label: "value",
141-
expression: .identifier(inputVariableName)
142-
.dot(parameter.location.shortVariableName)
143-
.dot(parameter.variableName)
144-
),
145-
])
133+
.dot("set\(methodPrefix)As\(parameter.codingStrategy.runtimeName)")
134+
.call(
135+
[
136+
.init(
137+
label: "in",
138+
expression: .inOut(containerExpr)
139+
),
140+
.init(label: "name", expression: .literal(parameter.name)),
141+
.init(
142+
label: "value",
143+
expression: .identifier(inputVariableName)
144+
.dot(parameter.location.shortVariableName)
145+
.dot(parameter.variableName)
146+
),
147+
]
148+
)
146149
)
147150
}
148151
}
@@ -160,17 +163,21 @@ extension ServerFileTranslator {
160163
.typeUsage
161164
.fullyQualifiedNonOptionalSwiftName
162165

166+
func methodName(_ parameterLocationName: String, _ requiresOptionality: Bool = true) -> String {
167+
let optionality: String
168+
if requiresOptionality {
169+
optionality = parameter.required ? "Required" : "Optional"
170+
} else {
171+
optionality = ""
172+
}
173+
return "get\(optionality)\(parameterLocationName)As\(typedParameter.codingStrategy.runtimeName)"
174+
}
175+
163176
let convertExpr: Expression
164177
switch parameter.location {
165178
case .path:
166-
let methodName: String
167-
if parameter.required {
168-
methodName = "pathGetRequired"
169-
} else {
170-
methodName = "pathGetOptional"
171-
}
172179
convertExpr = .try(
173-
.identifier("converter").dot(methodName)
180+
.identifier("converter").dot(methodName("PathParameter", false))
174181
.call([
175182
.init(label: "in", expression: .identifier("metadata").dot("pathParameters")),
176183
.init(label: "name", expression: .literal(parameter.name)),
@@ -181,14 +188,8 @@ extension ServerFileTranslator {
181188
])
182189
)
183190
case .query:
184-
let methodName: String
185-
if parameter.required {
186-
methodName = "queryGetRequired"
187-
} else {
188-
methodName = "queryGetOptional"
189-
}
190191
convertExpr = .try(
191-
.identifier("converter").dot(methodName)
192+
.identifier("converter").dot(methodName("QueryItem"))
192193
.call([
193194
.init(label: "in", expression: .identifier("metadata").dot("queryParameters")),
194195
.init(label: "name", expression: .literal(parameter.name)),
@@ -199,15 +200,9 @@ extension ServerFileTranslator {
199200
])
200201
)
201202
case .header:
202-
let methodName: String
203-
if parameter.required {
204-
methodName = "headerFieldGetRequired"
205-
} else {
206-
methodName = "headerFieldGetOptional"
207-
}
208203
convertExpr = .try(
209204
.identifier("converter")
210-
.dot(methodName)
205+
.dot(methodName("HeaderField"))
211206
.call([
212207
.init(label: "in", expression: .identifier("request").dot("headerFields")),
213208
.init(label: "name", expression: .literal(parameter.name)),

0 commit comments

Comments
 (0)