Skip to content

Commit abcc8bb

Browse files
authored
SOAR-0002 implementation (#194)
SOAR-0002 implementation ### Motivation Implements the approved SOAR-0002 proposal: #170 ### Modifications - updates the function that computes content type names to be in line with v3 of SOAR-0002 - refactors the `ContentType` type a bit to make it harder to accidentally perform case-sensitive comparions where case-insensitive ones are required ### Result When an adopter passes the `multipleContentTypes` feature flag, they'll get the new logic as described in SOAR-0002. ### Test Plan - adapted unit tests for the `contentSwiftName` function - enabled the `proposal0001` feature flag as well, since they will be enabled together in 0.2.0, so makes sense to test the new world together - this lead to a few more updates needed in the reference tests that weren't caused by 0.2.0 alone Reviewed by: glbrntt Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - 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. #194
1 parent a418cf9 commit abcc8bb

File tree

12 files changed

+146
-99
lines changed

12 files changed

+146
-99
lines changed

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,37 @@ extension FileTranslator {
2121
/// - Parameter contentType: The content type for which to compute the name.
2222
func contentSwiftName(_ contentType: ContentType) -> String {
2323
if config.featureFlags.contains(.multipleContentTypes) {
24-
let rawMIMEType = contentType.lowercasedTypeAndSubtype
25-
switch rawMIMEType {
24+
switch contentType.lowercasedTypeAndSubtype {
2625
case "application/json":
2726
return "json"
2827
case "application/x-www-form-urlencoded":
29-
return "form"
28+
return "urlEncodedForm"
3029
case "multipart/form-data":
31-
return "multipart"
30+
return "multipartForm"
3231
case "text/plain":
33-
return "text"
32+
return "plainText"
3433
case "*/*":
3534
return "any"
3635
case "application/xml":
3736
return "xml"
3837
case "application/octet-stream":
3938
return "binary"
39+
case "text/html":
40+
return "html"
41+
case "application/yaml":
42+
return "yaml"
43+
case "text/csv":
44+
return "csv"
45+
case "image/png":
46+
return "png"
47+
case "application/pdf":
48+
return "pdf"
49+
case "image/jpeg":
50+
return "jpeg"
4051
default:
41-
return swiftSafeName(for: rawMIMEType)
52+
let safedType = swiftSafeName(for: contentType.originallyCasedType)
53+
let safedSubtype = swiftSafeName(for: contentType.originallyCasedSubtype)
54+
return "\(safedType)_\(safedSubtype)"
4255
}
4356
} else {
4457
switch contentType.category {

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,9 @@ struct ContentType: Hashable {
5050
/// First checks if the provided content type is a JSON, then text,
5151
/// and uses binary if none of the two match.
5252
/// - Parameters:
53-
/// - type: The first component of the MIME type.
54-
/// - subtype: The second component of the MIME type.
55-
init(type: String, subtype: String) {
56-
let lowercasedType = type.lowercased()
57-
let lowercasedSubtype = subtype.lowercased()
58-
53+
/// - lowercasedType: The first component of the MIME type.
54+
/// - lowercasedSubtype: The second component of the MIME type.
55+
fileprivate init(lowercasedType: String, lowercasedSubtype: String) {
5956
// https://json-schema.org/draft/2020-12/json-schema-core.html#section-4.2
6057
if (lowercasedType == "application" && lowercasedSubtype == "json") || lowercasedSubtype.hasSuffix("+json")
6158
{
@@ -82,27 +79,33 @@ struct ContentType: Hashable {
8279

8380
/// The mapped content type category.
8481
var category: Category {
85-
Category(type: type, subtype: subtype)
82+
Category(lowercasedType: lowercasedType, lowercasedSubtype: lowercasedSubtype)
8683
}
8784

8885
/// The first component of the MIME type.
89-
private let type: String
86+
///
87+
/// Preserves the casing from the input, do not use this
88+
/// for equality comparisons, use `lowercasedType` instead.
89+
let originallyCasedType: String
9090

9191
/// The first component of the MIME type, as a lowercase string.
9292
///
9393
/// The raw value in its original casing is only provided by `rawTypeAndSubtype`.
9494
var lowercasedType: String {
95-
type.lowercased()
95+
originallyCasedType.lowercased()
9696
}
9797

9898
/// The second component of the MIME type.
99-
private let subtype: String
99+
///
100+
/// Preserves the casing from the input, do not use this
101+
/// for equality comparisons, use `lowercasedSubtype` instead.
102+
let originallyCasedSubtype: String
100103

101104
/// The second component of the MIME type, as a lowercase string.
102105
///
103106
/// The raw value in its original casing is only provided by `originallyCasedTypeAndSubtype`.
104107
var lowercasedSubtype: String {
105-
subtype.lowercased()
108+
originallyCasedSubtype.lowercased()
106109
}
107110

108111
/// Creates a new content type by parsing the specified MIME type.
@@ -122,15 +125,15 @@ struct ContentType: Hashable {
122125
typeAndSubtype.count == 2,
123126
"Invalid ContentType string, must have 2 components separated by a slash."
124127
)
125-
self.type = typeAndSubtype[0]
126-
self.subtype = typeAndSubtype[1]
128+
self.originallyCasedType = typeAndSubtype[0]
129+
self.originallyCasedSubtype = typeAndSubtype[1]
127130
}
128131

129132
/// Returns the type and subtype as a "<type>/<subtype>" string.
130133
///
131134
/// Respects the original casing provided as input.
132135
var originallyCasedTypeAndSubtype: String {
133-
"\(type)/\(subtype)"
136+
"\(originallyCasedType)/\(originallyCasedSubtype)"
134137
}
135138

136139
/// Returns the type and subtype as a "<type>/<subtype>" string.

Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ final class Test_String: Test_Core {
8383

8484
// Non Latin Characters
8585
("$مرحبا", "_dollar_مرحبا"),
86+
87+
// Content type components
88+
("application", "application"),
89+
("vendor1+json", "vendor1_plus_json"),
8690
]
8791
let translator = makeTranslator(featureFlags: [.proposal0001])
8892
let asSwiftSafeName: (String) -> String = translator.swiftSafeName

Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentSwiftName.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,30 @@ final class Test_ContentSwiftName: Test_Core {
3434
}
3535

3636
func testProposed_multipleContentTypes() throws {
37-
let nameMaker = makeTranslator(featureFlags: [.multipleContentTypes]).contentSwiftName
37+
let nameMaker = makeTranslator(featureFlags: [
38+
.proposal0001,
39+
.multipleContentTypes,
40+
])
41+
.contentSwiftName
3842
let cases: [(String, String)] = [
43+
44+
// Short names.
3945
("application/json", "json"),
40-
("application/x-www-form-urlencoded", "form"),
41-
("multipart/form-data", "multipart"),
42-
("text/plain", "text"),
46+
("application/x-www-form-urlencoded", "urlEncodedForm"),
47+
("multipart/form-data", "multipartForm"),
48+
("text/plain", "plainText"),
4349
("*/*", "any"),
4450
("application/xml", "xml"),
4551
("application/octet-stream", "binary"),
46-
("application/myformat+json", "application_myformat_json"),
52+
("text/html", "html"),
53+
("application/yaml", "yaml"),
54+
("text/csv", "csv"),
55+
("image/png", "png"),
56+
("application/pdf", "pdf"),
57+
("image/jpeg", "jpeg"),
58+
59+
// Generic names.
60+
("application/myformat+json", "application_myformat_plus_json"),
4761
("foo/bar", "foo_bar"),
4862
]
4963
try _testIdentifiers(cases: cases, nameMaker: nameMaker)

Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ class FileBasedReferenceTests: XCTestCase {
5959
name: .petstore,
6060
customDirectoryName: "Petstore_FF_MultipleContentTypes"
6161
),
62-
featureFlags: [.multipleContentTypes]
62+
featureFlags: [
63+
.multipleContentTypes,
64+
.proposal0001,
65+
]
6366
)
6467
}
6568

Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public struct Client: APIProtocol {
7474
try converter.setHeaderFieldAsText(
7575
in: &request.headerFields,
7676
name: "My-Request-UUID",
77-
value: input.headers.My_Request_UUID
77+
value: input.headers.My_hyphen_Request_hyphen_UUID
7878
)
7979
try converter.setQueryItemAsText(
8080
in: &request,
@@ -94,12 +94,12 @@ public struct Client: APIProtocol {
9494
switch response.statusCode {
9595
case 200:
9696
let headers: Operations.listPets.Output.Ok.Headers = .init(
97-
My_Response_UUID: try converter.getRequiredHeaderFieldAsText(
97+
My_hyphen_Response_hyphen_UUID: try converter.getRequiredHeaderFieldAsText(
9898
in: response.headerFields,
9999
name: "My-Response-UUID",
100100
as: Swift.String.self
101101
),
102-
My_Tracing_Header: try converter.getOptionalHeaderFieldAsText(
102+
My_hyphen_Tracing_hyphen_Header: try converter.getOptionalHeaderFieldAsText(
103103
in: response.headerFields,
104104
name: "My-Tracing-Header",
105105
as: Components.Headers.TracingHeader.self
@@ -169,7 +169,7 @@ public struct Client: APIProtocol {
169169
try converter.setHeaderFieldAsJSON(
170170
in: &request.headerFields,
171171
name: "X-Extra-Arguments",
172-
value: input.headers.X_Extra_Arguments
172+
value: input.headers.X_hyphen_Extra_hyphen_Arguments
173173
)
174174
try converter.setHeaderFieldAsText(
175175
in: &request.headerFields,
@@ -190,7 +190,7 @@ public struct Client: APIProtocol {
190190
switch response.statusCode {
191191
case 201:
192192
let headers: Operations.createPet.Output.Created.Headers = .init(
193-
X_Extra_Arguments: try converter.getOptionalHeaderFieldAsJSON(
193+
X_hyphen_Extra_hyphen_Arguments: try converter.getOptionalHeaderFieldAsJSON(
194194
in: response.headerFields,
195195
name: "X-Extra-Arguments",
196196
as: Components.Schemas.CodeError.self
@@ -217,7 +217,7 @@ public struct Client: APIProtocol {
217217
return .created(.init(headers: headers, body: body))
218218
case 400:
219219
let headers: Components.Responses.ErrorBadRequest.Headers = .init(
220-
X_Reason: try converter.getOptionalHeaderFieldAsText(
220+
X_hyphen_Reason: try converter.getOptionalHeaderFieldAsText(
221221
in: response.headerFields,
222222
name: "X-Reason",
223223
as: Swift.String.self
@@ -295,7 +295,7 @@ public struct Client: APIProtocol {
295295
body = try converter.getResponseBodyAsText(
296296
Swift.String.self,
297297
from: response.body,
298-
transforming: { value in .text(value) }
298+
transforming: { value in .plainText(value) }
299299
)
300300
} else if try converter.isMatchingContentType(
301301
received: contentType,
@@ -337,7 +337,7 @@ public struct Client: APIProtocol {
337337
headerFields: &request.headerFields,
338338
contentType: "application/json; charset=utf-8"
339339
)
340-
case let .text(value):
340+
case let .plainText(value):
341341
request.body = try converter.setRequiredRequestBodyAsText(
342342
value,
343343
headerFields: &request.headerFields,
@@ -543,7 +543,7 @@ public struct Client: APIProtocol {
543543
body = try converter.getResponseBodyAsText(
544544
Swift.String.self,
545545
from: response.body,
546-
transforming: { value in .text(value) }
546+
transforming: { value in .plainText(value) }
547547
)
548548
} else {
549549
throw converter.makeUnexpectedContentTypeError(contentType: contentType)

Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,11 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
111111
style: .form,
112112
explode: true,
113113
name: "since",
114-
as: Components.Parameters.query_born_since.self
114+
as: Components.Parameters.query_period_born_hyphen_since.self
115115
)
116116
)
117117
let headers: Operations.listPets.Input.Headers = .init(
118-
My_Request_UUID: try converter.getOptionalHeaderFieldAsText(
118+
My_hyphen_Request_hyphen_UUID: try converter.getOptionalHeaderFieldAsText(
119119
in: request.headerFields,
120120
name: "My-Request-UUID",
121121
as: Swift.String.self
@@ -139,12 +139,12 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
139139
try converter.setHeaderFieldAsText(
140140
in: &response.headerFields,
141141
name: "My-Response-UUID",
142-
value: value.headers.My_Response_UUID
142+
value: value.headers.My_hyphen_Response_hyphen_UUID
143143
)
144144
try converter.setHeaderFieldAsText(
145145
in: &response.headerFields,
146146
name: "My-Tracing-Header",
147-
value: value.headers.My_Tracing_Header
147+
value: value.headers.My_hyphen_Tracing_hyphen_Header
148148
)
149149
switch value.body {
150150
case let .json(value):
@@ -193,7 +193,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
193193
deserializer: { request, metadata in let path: Operations.createPet.Input.Path = .init()
194194
let query: Operations.createPet.Input.Query = .init()
195195
let headers: Operations.createPet.Input.Headers = .init(
196-
X_Extra_Arguments: try converter.getOptionalHeaderFieldAsJSON(
196+
X_hyphen_Extra_hyphen_Arguments: try converter.getOptionalHeaderFieldAsJSON(
197197
in: request.headerFields,
198198
name: "X-Extra-Arguments",
199199
as: Components.Schemas.CodeError.self
@@ -233,7 +233,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
233233
try converter.setHeaderFieldAsJSON(
234234
in: &response.headerFields,
235235
name: "X-Extra-Arguments",
236-
value: value.headers.X_Extra_Arguments
236+
value: value.headers.X_hyphen_Extra_hyphen_Arguments
237237
)
238238
switch value.body {
239239
case let .json(value):
@@ -255,7 +255,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
255255
try converter.setHeaderFieldAsText(
256256
in: &response.headerFields,
257257
name: "X-Reason",
258-
value: value.headers.X_Reason
258+
value: value.headers.X_hyphen_Reason
259259
)
260260
switch value.body {
261261
case let .json(value):
@@ -312,7 +312,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
312312
headerFields: &response.headerFields,
313313
contentType: "application/json; charset=utf-8"
314314
)
315-
case let .text(value):
315+
case let .plainText(value):
316316
try converter.validateAcceptIfPresent(
317317
"text/plain",
318318
in: request.headerFields
@@ -371,7 +371,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
371371
body = try converter.getRequiredRequestBodyAsText(
372372
Swift.String.self,
373373
from: request.body,
374-
transforming: { value in .text(value) }
374+
transforming: { value in .plainText(value) }
375375
)
376376
} else if try converter.isMatchingContentType(
377377
received: contentType,
@@ -530,7 +530,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
530530
petId: try converter.getPathParameterAsText(
531531
in: metadata.pathParameters,
532532
name: "petId",
533-
as: Components.Parameters.path_petId.self
533+
as: Components.Parameters.path_period_petId.self
534534
)
535535
)
536536
let query: Operations.uploadAvatarForPet.Input.Query = .init()
@@ -601,7 +601,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
601601
var response = Response(statusCode: 500)
602602
suppressMutabilityWarning(&response)
603603
switch value.body {
604-
case let .text(value):
604+
case let .plainText(value):
605605
try converter.validateAcceptIfPresent(
606606
"text/plain",
607607
in: request.headerFields

0 commit comments

Comments
 (0)