Skip to content

Commit a8db309

Browse files
authored
Support base64-encoded data (#326)
This change accompanies apple/swift-openapi-runtime#55 and relies on it for the `OpenAPIRuntime.Base64EncodedData` type. ### Motivation OpenAPI supports base64-encoded data but to this point Swift OpenAPI Generator has not (#11). ### Modifications A data type specified as `type: string, format: byte` will now result in a generated type which is `Codable` and backed by a `OpenAPIRuntime.Base64EncodedData` type which knows how to encode and decode base64 data. ### Result Users will be able to specify request/response payloads as base64-encoded data which will be encoded and decoded transparently ### Test Plan Unit tested locally.
1 parent a919c65 commit a8db309

File tree

10 files changed

+278
-9
lines changed

10 files changed

+278
-9
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ let package = Package(
8989
// Tests-only: Runtime library linked by generated code, and also
9090
// helps keep the runtime library new enough to work with the generated
9191
// code.
92-
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.1")),
92+
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.2")),
9393

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

Sources/_OpenAPIGeneratorCore/FeatureFlags.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
public enum FeatureFlag: String, Hashable, Codable, CaseIterable, Sendable {
2929
// needs to be here for the enum to compile
3030
case empty
31+
32+
/// Base64 encoding and decoding.
33+
///
34+
/// Enable interpretation of `type: string, format: byte` as base64-encoded data.
35+
case base64DataEncodingDecoding
3136
}
3237

3338
/// A set of enabled feature flags.

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ struct TypeAssigner {
4545
/// safe to be used as a Swift identifier.
4646
var asSwiftSafeName: (String) -> String
4747

48+
///Enable decoding and encoding of as base64-encoded data strings.
49+
var enableBase64EncodingDecoding: Bool
50+
4851
/// Returns a type name for an OpenAPI-named component type.
4952
///
5053
/// A component type is any type in `#/components` in the OpenAPI document.
@@ -270,7 +273,8 @@ struct TypeAssigner {
270273
subtype: SubtypeNamingMethod
271274
) throws -> TypeUsage {
272275
let typeMatcher = TypeMatcher(
273-
asSwiftSafeName: asSwiftSafeName
276+
asSwiftSafeName: asSwiftSafeName,
277+
enableBase64EncodingDecoding: enableBase64EncodingDecoding
274278
)
275279
// Check if this type can be simply referenced without
276280
// creating a new inline type.
@@ -472,14 +476,16 @@ extension FileTranslator {
472476
/// A configured type assigner.
473477
var typeAssigner: TypeAssigner {
474478
TypeAssigner(
475-
asSwiftSafeName: swiftSafeName
479+
asSwiftSafeName: swiftSafeName,
480+
enableBase64EncodingDecoding: config.featureFlags.contains(.base64DataEncodingDecoding)
476481
)
477482
}
478483

479484
/// A configured type matcher.
480485
var typeMatcher: TypeMatcher {
481486
TypeMatcher(
482-
asSwiftSafeName: swiftSafeName
487+
asSwiftSafeName: swiftSafeName,
488+
enableBase64EncodingDecoding: config.featureFlags.contains(.base64DataEncodingDecoding)
483489
)
484490
}
485491
}

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ struct TypeMatcher {
2020
/// safe to be used as a Swift identifier.
2121
var asSwiftSafeName: (String) -> String
2222

23+
///Enable decoding and encoding of as base64-encoded data strings.
24+
var enableBase64EncodingDecoding: Bool
25+
2326
/// Returns the type name of a built-in type that matches the specified
2427
/// schema.
2528
///
@@ -82,7 +85,8 @@ struct TypeMatcher {
8285
return nil
8386
}
8487
return try TypeAssigner(
85-
asSwiftSafeName: asSwiftSafeName
88+
asSwiftSafeName: asSwiftSafeName,
89+
enableBase64EncodingDecoding: enableBase64EncodingDecoding
8690
)
8791
.typeName(for: ref).asUsage
8892
},
@@ -331,10 +335,10 @@ struct TypeMatcher {
331335
return nil
332336
}
333337
switch core.format {
334-
case .byte:
335-
typeName = .swift("String")
336338
case .binary:
337339
typeName = .body
340+
case .byte:
341+
typeName = .runtime("Base64EncodedData")
338342
case .dateTime:
339343
typeName = .foundation("Date")
340344
default:

Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ final class Test_TypeMatcher: Test_Core {
2424

2525
static let builtinTypes: [(JSONSchema, String)] = [
2626
(.string, "Swift.String"),
27-
(.string(.init(format: .byte), .init()), "Swift.String"),
2827
(.string(.init(format: .binary), .init()), "OpenAPIRuntime.HTTPBody"),
28+
(.string(.init(format: .byte), .init()), "OpenAPIRuntime.Base64EncodedData"),
2929
(.string(.init(format: .date), .init()), "Swift.String"),
3030
(.string(.init(format: .dateTime), .init()), "Foundation.Date"),
3131

Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ components:
265265
type: string
266266
tag:
267267
type: string
268+
genome:
269+
description: "Pet genome (base64-encoded)"
270+
type: string
271+
format: byte
268272
kind:
269273
$ref: '#/components/schemas/PetKind'
270274
MixedAnyOf:
@@ -307,6 +311,9 @@ components:
307311
$ref: '#/components/schemas/PetKind'
308312
tag:
309313
type: string
314+
genome:
315+
type: string
316+
format: byte
310317
Pets:
311318
type: array
312319
items:

Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ public enum Components {
140140
public var name: Swift.String
141141
/// - Remark: Generated from `#/components/schemas/Pet/tag`.
142142
public var tag: Swift.String?
143+
/// Pet genome (base64-encoded)
144+
///
145+
/// - Remark: Generated from `#/components/schemas/Pet/genome`.
146+
public var genome: OpenAPIRuntime.Base64EncodedData?
143147
/// - Remark: Generated from `#/components/schemas/Pet/kind`.
144148
public var kind: Components.Schemas.PetKind?
145149
/// Creates a new `Pet`.
@@ -148,22 +152,26 @@ public enum Components {
148152
/// - id: Pet id
149153
/// - name: Pet name
150154
/// - tag:
155+
/// - genome: Pet genome (base64-encoded)
151156
/// - kind:
152157
public init(
153158
id: Swift.Int64,
154159
name: Swift.String,
155160
tag: Swift.String? = nil,
161+
genome: OpenAPIRuntime.Base64EncodedData? = nil,
156162
kind: Components.Schemas.PetKind? = nil
157163
) {
158164
self.id = id
159165
self.name = name
160166
self.tag = tag
167+
self.genome = genome
161168
self.kind = kind
162169
}
163170
public enum CodingKeys: String, CodingKey {
164171
case id
165172
case name
166173
case tag
174+
case genome
167175
case kind
168176
}
169177
}
@@ -282,21 +290,31 @@ public enum Components {
282290
public var kind: Components.Schemas.PetKind?
283291
/// - Remark: Generated from `#/components/schemas/CreatePetRequest/tag`.
284292
public var tag: Swift.String?
293+
/// - Remark: Generated from `#/components/schemas/CreatePetRequest/genome`.
294+
public var genome: OpenAPIRuntime.Base64EncodedData?
285295
/// Creates a new `CreatePetRequest`.
286296
///
287297
/// - Parameters:
288298
/// - name:
289299
/// - kind:
290300
/// - tag:
291-
public init(name: Swift.String, kind: Components.Schemas.PetKind? = nil, tag: Swift.String? = nil) {
301+
/// - genome:
302+
public init(
303+
name: Swift.String,
304+
kind: Components.Schemas.PetKind? = nil,
305+
tag: Swift.String? = nil,
306+
genome: OpenAPIRuntime.Base64EncodedData? = nil
307+
) {
292308
self.name = name
293309
self.kind = kind
294310
self.tag = tag
311+
self.genome = genome
295312
}
296313
public enum CodingKeys: String, CodingKey {
297314
case name
298315
case kind
299316
case tag
317+
case genome
300318
}
301319
}
302320
/// - Remark: Generated from `#/components/schemas/Pets`.

Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,47 @@ final class SnippetBasedReferenceTests: XCTestCase {
961961
)
962962
}
963963

964+
func testComponentsSchemasBase64() throws {
965+
try self.assertSchemasTranslation(
966+
"""
967+
schemas:
968+
MyData:
969+
type: string
970+
format: byte
971+
""",
972+
"""
973+
public enum Schemas {
974+
public typealias MyData = OpenAPIRuntime.Base64EncodedData
975+
}
976+
"""
977+
)
978+
}
979+
980+
func testComponentsSchemasBase64Object() throws {
981+
try self.assertSchemasTranslation(
982+
"""
983+
schemas:
984+
MyObj:
985+
type: object
986+
properties:
987+
stuff:
988+
type: string
989+
format: byte
990+
""",
991+
"""
992+
public enum Schemas {
993+
public struct MyObj: Codable, Hashable, Sendable {
994+
public var stuff: OpenAPIRuntime.Base64EncodedData?
995+
public init(stuff: OpenAPIRuntime.Base64EncodedData? = nil) {
996+
self.stuff = stuff
997+
}
998+
public enum CodingKeys: String, CodingKey { case stuff }
999+
}
1000+
}
1001+
"""
1002+
)
1003+
}
1004+
9641005
func testComponentsResponsesResponseNoBody() throws {
9651006
try self.assertResponsesTranslation(
9661007
"""
@@ -1764,6 +1805,46 @@ final class SnippetBasedReferenceTests: XCTestCase {
17641805
"""
17651806
)
17661807
}
1808+
1809+
func testResponseWithExampleWithOnlyValueByte() throws {
1810+
try self.assertResponsesTranslation(
1811+
featureFlags: [.base64DataEncodingDecoding],
1812+
"""
1813+
responses:
1814+
MyResponse:
1815+
description: Some response
1816+
content:
1817+
application/json:
1818+
schema:
1819+
type: string
1820+
format: byte
1821+
examples:
1822+
application/json:
1823+
summary: "a hello response"
1824+
""",
1825+
"""
1826+
public enum Responses {
1827+
public struct MyResponse: Sendable, Hashable {
1828+
@frozen public enum Body: Sendable, Hashable {
1829+
case json(OpenAPIRuntime.Base64EncodedData)
1830+
public var json: OpenAPIRuntime.Base64EncodedData {
1831+
get throws {
1832+
switch self { case let .json(body): return body }
1833+
}
1834+
}
1835+
}
1836+
public var body: Components.Responses.MyResponse.Body
1837+
public init(
1838+
body: Components.Responses.MyResponse.Body
1839+
) {
1840+
self.body = body
1841+
}
1842+
}
1843+
}
1844+
"""
1845+
)
1846+
}
1847+
17671848
}
17681849

17691850
extension SnippetBasedReferenceTests {

Tests/PetstoreConsumerTests/Test_Client.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,87 @@ final class Test_Client: XCTestCase {
357357
}
358358
}
359359

360+
func testCreatePet_201_withBase64() async throws {
361+
transport = .init { request, body, baseURL, operationID in
362+
XCTAssertEqual(operationID, "createPet")
363+
XCTAssertEqual(request.path, "/pets")
364+
XCTAssertEqual(baseURL.absoluteString, "/api")
365+
XCTAssertEqual(request.method, .post)
366+
XCTAssertEqual(
367+
request.headerFields,
368+
[
369+
.accept: "application/json",
370+
.contentType: "application/json; charset=utf-8",
371+
.init("X-Extra-Arguments")!: #"{"code":1}"#,
372+
]
373+
)
374+
let bodyString: String
375+
if let body {
376+
bodyString = try await String(collecting: body, upTo: .max)
377+
} else {
378+
bodyString = ""
379+
}
380+
XCTAssertEqual(
381+
bodyString,
382+
#"""
383+
{
384+
"genome" : "IkdBQ1RBVFRDQVRBR0FHVFRUQ0FDQ1RDQUdHQUdBR0FHQUFHVEFBR0NBVFRBR0NBR0NUR0Mi",
385+
"name" : "Fluffz"
386+
}
387+
"""#
388+
)
389+
return try HTTPResponse(
390+
status: .created,
391+
headerFields: [
392+
.contentType: "application/json; charset=utf-8",
393+
.init("x-extra-arguments")!: #"{"code":1}"#,
394+
]
395+
)
396+
.withEncodedBody(
397+
#"""
398+
{
399+
"id": 1,
400+
"genome" : "IkdBQ1RBVFRDQVRBR0FHVFRUQ0FDQ1RDQUdHQUdBR0FHQUFHVEFBR0NBVFRBR0NBR0NUR0Mi",
401+
"name": "Fluffz"
402+
}
403+
"""#
404+
)
405+
}
406+
let response = try await client.createPet(
407+
.init(
408+
headers: .init(
409+
X_hyphen_Extra_hyphen_Arguments: .init(code: 1)
410+
),
411+
body: .json(
412+
.init(
413+
name: "Fluffz",
414+
genome: Base64EncodedData(
415+
data: ArraySlice(#""GACTATTCATAGAGTTTCACCTCAGGAGAGAGAAGTAAGCATTAGCAGCTGC""#.utf8)
416+
)
417+
)
418+
)
419+
)
420+
)
421+
guard case let .created(value) = response else {
422+
XCTFail("Unexpected response: \(response)")
423+
return
424+
}
425+
XCTAssertEqual(value.headers.X_hyphen_Extra_hyphen_Arguments, .init(code: 1))
426+
switch value.body {
427+
case .json(let pets):
428+
XCTAssertEqual(
429+
pets,
430+
.init(
431+
id: 1,
432+
name: "Fluffz",
433+
genome: Base64EncodedData(
434+
data: ArraySlice(#""GACTATTCATAGAGTTTCACCTCAGGAGAGAGAAGTAAGCATTAGCAGCTGC""#.utf8)
435+
)
436+
)
437+
)
438+
}
439+
}
440+
360441
func testUpdatePet_400() async throws {
361442
transport = .init { request, requestBody, baseURL, operationID in
362443
XCTAssertEqual(operationID, "updatePet")

0 commit comments

Comments
 (0)