Skip to content
Draft
23 changes: 23 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 All @@ -268,6 +287,10 @@ let package = Package(
],
resources: [ .process("Resources") ]
),
.testTarget(
name: "SmithyTests",
dependencies: ["Smithy"]
),
.testTarget(
name: "SmithyCBORTests",
dependencies: ["SmithyCBOR", "ClientRuntime", "SmithyTestUtil"]
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" }
}
109 changes: 109 additions & 0 deletions Sources/Smithy/Schema/Node.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// Contains the value of a Smithy Node.
///
/// Smithy node data is basically the same as the data that can be stored in JSON.
/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list.
///
/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values
public enum Node: Sendable {
case object([String: Node])
case list([Node])
case string(String)
case number(Double)
case boolean(Bool)
case null
}

public extension Node {

/// Returns the object dictionary if this Node is `.object`, else returns `nil`.
var object: [String: Node]? {
guard case .object(let value) = self else { return nil }
return value
}

/// Returns the array of `Node` if this node is `.list`, else returns `nil`.
var list: [Node]? {
guard case .list(let value) = self else { return nil }
return value
}

/// Returns the string if this node is `.string`, else returns `nil`.
var string: String? {
guard case .string(let value) = self else { return nil }
return value
}

/// Returns the Double if this node is `.number`, else returns `nil`.
var number: Double? {
guard case .number(let value) = self else { return nil }
return value
}

/// Returns the `Bool` value if this node is `.boolean`, else returns `nil`.
var boolean: Bool? {
guard case .boolean(let value) = self else { return nil }
return value
}

/// Returns `true` if this node is `.null`, else returns `false`.
var null: Bool {
guard case .null = self else { return false }
return true
}
}

extension Node: ExpressibleByDictionaryLiteral {

public init(dictionaryLiteral elements: (String, Node)...) {
self = .object(Dictionary(uniqueKeysWithValues: elements))
}
}

extension Node: ExpressibleByArrayLiteral {

public init(arrayLiteral elements: Node...) {
self = .list(elements)
}
}

extension Node: ExpressibleByStringLiteral {

public init(stringLiteral value: String) {
self = .string(value)
}
}

extension Node: ExpressibleByIntegerLiteral {

public init(integerLiteral value: IntegerLiteralType) {
self = .number(Double(value))
}
}

extension Node: ExpressibleByFloatLiteral {

public init(floatLiteral value: FloatLiteralType) {
self = .number(Double(value))
}
}

extension Node: ExpressibleByBooleanLiteral {

public init(booleanLiteral value: BooleanLiteralType) {
self = .boolean(value)
}
}

extension Node: ExpressibleByNilLiteral {

public init(nilLiteral: ()) {
self = .null
}
}
90 changes: 90 additions & 0 deletions Sources/Smithy/Schema/Prelude.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// Below are schemas for all model shapes defined in the Smithy 2.0 prelude.
// Schemas for custom Smithy types may use these schemas in their definitions.

public enum Prelude {

public static var unitSchema: Schema {
Schema(id: .init("smithy.api", "Unit"), type: .structure)
}

public static var booleanSchema: Schema {
Schema(id: .init("smithy.api", "Boolean"), type: .boolean)
}

public static var stringSchema: Schema {
Schema(id: .init("smithy.api", "String"), type: .string)
}

public static var integerSchema: Schema {
Schema(id: .init("smithy.api", "Integer"), type: .integer)
}

public static var blobSchema: Schema {
Schema(id: .init("smithy.api", "Blob"), type: .blob)
}

public static var timestampSchema: Schema {
Schema(id: .init("smithy.api", "Timestamp"), type: .timestamp)
}

public static var byteSchema: Schema {
Schema(id: .init("smithy.api", "Byte"), type: .byte)
}

public static var shortSchema: Schema {
Schema(id: .init("smithy.api", "Short"), type: .short)
}

public static var longSchema: Schema {
Schema(id: .init("smithy.api", "Long"), type: .long)
}

public static var floatSchema: Schema {
Schema(id: .init("smithy.api", "Float"), type: .float)
}

public static var doubleSchema: Schema {
Schema(id: .init("smithy.api", "Double"), type: .double)
}

public static var documentSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveDocument"), type: .document)
}

public static var primitiveBooleanSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveBoolean"), type: .boolean, traits: [defaultTraitID: false])
}

public static var primitiveIntegerSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveInteger"), type: .integer, traits: [defaultTraitID: 0])
}

public static var primitiveByteSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveByte"), type: .byte, traits: [defaultTraitID: 0])
}

public static var primitiveShortSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveShort"), type: .short, traits: [defaultTraitID: 0])
}

public static var primitiveLongSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveLong"), type: .long, traits: [defaultTraitID: 0])
}

public static var primitiveFloatSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveFloat"), type: .float, traits: [defaultTraitID: 0])
}

public static var primitiveDoubleSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [defaultTraitID: 0])
}
}

private let defaultTraitID = ShapeID("smithy.api", "default")
72 changes: 72 additions & 0 deletions Sources/Smithy/Schema/Schema.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// A class which describes selected Smithy model information for a Smithy model shape.
///
/// Typically, the Schema contains only modeled info & properties that are relevant to
/// serialization, transport bindings, and other functions performed by the SDK.
public final class Schema: Sendable {

/// The Smithy shape ID for the shape described by this schema.
public let id: ShapeID

/// The type of the shape being described.
public let type: ShapeType

/// A dictionary of the described shape's trait shape IDs to Nodes with trait data.
///
/// Not all traits for a shape will be represented in the schema;
/// typically the Schema contains only the traits relevant to the client-side SDK.
public let traits: [ShapeID: Node]

/// The member schemas for this schema, if any.
///
/// Typically only a schema of type Structure, Union, Enum, IntEnum, List or Map will have members.
public let members: [Schema]

/// The target schema for this schema. Will only be used when this is a member schema.
public let target: Schema?

/// The index of this schema, if it represents a Smithy member.
///
/// For a member schema, index will be set to its index in the members array.
/// For other types of schema, index will be `-1`.
///
/// This index is intended for use as a performance enhancement when looking up member schemas
/// during deserialization.
public let index: Int

/// Creates a new Schema using the passed parameters.
///
/// No validation is performed on the parameters since calls to this initializer
/// are almost always code-generated from a previously validated Smithy model.
public init(
id: ShapeID,
type: ShapeType,
traits: [ShapeID: Node] = [:],
members: [Schema] = [],
target: Schema? = nil,
index: Int = -1
) {
self.id = id
self.type = type
self.traits = traits
self.members = members
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
}
}
Loading
Loading