diff --git a/Package.swift b/Package.swift index 70d3a7cc8..828214760 100644 --- a/Package.swift +++ b/Package.swift @@ -303,9 +303,13 @@ let package = Package( .executableTarget( name: "SmithyCodegenCLI", dependencies: [ + "SmithyCodegenCore", .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), + .target( + name: "SmithyCodegenCore" + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index b8b247155..bf0b608c8 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -7,6 +7,7 @@ import ArgumentParser import Foundation +import struct SmithyCodegenCore.CodeGenerator @main struct SmithyCodegenCLI: AsyncParsableCommand { @@ -17,6 +18,9 @@ struct SmithyCodegenCLI: AsyncParsableCommand { @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? + func run() async throws { let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() print("Current working directory: \(currentWorkingDirectoryFileURL.path)") @@ -31,12 +35,11 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) - // All file URLs needed for code generation have now been resolved. - // Implement code generation here. - if let schemasFileURL { - print("Schemas file path: \(schemasFileURL)") - FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) - } + // Use resolved file URLs to run code generator + try CodeGenerator( + modelFileURL: modelFileURL, + schemasFileURL: schemasFileURL + ).run() } private func currentWorkingDirectoryFileURL() -> URL { diff --git a/Sources/SmithyCodegenCore/AST/ASTError.swift b/Sources/SmithyCodegenCore/AST/ASTError.swift new file mode 100644 index 000000000..3e32bc097 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ASTError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/AST/ASTMember.swift b/Sources/SmithyCodegenCore/AST/ASTMember.swift new file mode 100644 index 000000000..5d3a30c35 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#ast-member +struct ASTMember: Decodable { + let target: String + let traits: [String: ASTNode]? +} diff --git a/Sources/SmithyCodegenCore/AST/ASTModel.swift b/Sources/SmithyCodegenCore/AST/ASTModel.swift new file mode 100644 index 000000000..07de8bf89 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -0,0 +1,13 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#top-level-properties +struct ASTModel: Decodable { + let smithy: String + let metadata: ASTNode? + let shapes: [String: ASTShape] +} diff --git a/Sources/SmithyCodegenCore/AST/ASTNode.swift b/Sources/SmithyCodegenCore/AST/ASTNode.swift new file mode 100644 index 000000000..a3d6fd58b --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTNode.swift @@ -0,0 +1,45 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Contains the value of a Smithy Node, as used in a JSON AST. +/// +/// 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 +enum ASTNode { + case object([String: ASTNode]) + case list([ASTNode]) + case string(String) + case number(Double) + case boolean(Bool) + case null +} + +extension ASTNode: Decodable { + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let bool = try? container.decode(Bool.self) { + self = .boolean(bool) + } else if let int = try? container.decode(Int.self) { + self = .number(Double(int)) + } else if let double = try? container.decode(Double.self) { + self = .number(double) + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let array = try? container.decode([ASTNode].self) { + self = .list(array) + } else if let dictionary = try? container.decode([String: ASTNode].self) { + self = .object(dictionary) + } else { + throw ASTError("Undecodable value in AST node") + } + } +} diff --git a/Sources/SmithyCodegenCore/AST/ASTReference.swift b/Sources/SmithyCodegenCore/AST/ASTReference.swift new file mode 100644 index 000000000..4cdce691a --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTReference.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#ast-shape-reference +struct ASTReference: Decodable { + let target: String +} diff --git a/Sources/SmithyCodegenCore/AST/ASTShape.swift b/Sources/SmithyCodegenCore/AST/ASTShape.swift new file mode 100644 index 000000000..abeb8d425 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -0,0 +1,33 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#json-ast +// This Swift type captures fields for all AST shape types +struct ASTShape: Decodable { + let type: ASTType + let traits: [String: ASTNode]? + let member: ASTMember? + let key: ASTMember? + let value: ASTMember? + let members: [String: ASTMember]? + let version: String? + let operations: [ASTReference]? + let resources: [ASTReference]? + let errors: [ASTReference]? + let rename: [String: String]? + let identifiers: [String: ASTReference]? + let properties: [String: ASTReference]? + let create: ASTReference? + let put: ASTReference? + let read: ASTReference? + let update: ASTReference? + let delete: ASTReference? + let list: ASTReference? + let collectionOperations: [ASTReference]? + let input: ASTReference? + let output: ASTReference? +} diff --git a/Sources/SmithyCodegenCore/AST/ASTType.swift b/Sources/SmithyCodegenCore/AST/ASTType.swift new file mode 100644 index 000000000..adf1e6b52 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTType.swift @@ -0,0 +1,38 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/model.html#shape-types +enum ASTType: String, Decodable { + // These cases are all the standard Smithy shape types + case blob + case boolean + case string + case timestamp + case byte + case short + case integer + case long + case float + case document + case double + case bigDecimal + case bigInteger + case `enum` + case intEnum + case list + case set + case map + case structure + case union + case member + case service + case resource + case operation + + // Special for AST, added 'apply' case + case apply +} diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift new file mode 100644 index 000000000..24f7b056c --- /dev/null +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -0,0 +1,41 @@ +// +// 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 + +public struct CodeGenerator { + let modelFileURL: URL + let schemasFileURL: URL? + + public init( + modelFileURL: URL, + schemasFileURL: URL? + ) { + self.modelFileURL = modelFileURL + self.schemasFileURL = schemasFileURL + } + + public func run() throws { + // Load the AST from the model file + let modelData = try Data(contentsOf: modelFileURL) + let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) + + // In the future, AST will be used to create a Model. + // Model will be used to generate code. + + // 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 let schemasFileURL { + FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) + } + } +}