Skip to content
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

Fix multipart + additionalProperties + string support #597

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
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)))
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
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 {
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
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))
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
}
} 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 @@ -4567,6 +4567,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 @@ -5323,7 +5457,7 @@ private func XCTAssertSwiftEquivalent(
}

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