Skip to content

Add convenience initializers for primitive-typed multipart parts #775

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ extension FileTranslator {
let propertyDecls = allProperties.flatMap(translatePropertyBlueprint)

var members = propertyDecls
let initDecl = translateStructBlueprintInitializer(typeName: typeName, properties: allProperties)
members.append(initDecl)
let initializers = translateStructBlueprintInitializers(
typeName: typeName,
properties: allProperties,
initializerContext: blueprint.initializerContext
)
members.append(contentsOf: initializers)

if blueprint.shouldGenerateCodingKeys && !serializableProperties.isEmpty {
let codingKeysDecl = translateStructBlueprintCodingKeys(properties: serializableProperties)
Expand Down Expand Up @@ -60,13 +64,46 @@ extension FileTranslator {
return .commentable(blueprint.comment, .struct(structDesc).deprecate(if: blueprint.isDeprecated))
}

/// Returns a declaration of an initializer declared in a structure.
/// Returns declarations of initializers for a structure.
/// - Parameters:
/// - typeName: The type name of the structure.
/// - properties: The properties to include in the initializer.
/// - Returns: A `Declaration` representing the translated struct.
func translateStructBlueprintInitializer(typeName: TypeName, properties: [PropertyBlueprint]) -> Declaration {
/// - initializerContext: Context that determines what initializers to generate.
/// - Returns: An array of `Declaration` representing the initializers.
func translateStructBlueprintInitializers(
typeName: TypeName,
properties: [PropertyBlueprint],
initializerContext: StructBlueprint.InitializerContext
) -> [Declaration] {
var initializers: [Declaration] = []

// Always include the memberwise initializer
let memberwiseInit = translateMemberwiseInitializer(typeName: typeName, properties: properties)
initializers.append(memberwiseInit)

// Add context-specific initializers
switch initializerContext {
case .memberwise:
break // No additional initializers
case .multipartPayload(let originalSchema):
if let valueInit = translateMultipartValueInitializer(
typeName: typeName,
properties: properties,
originalSchema: originalSchema
) {
initializers.append(valueInit)
}
}

return initializers
}

/// Returns a declaration of the memberwise initializer for a structure.
/// - Parameters:
/// - typeName: The type name of the structure.
/// - properties: The properties to include in the initializer.
/// - Returns: A `Declaration` representing the memberwise initializer.
func translateMemberwiseInitializer(typeName: TypeName, properties: [PropertyBlueprint]) -> Declaration {
let comment: Comment = properties.initializerComment(typeName: typeName.shortSwiftName)

let decls: [(ParameterDescription, String)] = properties.map { property in
Expand Down Expand Up @@ -95,6 +132,110 @@ extension FileTranslator {
)
}

/// Returns a value initializer for multipart payload structs with primitive types.
/// - Parameters:
/// - typeName: The type name of the structure.
/// - properties: The properties of the structure.
/// - originalSchema: The original schema before any transformations.
/// - Returns: A value initializer declaration if applicable, nil otherwise.
func translateMultipartValueInitializer(
typeName: TypeName,
properties: [PropertyBlueprint],
originalSchema: JSONSchema
) -> Declaration? {
let typeMatcher = TypeMatcher(context: context)
guard let matchedType = typeMatcher.tryMatchBuiltinType(for: originalSchema.value) else {
return nil
}

let valueTypeName = matchedType.typeName
let needsStringConversion: Bool

switch originalSchema.value {
case .integer, .boolean, .number:
// For these types, if tryMatchBuiltinType succeeded, it means they are
// simple primitive types (e.g., Int, Bool, Double) and not enums.
// They will be converted to String for the HTTPBody.
needsStringConversion = true
case .string:
// For string schemas, we must ensure it's a plain Swift.String.
// Other string-based formats (Date, binary Data, base64 Data)
// are not handled by this specific "value" initializer.
guard valueTypeName == .string else { return nil }
needsStringConversion = false
case .object, .array, .all, .one, .any, .not, .reference, .fragment, .null:
// Other schema types are not supported for this value initializer,
// even if they might be considered "built-in" by the TypeMatcher
// for other purposes.
return nil
}

// Find headers and body properties
let headersProperty = properties.first { $0.originalName == Constants.Operation.Output.Payload.Headers.variableName }
let bodyProperty = properties.first { $0.originalName == Constants.Operation.Body.variableName }

// This initializer requires a body part to set the value.
guard bodyProperty != nil else { return nil }

var parameters: [ParameterDescription] = []

// Add headers parameter if a headers property exists
if let headersProperty {
parameters.append(ParameterDescription(
label: headersProperty.swiftSafeName,
type: .init(headersProperty.typeUsage),
defaultValue: nil
))
}

// Add the main value parameter
parameters.append(ParameterDescription(
label: "value",
type: .init(valueTypeName.asUsage),
defaultValue: nil
))

let bodyContentExpression: Expression
if needsStringConversion {
bodyContentExpression = .functionCall(
calledExpression: .identifierType(TypeName.string),
arguments: [.init(label: nil, expression: .identifierPattern("value"))]
)
} else {
bodyContentExpression = .identifierPattern("value")
}

var bodyStatements: [CodeBlock] = []

// Assign headers if provided
if let headersProperty {
bodyStatements.append(.expression(
.assignment(
left: .identifierPattern("self").dot(headersProperty.swiftSafeName),
right: .identifierPattern(headersProperty.swiftSafeName)
)
))
}

// Initialize the body property using the value
bodyStatements.append(.expression(
.assignment(
left: .identifierPattern("self").dot(Constants.Operation.Body.variableName),
right: .functionCall(
calledExpression: .identifierType(TypeName.runtime("HTTPBody")),
arguments: [.init(label: nil, expression: bodyContentExpression)]
)
)
))

return .function(
accessModifier: config.access,
kind: .initializer(failable: false),
parameters: parameters,
body: bodyStatements
)
}

/// Returns a list of declarations for a specified property blueprint.
///
/// May return multiple declarations when the property contains an unnamed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ struct StructBlueprint {

/// The properties of the structure.
var properties: [PropertyBlueprint]

/// Context information for generating additional initializers.
enum InitializerContext {
/// Standard struct with only the memberwise initializer.
case memberwise

/// Multipart payload struct that may need value initializers.
case multipartPayload(originalSchema: JSONSchema)
}

/// The context for initializer generation.
var initializerContext: InitializerContext = .memberwise
}

extension StructBlueprint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,33 @@ struct MultipartContent {
/// A container of information about an individual multipart part.
enum MultipartSchemaTypedContent {

/// The associated data with the `documentedTyped` case.
struct DocumentedTypeInfo {
/// A part that has a statically known name and type.
struct DocumentedMultipartPart {

/// The original name of the case from the OpenAPI document.
/// The name of the part as specified in the OpenAPI document.
var originalName: String

/// The type name of the part wrapper.
/// The Swift type name that represents the part.
var typeName: TypeName

/// Information about the kind of the part.
/// Information about the part's kind, content type, etc.
var partInfo: MultipartPartInfo

/// The value schema of the part defined in the OpenAPI document.
/// The schema of the part.
///
/// This schema might have been transformed by the `MultipartContentInspector`
/// (e.g., an integer might become a string with binary encoding).
var schema: JSONSchema

/// The headers defined for the part in the OpenAPI document.
/// The original schema of the part, before any transformation by `MultipartContentInspector`.
var originalSchema: JSONSchema

/// The headers of the part.
var headers: OpenAPI.Header.Map?
}
/// A documented part with a name specified in the OpenAPI document.
case documentedTyped(DocumentedTypeInfo)

/// A documented part with a statically known name and type.
case documentedTyped(DocumentedMultipartPart)

/// The associated data with the `otherDynamicallyNamed` case.
struct OtherDynamicallyNamedInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ extension FileTranslator {
typeName: typeName,
partInfo: info,
schema: resolvedSchema,
originalSchema: value,
headers: partEncoding?.headers
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ extension TypesFileTranslator {
typeName: TypeName,
headers headerMap: OpenAPI.Header.Map?,
contentType: ContentType,
schema: JSONSchema
schema: JSONSchema,
originalSchema: JSONSchema
) throws -> Declaration {
let headersTypeName = typeName.appending(
swiftComponent: Constants.Operation.Output.Payload.Headers.typeName,
Expand Down Expand Up @@ -98,7 +99,8 @@ extension TypesFileTranslator {
access: config.access,
typeName: typeName,
conformances: Constants.Operation.Output.Payload.conformances,
properties: [headersProperty, bodyProperty].compactMap { $0 }
properties: [headersProperty, bodyProperty].compactMap { $0 },
initializerContext: .multipartPayload(originalSchema: originalSchema)
)
)
return .commentable(typeName.docCommentWithUserDescription(nil), structDecl)
Expand Down Expand Up @@ -144,7 +146,8 @@ extension TypesFileTranslator {
typeName: documentedPart.typeName,
headers: documentedPart.headers,
contentType: documentedPart.partInfo.contentType,
schema: documentedPart.schema
schema: documentedPart.schema,
originalSchema: documentedPart.originalSchema
)
return [decl, caseDecl]
case .otherDynamicallyNamed(let dynamicallyNamedPart):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ extension TypeName {
/// Returns the type name for the Int type.
static var int: Self { .swift("Int") }

/// Returns the type name for the Bool type.
static var bool: Self { .swift("Bool") }

/// Returns the type name for the Double type.
static var double: Self { .swift("Double") }

/// Returns a type name for a type with the specified name in the
/// Swift module.
/// - Parameter name: The name of the type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,13 @@ public enum Components {
self.headers = headers
self.body = body
}
public init(
headers: Components.RequestBodies.MultipartUploadTypedRequest.MultipartFormPayload.LogPayload.Headers,
value: Swift.String
) {
self.headers = headers
self.body = OpenAPIRuntime.HTTPBody(value)
}
}
case log(OpenAPIRuntime.MultipartPart<Components.RequestBodies.MultipartUploadTypedRequest.MultipartFormPayload.LogPayload>)
/// - Remark: Generated from `#/components/requestBodies/MultipartUploadTypedRequest/multipartForm/metadata`.
Expand Down Expand Up @@ -1780,6 +1787,13 @@ public enum Components {
self.headers = headers
self.body = body
}
public init(
headers: Components.Responses.MultipartDownloadTypedResponse.Body.MultipartFormPayload.LogPayload.Headers,
value: Swift.String
) {
self.headers = headers
self.body = OpenAPIRuntime.HTTPBody(value)
}
}
case log(OpenAPIRuntime.MultipartPart<Components.Responses.MultipartDownloadTypedResponse.Body.MultipartFormPayload.LogPayload>)
/// - Remark: Generated from `#/components/responses/MultipartDownloadTypedResponse/content/multipartForm/metadata`.
Expand Down
Loading