-
Notifications
You must be signed in to change notification settings - Fork 35
feat: Add Swift-native codegen plugin #993
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
base: epic/sbs
Are you sure you want to change the base?
Changes from 13 commits
495f2e1
ca5b331
212120e
fcdf1bf
b2152c2
88f7be9
aa823be
e817d55
6a9c537
33cc093
6b54575
ae474f1
70f7669
3d623e6
d535e66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"]), | ||
| ], | ||
| 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"), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| .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"), | ||
| ] | ||
|
|
@@ -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" | ||
| ), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3 targets are added above:
|
||
| .testTarget( | ||
| name: "ClientRuntimeTests", | ||
| dependencies: [ | ||
|
|
@@ -268,6 +287,10 @@ let package = Package( | |
| ], | ||
| resources: [ .process("Resources") ] | ||
| ), | ||
| .testTarget( | ||
| name: "SmithyTests", | ||
| dependencies: ["Smithy"] | ||
| ), | ||
| .testTarget( | ||
| name: "SmithyCBORTests", | ||
| dependencies: ["SmithyCBOR", "ClientRuntime", "SmithyTestUtil"] | ||
|
|
||
| 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 Foundation | ||
| import PackagePlugin | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
| @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 `SmithySchemaCodegenTool` from the plugin's tools. | ||
| let generatorTool = 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: generatorTool.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 inputName = inputPath.lastComponent | ||
| let schemasSwiftPath = outputDirectoryPath.appending("Schemas.swift") | ||
| return .buildCommand( | ||
| displayName: "Generating Schemas.swift from \(inputName)", | ||
| executable: generatorToolPath, | ||
| arguments: [modelPath, schemasSwiftPath], | ||
| inputFiles: [inputPath, modelPath], | ||
| outputFiles: [schemasSwiftPath] | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| struct SmithySchemaGeneratorPluginError: Error { | ||
| let localizedDescription: String | ||
|
|
||
| init(_ localizedDescription: String) { | ||
| self.localizedDescription = localizedDescription | ||
| } | ||
| } | ||
|
|
||
| struct SmithyModelInfo: Codable { | ||
| let path: String | ||
| } | ||
| 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 | ||
| } | ||
| } |
| 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") |
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
SmithyCodeGeneratorplugin is exported so that service clients inaws-sdk-swiftcan add it to their target definitions.