Skip to content
Draft
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
19 changes: 19 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ let package = Package(
.library(name: "SmithyCBOR", targets: ["SmithyCBOR"]),
.library(name: "SmithyWaitersAPI", targets: ["SmithyWaitersAPI"]),
.library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]),
.plugin(name: "SmithyCodeGenerator", targets: ["SmithyCodeGenerator"]),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SmithyCodeGenerator plugin is exported so that service clients in aws-sdk-swift can add it to their target definitions.

],
dependencies: {
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.54.2"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swift-argument-parser is added as a dependency because the SmithyCodegenCLI target is essentially a command-line tool, and argument-parser makes the API for that tool easier to manage & use.

aws-crt-swift already imports this dependency so the effect on customers' dependencies at compile time is zero add.

.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"),
]
Expand Down Expand Up @@ -258,6 +260,23 @@ let package = Package(
.target(
name: "SmithyWaitersAPI"
),
.plugin(
name: "SmithyCodeGenerator",
capability: .buildTool(),
dependencies: [
"SmithyCodegenCLI",
]
),
.executableTarget(
name: "SmithyCodegenCLI",
dependencies: [
"SmithyCodegenCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.target(
name: "SmithyCodegenCore"
),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 targets are added above:

  • SmithyCodeGenerator which is the build plugin that directs code generation at build time.
  • SmithyCodegenCLI which is the executable that performs code generation.
  • SmithyCodegenCore which contains components for reading the Smithy model. In the future it will provide features comparable to existing Smithy codegen.

.testTarget(
name: "ClientRuntimeTests",
dependencies: [
Expand Down
66 changes: 66 additions & 0 deletions Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

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

Copy link
Contributor Author

@jbelkins jbelkins Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type implements the Swift build tool plugin.

Essentially, all the build plugin does is:

  • Receives a list of all files in this target, and a "working directory" it can write its output to.
  • Iterates over the files to decide what executable tool(s) to run on each of them.
    • All source files except for smithy-model-info.json are ignored & no action is taken on them.
    • smithy-model-info.json is read to get the model file location, then the code generator SmithyCodegenCLI is run with the model as the input.
  • Code-generated Swift files are written to the working directory, and after the build plugin finishes all its work, the Swift compiler compiles generated Swift files along with the rest of the target.

@main
struct SmithyCodeGeneratorPlugin: BuildToolPlugin {

func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
// This plugin only runs for package targets that can have source files.
guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] }

// Retrieve the `SmithyCodegenCLI` tool from the plugin's tools.
let smithyCodegenCLITool = try context.tool(named: "SmithyCodegenCLI")

// Construct a build command for each source file with a particular suffix.
return try sourceFiles.map(\.path).compactMap {
try createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path)
}
}

private func createBuildCommand(
for inputPath: Path,
in outputDirectoryPath: Path,
with generatorToolPath: Path
) throws -> Command? {
let currentWorkingDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)

// Skip any file that isn't the smithy-model-info.json for this service.
guard inputPath.lastComponent == "smithy-model-info.json" else { return nil }

// Get the smithy model path.
let modelInfoData = try Data(contentsOf: URL(fileURLWithPath: inputPath.string))
let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData)
let modelPathURL = currentWorkingDirectoryURL.appendingPathComponent(smithyModelInfo.path)
let modelPath = Path(modelPathURL.path)

// Return a command that will run during the build to generate the output file.
let modelCountSwiftPath = outputDirectoryPath.appending("ModelCount.swift")
return .buildCommand(
displayName: "Generating Swift source files from \(smithyModelInfo.path)",
executable: generatorToolPath,
arguments: [modelPath, modelCountSwiftPath],
inputFiles: [inputPath, modelPath],
outputFiles: [modelCountSwiftPath]
)
}
}

/// Codable structure for reading the contents of `smithy-model-info.json`
private struct SmithyModelInfo: Decodable {
/// The path to the model, from the root of the target's project. Required.
let path: String
}

struct Err: Error {
var localizedDescription: String { "boom" }
}
51 changes: 51 additions & 0 deletions Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import ArgumentParser
import Foundation
import SmithyCodegenCore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very simple code generator that:

  • Reads the Smithy JSON model file into memory.
  • Generates a Swift source file with a public variable modelCount: Int that contains the number of bytes in the model file.
  • Adds a simple doc comment to the variable.

While this example codegen is trivial and not useful in itself, it does prove that:

  • A Swift build plugin code generator can read from our model files elsewhere in the project.
  • It can generate a Swift file that is added to the target.
  • Doc comments in generated code get picked up with the rest of the target's doc comments.

Follow-on development would code-generate more substantial content from the model.

@main
struct SmithyCodegenCLI: AsyncParsableCommand {

@Argument(help: "The full path to the JSON model file.")
var modelPath: String

@Argument(help: "The full path to write the output file. Intermediate directories will be created as needed.")
var outputPath: String

func run() async throws {
guard FileManager.default.fileExists(atPath: modelPath) else {
throw SmithySchemaCodegenToolError(localizedDescription: "no file at model path")
}
let model = try SmithyModel(modelFileURL: URL(fileURLWithPath: modelPath))

let outputFileURL = URL(fileURLWithPath: outputPath)
let contents = """
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// Code generated by SmithyCodegenCLI. DO NOT EDIT!

/// The count of bytes in the model.
public let modelCount = \(model.count)
"""
try FileManager.default.createDirectory(
at: outputFileURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try Data(contents.utf8).write(to: outputFileURL)
}
}

struct SmithySchemaCodegenToolError: Error {
let localizedDescription: String
}
18 changes: 18 additions & 0 deletions Sources/SmithyCodegenCore/SmithyModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import struct Foundation.Data
import struct Foundation.URL

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this type does right now is record the number of bytes in the model file. In the future it would read the model into an in-memory tree for use during code generation.

public struct SmithyModel {
public let count: Int

public init(modelFileURL: URL) throws {
let modelData = try Data(contentsOf: modelFileURL)
self.count = modelData.count
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ class DirectedSwiftCodegen(
DependencyJSONGenerator(ctx).writePackageJSON(writers.dependencies)
}

LOGGER.info("Generating Smithy model file info")
SmithyModelFileInfoGenerator(ctx).writeSmithyModelFileInfo()

Copy link
Contributor Author

@jbelkins jbelkins Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initiates generation of the smithy-model-info.json file.

LOGGER.info("Flushing swift writers")
writers.flushWriters()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package software.amazon.smithy.swift.codegen

import software.amazon.smithy.aws.traits.ServiceTrait
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
import software.amazon.smithy.swift.codegen.model.getTrait

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just generates a very simple JSON file named smithy-model-info.json into the directory where the Swift source files go. Its purpose is to tell the Swift code generator where it can find the JSON model file for this service client, relative to project root. Example contents:

{"path":"codegen/sdk-codegen/aws-models/s3.json"}

class SmithyModelFileInfoGenerator(
val ctx: ProtocolGenerator.GenerationContext,
) {
fun writeSmithyModelFileInfo() {
ctx.service.getTrait<ServiceTrait>()?.let { serviceTrait ->
val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json"
val modelFileName =
serviceTrait
.sdkId
.lowercase()
.replace(",", "")
.replace(" ", "-")
val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json"
ctx.delegator.useFileWriter(filename) { writer ->
writer.write("{\"path\":\"$contents\"}")
}
}
}
}
Loading