Skip to content

Commit e02d4b8

Browse files
authored
Include parameters in the content type name (#236)
Include parameters in the content type name ### Motivation Fixes #232, which was blocking the Kubernetes OpenAPI doc from successfully generating and building. ### Modifications Include parameters, such as `application/json; foo=bar` in the content type name, e.g. `application_json_foo_bar`. ### Result #232 is fixed, verified already locally. ### Test Plan Updated unit and snippet tests. Reviewed by: gjcairo, 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 (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. ✖︎ pull request validation (integration test) - Build finished. #236
1 parent 6eb9084 commit e02d4b8

File tree

7 files changed

+131
-20
lines changed

7 files changed

+131
-20
lines changed

Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/CommentExtensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ extension ContentType {
167167
func docComment(typeName: TypeName) -> Comment? {
168168
typeName.docCommentWithUserDescription(
169169
nil,
170-
subPath: lowercasedTypeAndSubtypeWithEscape
170+
subPath: lowercasedTypeSubtypeAndParametersWithEscape
171171
)
172172
}
173173
}

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ extension FileTranslator {
155155
}
156156
let chosenContent: (SchemaContent, OpenAPI.Content)?
157157
if let (contentKey, contentValue) = map.first(where: { $0.key.isJSON }) {
158-
let contentType = ContentType(contentKey.typeAndSubtype)
158+
let contentType = contentKey.asGeneratorContentType
159159
chosenContent = (
160160
.init(
161161
contentType: contentType,
@@ -164,7 +164,7 @@ extension FileTranslator {
164164
contentValue
165165
)
166166
} else if let (contentKey, contentValue) = map.first(where: { $0.key.isText }) {
167-
let contentType = ContentType(contentKey.typeAndSubtype)
167+
let contentType = contentKey.asGeneratorContentType
168168
chosenContent = (
169169
.init(
170170
contentType: contentType,
@@ -173,7 +173,7 @@ extension FileTranslator {
173173
contentValue
174174
)
175175
} else if !excludeBinary, let (contentKey, contentValue) = map.first(where: { $0.key.isBinary }) {
176-
let contentType = ContentType(contentKey.typeAndSubtype)
176+
let contentType = contentKey.asGeneratorContentType
177177
chosenContent = (
178178
.init(
179179
contentType: contentType,
@@ -225,7 +225,7 @@ extension FileTranslator {
225225
foundIn: String
226226
) -> SchemaContent? {
227227
if contentKey.isJSON {
228-
let contentType = ContentType(contentKey.typeAndSubtype)
228+
let contentType = contentKey.asGeneratorContentType
229229
if contentType.lowercasedType == "multipart"
230230
|| contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded")
231231
{
@@ -241,14 +241,14 @@ extension FileTranslator {
241241
)
242242
}
243243
if contentKey.isText {
244-
let contentType = ContentType(contentKey.typeAndSubtype)
244+
let contentType = contentKey.asGeneratorContentType
245245
return .init(
246246
contentType: contentType,
247247
schema: .b(.string)
248248
)
249249
}
250250
if !excludeBinary, contentKey.isBinary {
251-
let contentType = ContentType(contentKey.typeAndSubtype)
251+
let contentType = contentKey.asGeneratorContentType
252252
return .init(
253253
contentType: contentType,
254254
schema: .b(.string(format: .binary))

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ extension FileTranslator {
2020
///
2121
/// - Parameter contentType: The content type for which to compute the name.
2222
func contentSwiftName(_ contentType: ContentType) -> String {
23-
switch contentType.lowercasedTypeAndSubtype {
23+
let rawContentType = contentType.lowercasedTypeSubtypeAndParameters
24+
switch rawContentType {
2425
case "application/json":
2526
return "json"
2627
case "application/x-www-form-urlencoded":
@@ -50,7 +51,22 @@ extension FileTranslator {
5051
default:
5152
let safedType = swiftSafeName(for: contentType.originallyCasedType)
5253
let safedSubtype = swiftSafeName(for: contentType.originallyCasedSubtype)
53-
return "\(safedType)_\(safedSubtype)"
54+
let prefix = "\(safedType)_\(safedSubtype)"
55+
let params = contentType
56+
.lowercasedParameterPairs
57+
guard !params.isEmpty else {
58+
return prefix
59+
}
60+
let safedParams =
61+
params
62+
.map { pair in
63+
pair
64+
.split(separator: "=")
65+
.map { swiftSafeName(for: String($0)) }
66+
.joined(separator: "_")
67+
}
68+
.joined(separator: "_")
69+
return prefix + "_" + safedParams
5470
}
5571
}
5672
}

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,56 @@ struct ContentType: Hashable {
108108
originallyCasedSubtype.lowercased()
109109
}
110110

111+
/// The parameter key-value pairs.
112+
///
113+
/// Preserves the casing from the input, do not use this
114+
/// for equality comparisons, use `lowercasedParameterPairs` instead.
115+
let originallyCasedParameterPairs: [String]
116+
117+
/// The parameter key-value pairs, lowercased.
118+
///
119+
/// The raw value in its original casing is only provided by `originallyCasedParameterPairs`.
120+
var lowercasedParameterPairs: [String] {
121+
originallyCasedParameterPairs.map { $0.lowercased() }
122+
}
123+
124+
/// The parameters string.
125+
var originallyCasedParametersString: String {
126+
originallyCasedParameterPairs.map { "; \($0)" }.joined()
127+
}
128+
129+
/// The parameters string, lowercased.
130+
var lowercasedParametersString: String {
131+
originallyCasedParametersString.lowercased()
132+
}
133+
134+
/// The type, subtype, and parameters components combined.
135+
var originallyCasedTypeSubtypeAndParameters: String {
136+
originallyCasedTypeAndSubtype + originallyCasedParametersString
137+
}
138+
139+
/// The type, subtype, and parameters components combined and lowercased.
140+
var lowercasedTypeSubtypeAndParameters: String {
141+
originallyCasedTypeSubtypeAndParameters.lowercased()
142+
}
143+
111144
/// Creates a new content type by parsing the specified MIME type.
112145
/// - Parameter rawValue: A MIME type, for example "application/json". Must
113146
/// not be empty.
114147
init(_ rawValue: String) {
115148
precondition(!rawValue.isEmpty, "rawValue of a ContentType cannot be empty.")
116-
let rawTypeAndSubtype =
149+
var semiComponents =
117150
rawValue
118-
.split(separator: ";")[0]
151+
.split(separator: ";")
152+
let typeAndSubtypeComponent = semiComponents.removeFirst()
153+
self.originallyCasedParameterPairs = semiComponents.map { component in
154+
component
155+
.split(separator: "=")
156+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
157+
.joined(separator: "=")
158+
}
159+
let rawTypeAndSubtype =
160+
typeAndSubtypeComponent
119161
.trimmingCharacters(in: .whitespaces)
120162
let typeAndSubtype =
121163
rawTypeAndSubtype
@@ -143,21 +185,25 @@ struct ContentType: Hashable {
143185
"\(lowercasedType)/\(lowercasedSubtype)"
144186
}
145187

146-
/// Returns the type and subtype as a "<type>\/<subtype>" string.
188+
/// Returns the type, subtype and parameters (if present) as a "<type>\/<subtype>[;<param>...]" string.
147189
///
148190
/// Lowercased to ease case-insensitive comparisons, and escaped to show
149191
/// that the slash between type and subtype is not a path separator.
150-
var lowercasedTypeAndSubtypeWithEscape: String {
151-
"\(lowercasedType)\\/\(lowercasedSubtype)"
192+
var lowercasedTypeSubtypeAndParametersWithEscape: String {
193+
"\(lowercasedType)\\/\(lowercasedSubtype)" + lowercasedParametersString
152194
}
153195

154196
/// The header value used when sending a content-type header.
155197
var headerValueForSending: String {
156198
guard case .json = category else {
157-
return lowercasedTypeAndSubtype
199+
return lowercasedTypeSubtypeAndParameters
158200
}
159201
// We always encode JSON using JSONEncoder which uses UTF-8.
160-
return lowercasedTypeAndSubtype + "; charset=utf-8"
202+
// Check if it's already present, if not, append it.
203+
guard !lowercasedParameterPairs.contains("charset=") else {
204+
return lowercasedTypeSubtypeAndParameters
205+
}
206+
return lowercasedTypeSubtypeAndParameters + "; charset=utf-8"
161207
}
162208

163209
/// The header value used when validating a content-type header.
@@ -223,6 +269,6 @@ extension OpenAPI.ContentType {
223269
/// Returns the content type wrapped in the generator's representation
224270
/// of a content type, as opposed to the one from OpenAPIKit.
225271
var asGeneratorContentType: ContentType {
226-
ContentType(typeAndSubtype)
272+
ContentType(rawValue)
227273
}
228274
}

Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentSwiftName.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ final class Test_ContentSwiftName: Test_Core {
3939
// Generic names.
4040
("application/myformat+json", "application_myformat_plus_json"),
4141
("foo/bar", "foo_bar"),
42+
43+
// Names with a parameter.
44+
("application/foo", "application_foo"),
45+
("application/foo; bar=baz; boo=foo", "application_foo_bar_baz_boo_foo"),
46+
("application/foo; bar = baz", "application_foo_bar_baz"),
4247
]
4348
for item in cases {
4449
let contentType = try XCTUnwrap(ContentType(item.0))

Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentType.swift

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@ final class Test_ContentType: Test_Core {
2424
category: ContentType.Category,
2525
type: String,
2626
subtype: String,
27+
parameters: String,
2728
lowercasedOutput: String,
28-
originallyCasedOutput: String
29+
originallyCasedOutput: String,
30+
originallyCasedOutputWithParameters: String
2931
)] = [
3032
(
3133
"application/json",
3234
.json,
3335
"application",
3436
"json",
37+
"",
38+
"application/json",
3539
"application/json",
3640
"application/json"
3741
),
@@ -40,22 +44,28 @@ final class Test_ContentType: Test_Core {
4044
.json,
4145
"application",
4246
"json",
47+
"",
4348
"application/json",
49+
"APPLICATION/JSON",
4450
"APPLICATION/JSON"
4551
),
4652
(
4753
"application/json; charset=utf-8",
4854
.json,
4955
"application",
5056
"json",
57+
"; charset=utf-8",
5158
"application/json",
52-
"application/json"
59+
"application/json",
60+
"application/json; charset=utf-8"
5361
),
5462
(
5563
"application/x-www-form-urlencoded",
5664
.binary,
5765
"application",
5866
"x-www-form-urlencoded",
67+
"",
68+
"application/x-www-form-urlencoded",
5969
"application/x-www-form-urlencoded",
6070
"application/x-www-form-urlencoded"
6171
),
@@ -64,6 +74,8 @@ final class Test_ContentType: Test_Core {
6474
.binary,
6575
"multipart",
6676
"form-data",
77+
"",
78+
"multipart/form-data",
6779
"multipart/form-data",
6880
"multipart/form-data"
6981
),
@@ -72,6 +84,8 @@ final class Test_ContentType: Test_Core {
7284
.text,
7385
"text",
7486
"plain",
87+
"",
88+
"text/plain",
7589
"text/plain",
7690
"text/plain"
7791
),
@@ -80,6 +94,8 @@ final class Test_ContentType: Test_Core {
8094
.binary,
8195
"*",
8296
"*",
97+
"",
98+
"*/*",
8399
"*/*",
84100
"*/*"
85101
),
@@ -88,6 +104,8 @@ final class Test_ContentType: Test_Core {
88104
.binary,
89105
"application",
90106
"xml",
107+
"",
108+
"application/xml",
91109
"application/xml",
92110
"application/xml"
93111
),
@@ -96,6 +114,8 @@ final class Test_ContentType: Test_Core {
96114
.binary,
97115
"application",
98116
"octet-stream",
117+
"",
118+
"application/octet-stream",
99119
"application/octet-stream",
100120
"application/octet-stream"
101121
),
@@ -104,6 +124,8 @@ final class Test_ContentType: Test_Core {
104124
.json,
105125
"application",
106126
"myformat+json",
127+
"",
128+
"application/myformat+json",
107129
"application/myformat+json",
108130
"application/myformat+json"
109131
),
@@ -112,6 +134,8 @@ final class Test_ContentType: Test_Core {
112134
.binary,
113135
"foo",
114136
"bar",
137+
"",
138+
"foo/bar",
115139
"foo/bar",
116140
"foo/bar"
117141
),
@@ -120,24 +144,40 @@ final class Test_ContentType: Test_Core {
120144
.json,
121145
"foo",
122146
"bar+json",
147+
"",
148+
"foo/bar+json",
123149
"foo/bar+json",
124150
"foo/bar+json"
125151
),
152+
(
153+
"foo/bar+json; param1=a; param2=b",
154+
.json,
155+
"foo",
156+
"bar+json",
157+
"; param1=a; param2=b",
158+
"foo/bar+json",
159+
"foo/bar+json",
160+
"foo/bar+json; param1=a; param2=b"
161+
),
126162
]
127163
for (
128164
rawValue,
129165
category,
130166
type,
131167
subtype,
168+
parameters,
132169
lowercasedTypeAndSubtype,
133-
originallyCasedTypeAndSubtype
170+
originallyCasedTypeAndSubtype,
171+
originallyCasedOutputWithParameters
134172
) in cases {
135173
let contentType = ContentType(rawValue)
136174
XCTAssertEqual(contentType.category, category)
137175
XCTAssertEqual(contentType.lowercasedType, type)
138176
XCTAssertEqual(contentType.lowercasedSubtype, subtype)
177+
XCTAssertEqual(contentType.lowercasedParametersString, parameters)
139178
XCTAssertEqual(contentType.lowercasedTypeAndSubtype, lowercasedTypeAndSubtype)
140179
XCTAssertEqual(contentType.originallyCasedTypeAndSubtype, originallyCasedTypeAndSubtype)
180+
XCTAssertEqual(contentType.originallyCasedTypeSubtypeAndParameters, originallyCasedOutputWithParameters)
141181
}
142182
}
143183
}

Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,9 @@ final class SnippetBasedReferenceTests: XCTestCase {
896896
application/json:
897897
schema:
898898
type: integer
899+
application/json; foo=bar:
900+
schema:
901+
type: integer
899902
text/plain: {}
900903
application/octet-stream: {}
901904
""",
@@ -904,6 +907,7 @@ final class SnippetBasedReferenceTests: XCTestCase {
904907
public struct MultipleContentTypes: Sendable, Hashable {
905908
@frozen public enum Body: Sendable, Hashable {
906909
case json(Swift.Int)
910+
case application_json_foo_bar(Swift.Int)
907911
case plainText(Swift.String)
908912
case binary(Foundation.Data)
909913
}

0 commit comments

Comments
 (0)