Skip to content

Support base64-encoded data #326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ let package = Package(
// Tests-only: Runtime library linked by generated code, and also
// helps keep the runtime library new enough to work with the generated
// code.
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.1")),
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.2")),

// Build and preview docs
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
Expand Down
5 changes: 5 additions & 0 deletions Sources/_OpenAPIGeneratorCore/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
public enum FeatureFlag: String, Hashable, Codable, CaseIterable, Sendable {
// needs to be here for the enum to compile
case empty

/// Base64 encoding and decoding.
///
/// Enable interpretation of `type: string, format: byte` as base64-encoded data.
case base64DataEncodingDecoding
}

/// A set of enabled feature flags.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ struct TypeAssigner {
/// safe to be used as a Swift identifier.
var asSwiftSafeName: (String) -> String

///Enable decoding and encoding of as base64-encoded data strings.
var enableBase64EncodingDecoding: Bool

/// Returns a type name for an OpenAPI-named component type.
///
/// A component type is any type in `#/components` in the OpenAPI document.
Expand Down Expand Up @@ -270,7 +273,8 @@ struct TypeAssigner {
subtype: SubtypeNamingMethod
) throws -> TypeUsage {
let typeMatcher = TypeMatcher(
asSwiftSafeName: asSwiftSafeName
asSwiftSafeName: asSwiftSafeName,
enableBase64EncodingDecoding: enableBase64EncodingDecoding
)
// Check if this type can be simply referenced without
// creating a new inline type.
Expand Down Expand Up @@ -472,14 +476,16 @@ extension FileTranslator {
/// A configured type assigner.
var typeAssigner: TypeAssigner {
TypeAssigner(
asSwiftSafeName: swiftSafeName
asSwiftSafeName: swiftSafeName,
enableBase64EncodingDecoding: config.featureFlags.contains(.base64DataEncodingDecoding)
)
}

/// A configured type matcher.
var typeMatcher: TypeMatcher {
TypeMatcher(
asSwiftSafeName: swiftSafeName
asSwiftSafeName: swiftSafeName,
enableBase64EncodingDecoding: config.featureFlags.contains(.base64DataEncodingDecoding)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ struct TypeMatcher {
/// safe to be used as a Swift identifier.
var asSwiftSafeName: (String) -> String

///Enable decoding and encoding of as base64-encoded data strings.
var enableBase64EncodingDecoding: Bool

/// Returns the type name of a built-in type that matches the specified
/// schema.
///
Expand Down Expand Up @@ -82,7 +85,8 @@ struct TypeMatcher {
return nil
}
return try TypeAssigner(
asSwiftSafeName: asSwiftSafeName
asSwiftSafeName: asSwiftSafeName,
enableBase64EncodingDecoding: enableBase64EncodingDecoding
)
.typeName(for: ref).asUsage
},
Expand Down Expand Up @@ -331,10 +335,10 @@ struct TypeMatcher {
return nil
}
switch core.format {
case .byte:
typeName = .swift("String")
case .binary:
typeName = .body
case .byte:
typeName = .runtime("Base64EncodedData")
case .dateTime:
typeName = .foundation("Date")
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ final class Test_TypeMatcher: Test_Core {

static let builtinTypes: [(JSONSchema, String)] = [
(.string, "Swift.String"),
(.string(.init(format: .byte), .init()), "Swift.String"),
(.string(.init(format: .binary), .init()), "OpenAPIRuntime.HTTPBody"),
(.string(.init(format: .byte), .init()), "OpenAPIRuntime.Base64EncodedData"),
(.string(.init(format: .date), .init()), "Swift.String"),
(.string(.init(format: .dateTime), .init()), "Foundation.Date"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ components:
type: string
tag:
type: string
genome:
description: "Pet genome (base64-encoded)"
type: string
format: byte
kind:
$ref: '#/components/schemas/PetKind'
MixedAnyOf:
Expand Down Expand Up @@ -307,6 +311,9 @@ components:
$ref: '#/components/schemas/PetKind'
tag:
type: string
genome:
type: string
format: byte
Pets:
type: array
items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ public enum Components {
public var name: Swift.String
/// - Remark: Generated from `#/components/schemas/Pet/tag`.
public var tag: Swift.String?
/// Pet genome (base64-encoded)
///
/// - Remark: Generated from `#/components/schemas/Pet/genome`.
public var genome: OpenAPIRuntime.Base64EncodedData?
/// - Remark: Generated from `#/components/schemas/Pet/kind`.
public var kind: Components.Schemas.PetKind?
/// Creates a new `Pet`.
Expand All @@ -148,22 +152,26 @@ public enum Components {
/// - id: Pet id
/// - name: Pet name
/// - tag:
/// - genome: Pet genome (base64-encoded)
/// - kind:
public init(
id: Swift.Int64,
name: Swift.String,
tag: Swift.String? = nil,
genome: OpenAPIRuntime.Base64EncodedData? = nil,
kind: Components.Schemas.PetKind? = nil
) {
self.id = id
self.name = name
self.tag = tag
self.genome = genome
self.kind = kind
}
public enum CodingKeys: String, CodingKey {
case id
case name
case tag
case genome
case kind
}
}
Expand Down Expand Up @@ -282,21 +290,31 @@ public enum Components {
public var kind: Components.Schemas.PetKind?
/// - Remark: Generated from `#/components/schemas/CreatePetRequest/tag`.
public var tag: Swift.String?
/// - Remark: Generated from `#/components/schemas/CreatePetRequest/genome`.
public var genome: OpenAPIRuntime.Base64EncodedData?
/// Creates a new `CreatePetRequest`.
///
/// - Parameters:
/// - name:
/// - kind:
/// - tag:
public init(name: Swift.String, kind: Components.Schemas.PetKind? = nil, tag: Swift.String? = nil) {
/// - genome:
public init(
name: Swift.String,
kind: Components.Schemas.PetKind? = nil,
tag: Swift.String? = nil,
genome: OpenAPIRuntime.Base64EncodedData? = nil
) {
self.name = name
self.kind = kind
self.tag = tag
self.genome = genome
}
public enum CodingKeys: String, CodingKey {
case name
case kind
case tag
case genome
}
}
/// - Remark: Generated from `#/components/schemas/Pets`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,47 @@ final class SnippetBasedReferenceTests: XCTestCase {
)
}

func testComponentsSchemasBase64() throws {
try self.assertSchemasTranslation(
"""
schemas:
MyData:
type: string
format: byte
""",
"""
public enum Schemas {
public typealias MyData = OpenAPIRuntime.Base64EncodedData
}
"""
)
}

func testComponentsSchemasBase64Object() throws {
try self.assertSchemasTranslation(
"""
schemas:
MyObj:
type: object
properties:
stuff:
type: string
format: byte
""",
"""
public enum Schemas {
public struct MyObj: Codable, Hashable, Sendable {
public var stuff: OpenAPIRuntime.Base64EncodedData?
public init(stuff: OpenAPIRuntime.Base64EncodedData? = nil) {
self.stuff = stuff
}
public enum CodingKeys: String, CodingKey { case stuff }
}
}
"""
)
}

func testComponentsResponsesResponseNoBody() throws {
try self.assertResponsesTranslation(
"""
Expand Down Expand Up @@ -1764,6 +1805,46 @@ final class SnippetBasedReferenceTests: XCTestCase {
"""
)
}

func testResponseWithExampleWithOnlyValueByte() throws {
try self.assertResponsesTranslation(
featureFlags: [.base64DataEncodingDecoding],
"""
responses:
MyResponse:
description: Some response
content:
application/json:
schema:
type: string
format: byte
examples:
application/json:
summary: "a hello response"
""",
"""
public enum Responses {
public struct MyResponse: Sendable, Hashable {
@frozen public enum Body: Sendable, Hashable {
case json(OpenAPIRuntime.Base64EncodedData)
public var json: OpenAPIRuntime.Base64EncodedData {
get throws {
switch self { case let .json(body): return body }
}
}
}
public var body: Components.Responses.MyResponse.Body
public init(
body: Components.Responses.MyResponse.Body
) {
self.body = body
}
}
}
"""
)
}

}

extension SnippetBasedReferenceTests {
Expand Down
81 changes: 81 additions & 0 deletions Tests/PetstoreConsumerTests/Test_Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,87 @@ final class Test_Client: XCTestCase {
}
}

func testCreatePet_201_withBase64() async throws {
transport = .init { request, body, baseURL, operationID in
XCTAssertEqual(operationID, "createPet")
XCTAssertEqual(request.path, "/pets")
XCTAssertEqual(baseURL.absoluteString, "/api")
XCTAssertEqual(request.method, .post)
XCTAssertEqual(
request.headerFields,
[
.accept: "application/json",
.contentType: "application/json; charset=utf-8",
.init("X-Extra-Arguments")!: #"{"code":1}"#,
]
)
let bodyString: String
if let body {
bodyString = try await String(collecting: body, upTo: .max)
} else {
bodyString = ""
}
XCTAssertEqual(
bodyString,
#"""
{
"genome" : "IkdBQ1RBVFRDQVRBR0FHVFRUQ0FDQ1RDQUdHQUdBR0FHQUFHVEFBR0NBVFRBR0NBR0NUR0Mi",
"name" : "Fluffz"
}
"""#
)
return try HTTPResponse(
status: .created,
headerFields: [
.contentType: "application/json; charset=utf-8",
.init("x-extra-arguments")!: #"{"code":1}"#,
]
)
.withEncodedBody(
#"""
{
"id": 1,
"genome" : "IkdBQ1RBVFRDQVRBR0FHVFRUQ0FDQ1RDQUdHQUdBR0FHQUFHVEFBR0NBVFRBR0NBR0NUR0Mi",
"name": "Fluffz"
}
"""#
)
}
let response = try await client.createPet(
.init(
headers: .init(
X_hyphen_Extra_hyphen_Arguments: .init(code: 1)
),
body: .json(
.init(
name: "Fluffz",
genome: Base64EncodedData(
data: ArraySlice(#""GACTATTCATAGAGTTTCACCTCAGGAGAGAGAAGTAAGCATTAGCAGCTGC""#.utf8)
)
)
)
)
)
guard case let .created(value) = response else {
XCTFail("Unexpected response: \(response)")
return
}
XCTAssertEqual(value.headers.X_hyphen_Extra_hyphen_Arguments, .init(code: 1))
switch value.body {
case .json(let pets):
XCTAssertEqual(
pets,
.init(
id: 1,
name: "Fluffz",
genome: Base64EncodedData(
data: ArraySlice(#""GACTATTCATAGAGTTTCACCTCAGGAGAGAGAAGTAAGCATTAGCAGCTGC""#.utf8)
)
)
)
}
}

func testUpdatePet_400() async throws {
transport = .init { request, requestBody, baseURL, operationID in
XCTAssertEqual(operationID, "updatePet")
Expand Down
Loading