Skip to content

Commit

Permalink
Fix multipart + additionalProperties + string support
Browse files Browse the repository at this point in the history
  • Loading branch information
czechboy0 committed Jul 22, 2024
1 parent 285ebba commit b97f18d
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ DerivedData/
/Package.resolved
.ci/
.docc-build/
.swiftpm
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,6 @@ extension FileTranslator {
case .disallowed: break
case .allowed: parts.append(.undocumented)
case .typed(let schema):
let typeUsage = try typeAssigner.typeUsage(
usingNamingHint: Constants.AdditionalProperties.variableName,
withSchema: .b(schema),
components: components,
inParent: typeName
)!
// The unwrap is safe, the method only returns nil when the input schema is nil.
let typeName = typeUsage.typeName
guard
let (info, resolvedSchema) = try parseMultipartPartInfo(
schema: schema,
Expand All @@ -167,7 +159,15 @@ extension FileTranslator {
message: "Failed to parse multipart info for additionalProperties in \(typeName.description)."
)
}
parts.append(.otherDynamicallyNamed(.init(typeName: typeName, partInfo: info, schema: resolvedSchema)))
let partTypeUsage = try typeAssigner.typeUsage(
usingNamingHint: Constants.AdditionalProperties.variableName,
withSchema: .b(resolvedSchema),
components: components,
inParent: typeName
)!
// The unwrap is safe, the method only returns nil when the input schema is nil.
let partTypeName = partTypeUsage.typeName
parts.append(.otherDynamicallyNamed(.init(typeName: partTypeName, partInfo: info, schema: resolvedSchema)))
case .any: parts.append(.otherRaw)
}
let requirements = try parseMultipartRequirements(
Expand Down
2 changes: 1 addition & 1 deletion Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ extension BindingKind {
}
}

extension Expression {
extension _OpenAPIGeneratorCore.Expression {
var info: ExprInfo {
switch self {
case .literal(let value): return .init(name: value.name, kind: .literal)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,17 @@ extension FileBasedReferenceTests {
)
}

private func temporaryDirectory(fileManager: FileManager = .default) throws -> URL {
let directoryURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
private func temporaryDirectory() throws -> URL {
let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(
UUID().uuidString,
isDirectory: true
)
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
addTeardownBlock {
do {
if fileManager.fileExists(atPath: directoryURL.path) {
try fileManager.removeItem(at: directoryURL)
XCTAssertFalse(fileManager.fileExists(atPath: directoryURL.path))
if FileManager.default.fileExists(atPath: directoryURL.path) {
try FileManager.default.removeItem(at: directoryURL)
XCTAssertFalse(FileManager.default.fileExists(atPath: directoryURL.path))
}
} catch {
// Treat any errors during file deletion as a test failure.
Expand Down
136 changes: 135 additions & 1 deletion Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4457,6 +4457,140 @@ final class SnippetBasedReferenceTests: XCTestCase {
)
}

func testRequestMultipartBodyAdditionalPropertiesSchemaBuiltin() throws {
try self.assertRequestInTypesClientServerTranslation(
"""
/foo:
post:
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
additionalProperties:
type: string
responses:
default:
description: Response
""",
types: """
public struct Input: Sendable, Hashable {
@frozen public enum Body: Sendable, Hashable {
@frozen public enum multipartFormPayload: Sendable, Hashable {
case additionalProperties(OpenAPIRuntime.MultipartDynamicallyNamedPart<OpenAPIRuntime.HTTPBody>)
}
case multipartForm(OpenAPIRuntime.MultipartBody<Operations.post_sol_foo.Input.Body.multipartFormPayload>)
}
public var body: Operations.post_sol_foo.Input.Body
public init(body: Operations.post_sol_foo.Input.Body) {
self.body = body
}
}
""",
client: """
{ input in
let path = try converter.renderedPath(
template: "/foo",
parameters: []
)
var request: HTTPTypes.HTTPRequest = .init(
soar_path: path,
method: .post
)
suppressMutabilityWarning(&request)
let body: OpenAPIRuntime.HTTPBody?
switch input.body {
case let .multipartForm(value):
body = try converter.setRequiredRequestBodyAsMultipart(
value,
headerFields: &request.headerFields,
contentType: "multipart/form-data",
allowsUnknownParts: true,
requiredExactlyOncePartNames: [],
requiredAtLeastOncePartNames: [],
atMostOncePartNames: [],
zeroOrMoreTimesPartNames: [],
encoding: { part in
switch part {
case let .additionalProperties(wrapped):
var headerFields: HTTPTypes.HTTPFields = .init()
let value = wrapped.payload
let body = try converter.setRequiredRequestBodyAsBinary(
value,
headerFields: &headerFields,
contentType: "text/plain"
)
return .init(
name: wrapped.name,
filename: wrapped.filename,
headerFields: headerFields,
body: body
)
}
}
)
}
return (request, body)
}
""",
server: """
{ request, requestBody, metadata in
let contentType = converter.extractContentTypeIfPresent(in: request.headerFields)
let body: Operations.post_sol_foo.Input.Body
let chosenContentType = try converter.bestContentType(
received: contentType,
options: [
"multipart/form-data"
]
)
switch chosenContentType {
case "multipart/form-data":
body = try converter.getRequiredRequestBodyAsMultipart(
OpenAPIRuntime.MultipartBody<Operations.post_sol_foo.Input.Body.multipartFormPayload>.self,
from: requestBody,
transforming: { value in
.multipartForm(value)
},
boundary: contentType.requiredBoundary(),
allowsUnknownParts: true,
requiredExactlyOncePartNames: [],
requiredAtLeastOncePartNames: [],
atMostOncePartNames: [],
zeroOrMoreTimesPartNames: [],
decoding: { part in
let headerFields = part.headerFields
let (name, filename) = try converter.extractContentDispositionNameAndFilename(in: headerFields)
switch name {
default:
try converter.verifyContentTypeIfPresent(
in: headerFields,
matches: "text/plain"
)
let body = try converter.getRequiredRequestBodyAsBinary(
OpenAPIRuntime.HTTPBody.self,
from: part.body,
transforming: {
$0
}
)
return .additionalProperties(.init(
payload: body,
filename: filename,
name: name
))
}
}
)
default:
preconditionFailure("bestContentType chose an invalid content type.")
}
return Operations.post_sol_foo.Input(body: body)
}
"""
)
}

func testResponseMultipartReferencedResponse() throws {
try self.assertResponseInTypesClientServerTranslation(
"""
Expand Down Expand Up @@ -5211,7 +5345,7 @@ private func XCTAssertSwiftEquivalent(
}

private func XCTAssertSwiftEquivalent(
_ expression: Expression,
_ expression: _OpenAPIGeneratorCore.Expression,
_ expectedSwift: String,
file: StaticString = #filePath,
line: UInt = #line
Expand Down

0 comments on commit b97f18d

Please sign in to comment.