Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
495f2e1
Disable input/output serializers
jbelkins Nov 4, 2025
ca5b331
add ShapeID, doc comments
jbelkins Nov 5, 2025
212120e
Re-enable serde code
jbelkins Nov 5, 2025
fcdf1bf
Cleanup
jbelkins Nov 5, 2025
b2152c2
Merge branch 'main' into jbe/add_schema
jbelkins Nov 7, 2025
88f7be9
Merge branch 'main' into jbe/add_schema
jbelkins Nov 8, 2025
aa823be
feat: Add Swift-native codegen plugin
jbelkins Nov 9, 2025
e817d55
Fix lint
jbelkins Nov 9, 2025
6a9c537
Make service trait optional
jbelkins Nov 9, 2025
33cc093
Fix ktlint again
jbelkins Nov 9, 2025
6b54575
Refactor code generator plugin
jbelkins Nov 9, 2025
ae474f1
Convert smithy model info file to .json
jbelkins Nov 9, 2025
70f7669
Fix Swift 5.9 URL initializer
jbelkins Nov 9, 2025
3d623e6
Code cleanup
jbelkins Nov 9, 2025
d535e66
Revert disabling of schemas
jbelkins Nov 9, 2025
52f9532
Schemas generate for entire SDK
jbelkins Nov 15, 2025
1fa7fbd
Merge branch 'epic/sbs' into jbe/swift_codegen_schema
jbelkins Nov 19, 2025
70e8a15
Fix method name renderSchema
jbelkins Nov 19, 2025
b1548a7
Fix swiftlint
jbelkins Nov 19, 2025
94a901d
Fix ktlint
jbelkins Nov 19, 2025
54a4300
Fix SmithyCodegenCoreTests
jbelkins Nov 19, 2025
6b860df
Merge branch 'epic/sbs' into jbe/swift_codegen_schema
jbelkins Nov 22, 2025
1eb996b
Merge branch 'epic/sbs' into jbe/swift_codegen_schema
jbelkins Nov 26, 2025
3fd9854
Cleanup
jbelkins Dec 5, 2025
64dd13a
Merge branch 'epic/sbs' into jbe/swift_codegen_schema
jbelkins Dec 9, 2025
6a65655
Fixes
jbelkins Dec 9, 2025
db5b76a
Merge branch 'epic/sbs' into jbe/swift_codegen_schema
jbelkins Dec 12, 2025
64eeff9
More codegen added
jbelkins Dec 12, 2025
953e1d9
Merge branch 'epic/sbs' into jbe/swift_codegen_schema
jbelkins Dec 15, 2025
260e5ca
Serializes to string
jbelkins Dec 17, 2025
2e73154
Serializes maps & lists to string
jbelkins Dec 17, 2025
0a3b8c6
Fix rendered string for structure
jbelkins Dec 18, 2025
05d8088
Convert ShapeSerializer methods to throwing
jbelkins Dec 19, 2025
dbfb2af
Add deser code
jbelkins Dec 22, 2025
b19ab8c
Clarify doc comment
jbelkins Dec 22, 2025
d99a7c0
Merge branch 'epic/sbs' into jbe/swift_codegen_schema
jbelkins Dec 22, 2025
811c8cc
Comments, fix swiftlint
jbelkins Dec 22, 2025
1005d47
Refine serializer interfaces
jbelkins Dec 22, 2025
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
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,9 @@ let package = Package(
name: "SmithyCodegenCore",
dependencies: [
"Smithy",
]
"SmithySerialization",
],
resources: [ .process("Resources") ]
),
.testTarget(
name: "ClientRuntimeTests",
Expand Down Expand Up @@ -400,5 +402,9 @@ let package = Package(
name: "SmithyStreamsTests",
dependencies: ["SmithyStreams", "Smithy"]
),
.testTarget(
name: "SmithyCodegenCoreTests",
dependencies: ["SmithyCodegenCore"]
),
].compactMap { $0 }
)
12 changes: 10 additions & 2 deletions Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,27 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin {
let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path)
let modelPath = Path(modelPathURL.path)

// Construct the schemas.swift path.
// Construct the Schemas.swift path.
let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift")

// Construct the Serialize.swift path.
let serializeSwiftPath = outputDirectoryPath.appending("\(name)Serialize.swift")

// Construct the Deserialize.swift path.
let deserializeSwiftPath = outputDirectoryPath.appending("\(name)Deserialize.swift")

// Construct the build command that invokes SmithyCodegenCLI.
return .buildCommand(
displayName: "Generating Swift source files from model file \(smithyModelInfo.path)",
executable: generatorToolPath,
arguments: [
"--schemas-path", schemasSwiftPath,
"--serialize-path", serializeSwiftPath,
"--deserialize-path", deserializeSwiftPath,
modelPath
],
inputFiles: [inputPath, modelPath],
outputFiles: [schemasSwiftPath]
outputFiles: [schemasSwiftPath, serializeSwiftPath, deserializeSwiftPath]
)
}
}
Expand Down
File renamed without changes.
24 changes: 10 additions & 14 deletions Sources/Smithy/Schema/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// SPDX-License-Identifier: Apache-2.0
//

/// A class which describes selected Smithy model information for a Smithy model shape.
/// A class which describes selected, modeled information for a Smithy shape.
///
/// Typically, the Schema contains only modeled info & properties that are relevant to
/// serialization, transport bindings, and other functions performed by the SDK.
Expand All @@ -29,7 +29,13 @@ public final class Schema: Sendable {
public let members: [Schema]

/// The target schema for this schema. Will only be used when this is a member schema.
public let target: Schema?
public var target: Schema? {
_target()
}

/// Target schema is passed as an autoclosure so that schemas with self-referencing targets will not cause
/// an infinite loop when accessed.
private let _target: @Sendable () -> Schema?

/// The index of this schema, if it represents a Smithy member.
///
Expand All @@ -49,24 +55,14 @@ public final class Schema: Sendable {
type: ShapeType,
traits: [ShapeID: Node] = [:],
members: [Schema] = [],
target: Schema? = nil,
target: @Sendable @escaping @autoclosure () -> Schema? = nil,
index: Int = -1
) {
self.id = id
self.type = type
self.traits = traits
self.members = members
self.target = target
self._target = target
self.index = index
}
}

public extension Schema {

/// The member name for this schema, if any.
///
/// Member name is computed from the schema's ID.
var memberName: String? {
id.member
}
}
File renamed without changes.
27 changes: 22 additions & 5 deletions Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import struct SmithyCodegenCore.CodeGenerator
@main
struct SmithyCodegenCLI: AsyncParsableCommand {

@Argument(help: "The full or relative path to the JSON model file.")
@Argument(help: "The full or relative path to read the JSON AST model input file.")
var modelPath: String

@Option(help: "The full or relative path to write the schemas output file.")
@Option(help: "The full or relative path to write the Schemas output file.")
var schemasPath: String?

@Option(help: "The full or relative path to write the struct consumers output file.")
var structConsumersPath: String?
@Option(help: "The full or relative path to write the Serialize output file.")
var serializePath: String?

@Option(help: "The full or relative path to write the Deserialize output file.")
var deserializePath: String?

func run() async throws {
let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL()
Expand All @@ -35,10 +38,24 @@ struct SmithyCodegenCLI: AsyncParsableCommand {
// If --schemas-path was supplied, create the schema file URL
let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath)

// If --serialize-path was supplied, create the Serialize file URL
let serializeFileURL = resolve(
paramName: "--serialize-path",
path: serializePath
)

// If --deserialize-path was supplied, create the Deserialize file URL
let deserializeFileURL = resolve(
paramName: "--deserialize-path",
path: deserializePath
)

// Use resolved file URLs to run code generator
try CodeGenerator(
modelFileURL: modelFileURL,
schemasFileURL: schemasFileURL
schemasFileURL: schemasFileURL,
serializeFileURL: serializeFileURL,
deserializeFileURL: deserializeFileURL
).run()
}

Expand Down
34 changes: 23 additions & 11 deletions Sources/SmithyCodegenCore/CodeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@
//

import struct Foundation.Data
import class Foundation.FileManager
import class Foundation.JSONDecoder
import struct Foundation.URL

public struct CodeGenerator {
let modelFileURL: URL
let schemasFileURL: URL?
let serializeFileURL: URL?
let deserializeFileURL: URL?

public init(
modelFileURL: URL,
schemasFileURL: URL?
schemasFileURL: URL?,
serializeFileURL: URL?,
deserializeFileURL: URL?
) {
self.modelFileURL = modelFileURL
self.schemasFileURL = schemasFileURL
self.serializeFileURL = serializeFileURL
self.deserializeFileURL = deserializeFileURL
}

public func run() throws {
Expand All @@ -31,17 +36,24 @@ public struct CodeGenerator {
let model = try Model(astModel: astModel)

// Create a generation context from the model
_ = try GenerationContext(model: model)
let ctx = try GenerationContext(model: model)

// Generation context will be used here in the future
// to generate needed files.

// This code simply writes an empty schemas file, since it is expected to exist after the
// code generator plugin runs.
//
// Actual code generation will be implemented here later.
// If a schemas file URL was provided, generate it
if let schemasFileURL {
FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data())
let schemasContents = try SmithySchemaCodegen().generate(ctx: ctx)
try Data(schemasContents.utf8).write(to: schemasFileURL)
}

// If a Serialize file URL was provided, generate it
if let serializeFileURL {
let serializeContents = try SerializeCodegen().generate(ctx: ctx)
try Data(serializeContents.utf8).write(to: serializeFileURL)
}

// If a Deserialize file URL was provided, generate it
if let deserializeFileURL {
let deserializeContents = try DeserializeCodegen().generate(ctx: ctx)
try Data(deserializeContents.utf8).write(to: deserializeFileURL)
}
}
}
156 changes: 156 additions & 0 deletions Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

package struct DeserializeCodegen {

package init() {}

package func generate(ctx: GenerationContext) throws -> String {
let writer = SwiftWriter()
writer.write("import Foundation")
writer.write("import enum Smithy.Prelude")
writer.write("import class Smithy.Schema")
writer.write("import protocol SmithySerialization.ShapeDeserializer")
writer.write("import protocol SmithySerialization.DeserializableStruct")
writer.write("import typealias SmithySerialization.StructMemberConsumer")
writer.write("")

let structsAndUnions = ctx.model.allShapesSorted.filter { $0.type == .structure || $0.type == .union }
for shape in structsAndUnions {
let swiftType = try ctx.symbolProvider.swiftType(shape: shape)
try writer.openBlock("extension \(swiftType): SmithySerialization.DeserializableStruct {", "}") { writer in
writer.write("")
let consumerType = "SmithySerialization.StructMemberConsumer<\(swiftType)>"
try writer.openBlock(
"public static var consumer: \(consumerType) {", "}") { writer in
try writer.openBlock("{ memberSchema, structure, deserializer in", "}") { writer in
try writer.openBlock("switch memberSchema.index {", "}") { writer in
writer.dedent()
for (index, member) in members(of: shape).enumerated() {
writer.write("case \(index):")
writer.indent()
try writeDeserializeCall(
ctx: ctx, writer: writer, shape: shape, member: member, index: index
)
writer.dedent()
}
writer.write("default: break")
writer.indent()
}
}
}
writer.write("")
let deserializerType = "any SmithySerialization.ShapeDeserializer"
writer.openBlock(
"public mutating func deserialize(_ deserializer: \(deserializerType)) throws {", "}"
) { writer in
writer.write("var value = self")
writer.write("try deserializer.readStruct(Self.schema, &value, Self.consumer)")
writer.write("self = value")
}
}
writer.write("")
}
writer.unwrite("\n")
return writer.finalize()
}

private func writeDeserializeCall(
ctx: GenerationContext, writer: SwiftWriter, shape: Shape, member: MemberShape, index: Int
) throws {
switch member.target.type {
case .structure:
try writeStructureDeserializeCall(
ctx: ctx, writer: writer, shape: shape, member: member, index: index, initializer: "()"
)
case .union:
try writeStructureDeserializeCall(
ctx: ctx, writer: writer, shape: shape, member: member, index: index, initializer: ".sdkUnknown(\"\")"
)
case .list, .set:
let listShape = member.target as! ListShape // swiftlint:disable:this force_cast
let elementSwiftType = try ctx.symbolProvider.swiftType(shape: listShape.member.target)
writer.write("var value = [\(elementSwiftType)]()")
let schemaVarName = try shape.schemaVarName
try writer.openBlock(
"try deserializer.readList(\(schemaVarName).members[\(index)], &value) { deserializer in",
"}"
) { writer in
try writeDeserializeCall(ctx: ctx, writer: writer, shape: listShape, member: listShape.member, index: 0)
}
try writeAssignment(ctx: ctx, writer: writer, shape: shape, member: member)
case .map:
let mapShape = member.target as! MapShape // swiftlint:disable:this force_cast
let keySwiftType = try ctx.symbolProvider.swiftType(shape: mapShape.key.target)
let valueSwiftType = try ctx.symbolProvider.swiftType(shape: mapShape.value.target)
writer.write("var value = [\(keySwiftType): \(valueSwiftType)]()")
let schemaVarName = try shape.schemaVarName
try writer.openBlock(
"try deserializer.readMap(\(schemaVarName).members[\(index)], &value) { key, deserializer in",
"}"
) { writer in
try writeDeserializeCall(ctx: ctx, writer: writer, shape: mapShape, member: mapShape.value, index: 1)
}
try writeAssignment(ctx: ctx, writer: writer, shape: shape, member: member)
default:
let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id)
let methodName = try member.target.deserializeMethodName
let schemaVarName = try shape.schemaVarName
let properties = shape.hasTrait(.init("smithy.api", "error")) ? "properties." : ""
let lhs = switch shape.type {
case .list, .set, .map:
""
default:
"structure.\(properties)\(propertyName) = "
}
writer.write("\(lhs)try deserializer.\(methodName)(\(schemaVarName).members[\(index)])")
}
}

private func writeStructureDeserializeCall(
ctx: GenerationContext, writer: SwiftWriter, shape: Shape, member: MemberShape, index: Int, initializer: String
) throws {
let target = member.target
let propertySwiftType = try ctx.symbolProvider.swiftType(shape: target)
let schemaVarName = try shape.schemaVarName
let consumer = "\(propertySwiftType).consumer"
writer.write("var value = \(propertySwiftType)\(initializer)")
writer.write("try deserializer.readStruct(\(schemaVarName).members[\(index)], &value, \(consumer))")
try writeAssignment(ctx: ctx, writer: writer, shape: shape, member: member)
}

private func writeAssignment(
ctx: GenerationContext, writer: SwiftWriter, shape: Shape, member: MemberShape
) throws {
// Only the "composite types" need to have an assignment written.
guard [.structure, .union, .list, .set, .map].contains(member.target.type) else { return }

// The assignment being written is based on the shape enclosing the member.
switch shape.type {
case .structure:
// For a structure member, write the value to the appropriate structure property,
// making the appropriate adjustment for an error.
let properties = shape.hasTrait(.init("smithy.api", "error")) ? "properties." : ""
let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id)
writer.write("structure.\(properties)\(propertyName) = value")
case .union:
// For a union member, write the value to the appropriate union case
let enumCaseName = try ctx.symbolProvider.enumCaseName(shapeID: member.id)
writer.write("structure = .\(enumCaseName)(value)")
case .list, .set, .map:
// For a collection member, return it to the caller since this is being written
// into a consumer block that returns the collection element.
writer.write("return value")
default: break
}
}

private func members(of shape: Shape) -> [MemberShape] {
guard let hasMembers = shape as? HasMembers else { return [] }
return hasMembers.members
}
}
Loading
Loading