From a4f284a61c604462e1eb7fd507aa4eb6d8b0523a Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 25 Feb 2026 22:41:45 -0600 Subject: [PATCH 1/4] feat: Add operation and type registry codegen --- .../SmithyCodeGeneratorPlugin.swift | 64 +++++++-- Sources/Smithy/Schema/Prelude.swift | 26 +++- .../TraitLibrary/AllSupportedTraits.swift | 3 + .../Smithy/TraitLibrary/RequiredTrait.swift | 16 +++ .../TraitLibrary/TimestampFormatTrait.swift | 31 +++++ .../Smithy/TraitLibrary/UnitTypeTrait.swift | 16 +++ .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 36 ++++- Sources/SmithyCodegenCore/CodeGenerator.swift | 31 +++-- .../SmithyCodegenCore/GenerationContext.swift | 15 ++- .../SmithyCodegenCore/Model/Model+AST.swift | 4 +- .../ModelTransformer/Model+Deprecated.swift | 95 +------------ .../ModelTransformer/Model+Operations.swift | 29 ++++ .../Model+TrimReferences.swift | 127 ++++++++++++++++++ .../Operations/OperationsCodegen.swift | 42 ++++++ .../Shape/ResourceShape.swift | 9 +- Sources/SmithyCodegenCore/SwiftSettings.swift | 42 ++++++ .../SymbolProvider/SymbolProvider.swift | 18 +-- .../TypeRegistry/TypeRegistryCodegen.swift | 41 ++++++ .../ClientProtocol/ClientProtocol.swift | 37 +++++ .../ClientProtocol/Operation.swift | 43 ++++++ .../ClientProtocol/OperationProperties.swift | 16 +++ Sources/SmithySerialization/Codec.swift | 13 ++ .../Serialization/ShapeSerializer.swift | 35 ++--- .../TypeRegistry/TypeRegistry.swift | 41 ++++++ .../swift/codegen/DirectedSwiftCodegen.kt | 4 +- .../codegen/SmithyModelFileInfoGenerator.kt | 20 --- .../smithy/swift/codegen/SwiftSettings.kt | 36 ++--- .../codegen/SwiftSettingsJSONGenerator.kt | 27 ++++ .../endpoints/EndpointParamsGenerator.kt | 3 +- .../HTTPBindingProtocolGenerator.kt | 127 +++--------------- .../codegen/integration/ProtocolGenerator.kt | 2 - .../HTTPResponseBindingOutputGenerator.kt | 2 + .../OperationInputBodyMiddleware.kt | 14 +- .../codegen/integration/serde/SerdeUtils.kt | 13 ++ .../serde/schema/SchemaGenerator.kt | 77 ----------- .../serde/schema/SchemaShapeUtils.kt | 48 ------- .../integration/serde/schema/SchemaTraits.kt | 12 -- .../serde/schema/SwiftNodeUtils.kt | 44 ------ .../rpcv2cbor/RpcV2CborProtocolGenerator.kt | 7 +- .../swift/codegen/utils/SchemaFileUtils.kt | 17 --- 40 files changed, 763 insertions(+), 520 deletions(-) create mode 100644 Sources/Smithy/TraitLibrary/RequiredTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/TimestampFormatTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/UnitTypeTrait.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/Model+Operations.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift create mode 100644 Sources/SmithyCodegenCore/Operations/OperationsCodegen.swift create mode 100644 Sources/SmithyCodegenCore/SwiftSettings.swift create mode 100644 Sources/SmithyCodegenCore/TypeRegistry/TypeRegistryCodegen.swift create mode 100644 Sources/SmithySerialization/ClientProtocol/ClientProtocol.swift create mode 100644 Sources/SmithySerialization/ClientProtocol/Operation.swift create mode 100644 Sources/SmithySerialization/ClientProtocol/OperationProperties.swift create mode 100644 Sources/SmithySerialization/Codec.swift create mode 100644 Sources/SmithySerialization/TypeRegistry/TypeRegistry.swift delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettingsJSONGenerator.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SwiftNodeUtils.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt diff --git a/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift index 62ad22234..125e998dc 100644 --- a/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift @@ -38,8 +38,8 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { in outputDirectoryPath: Path, with generatorToolPath: Path ) throws -> Command? { - // Skip any file that isn't the smithy-model-info.json for this service. - guard inputPath.lastComponent == "smithy-model-info.json" else { return nil } + // Skip any file that isn't the swift-settings.json for this service. + guard inputPath.lastComponent == "swift-settings.json" else { return nil } let currentWorkingDirectoryFileURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) @@ -47,10 +47,13 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { let modelInfoData = try Data(contentsOf: URL(fileURLWithPath: inputPath.string)) let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData) - // Get the service ID & model path & settings sdkId from smithy-model-info. + // Get the fields from smithy-model-info. let service = smithyModelInfo.service - let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path) + let sdkId = smithyModelInfo.sdkId + let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.modelPath) let modelPath = Path(modelPathURL.path) + let internalClient = smithyModelInfo.internalClient + let operations = smithyModelInfo.operations.joined(separator: ",") // Construct the Schemas.swift path. let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") @@ -61,32 +64,65 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Construct the Deserialize.swift path. let deserializeSwiftPath = outputDirectoryPath.appending("\(name)Deserialize.swift") + // Construct the Deserialize.swift path. + let typeRegistrySwiftPath = outputDirectoryPath.appending("\(name)TypeRegistry.swift") + + // Construct the Operations.swift path. + let operationsSwiftPath = outputDirectoryPath.appending("\(name)Operations.swift") + + var arguments: [any CustomStringConvertible] = [ + service, + modelPath, + "--internal", "\(internalClient)", + "--sdk-id", sdkId, + "--schemas-path", schemasSwiftPath, + "--serialize-path", serializeSwiftPath, + "--deserialize-path", deserializeSwiftPath, + "--type-registry-path", typeRegistrySwiftPath, + "--operations-path", operationsSwiftPath, + "--schemas-path", schemasSwiftPath, + "--serialize-path", serializeSwiftPath, + "--deserialize-path", deserializeSwiftPath + ] + + if !operations.isEmpty { + arguments.append(contentsOf: ["--operations", operations]) + } + // Construct the build command that invokes SmithyCodegenCLI. return .buildCommand( - displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", + displayName: "Generating Swift source files from model file \(smithyModelInfo.modelPath)", executable: generatorToolPath, - arguments: [ - service, - modelPath, - "--schemas-path", schemasSwiftPath, - "--serialize-path", serializeSwiftPath, - "--deserialize-path", deserializeSwiftPath - ], + arguments: arguments, inputFiles: [inputPath, modelPath], outputFiles: [ schemasSwiftPath, serializeSwiftPath, deserializeSwiftPath, + typeRegistrySwiftPath, + operationsSwiftPath, ] ) } } -/// Codable structure for reading the contents of `smithy-model-info.json` +/// Decodable structure for reading the contents of `smithy-model-info.json` private struct SmithyModelInfo: Decodable { /// The shape ID of the service being generated. Must exist in the model. let service: String + /// The name to be used for the enclosing module. + let module: String + + /// The `sdkId` used by the Smithy-based code generator. + let sdkId: String + + /// Set to `true` if the client should be rendered for internal use. + let internalClient: Bool + + /// A list of operations to be included in the client. If omitted or empty, all operations are included. + let operations: [String] + /// The path to the model, from the root of the target's project. Required. - let path: String + let modelPath: String } diff --git a/Sources/Smithy/Schema/Prelude.swift b/Sources/Smithy/Schema/Prelude.swift index 0383c5f1c..ba2a3894a 100644 --- a/Sources/Smithy/Schema/Prelude.swift +++ b/Sources/Smithy/Schema/Prelude.swift @@ -11,7 +11,7 @@ public enum Prelude { public static var unitSchema: Schema { - Schema(id: .init("smithy.api", "Unit"), type: .structure) + Schema(id: .init("smithy.api", "Unit"), type: .structure, traits: [UnitTypeTrait()]) } public static var booleanSchema: Schema { @@ -54,6 +54,14 @@ public enum Prelude { Schema(id: .init("smithy.api", "Double"), type: .double) } + public static var bigIntegerSchema: Schema { + Schema(id: .init("smithy.api", "BigInteger"), type: .bigInteger) + } + + public static var bigDecimalSchema: Schema { + Schema(id: .init("smithy.api", "BigDecimal"), type: .bigDecimal) + } + public static var documentSchema: Schema { Schema(id: .init("smithy.api", "Document"), type: .document) } @@ -85,4 +93,20 @@ public enum Prelude { public static var primitiveDoubleSchema: Schema { Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [DefaultTrait(0.0)]) } + + // The following schemas aren't strictly part of Smithy's prelude, but are used when deserializing a + // list or map contained in a Smithy document. + + public static var listDocumentSchema: Schema { + Schema(id: .init("swift.synthetic", "ListDocument"), type: .list, members: [ + .init(id: .init("swift.synthetic", "ListDocument", "member"), type: .document), + ]) + } + + public static var mapDocumentSchema: Schema { + Schema(id: .init("swift.synthetic", "MapDocument"), type: .list, members: [ + .init(id: .init("swift.synthetic", "MapDocument", "key"), type: .string), + .init(id: .init("swift.synthetic", "MapDocument", "value"), type: .document), + ]) + } } diff --git a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift index d128909c9..37a9a9809 100644 --- a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift +++ b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift @@ -21,8 +21,11 @@ public let allSupportedTraits = Set([ ErrorTrait.id, InputTrait.id, OutputTrait.id, + RequiredTrait.id, SensitiveTrait.id, SparseTrait.id, + TimestampFormatTrait.id, + UnitTypeTrait.id, // UnitTypeTrait will only ever appear in Prelude.unitSchema // Synthetic traits TargetsUnitTrait.id, diff --git a/Sources/Smithy/TraitLibrary/RequiredTrait.swift b/Sources/Smithy/TraitLibrary/RequiredTrait.swift new file mode 100644 index 000000000..374861b65 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/RequiredTrait.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct RequiredTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "required") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/Smithy/TraitLibrary/TimestampFormatTrait.swift b/Sources/Smithy/TraitLibrary/TimestampFormatTrait.swift new file mode 100644 index 000000000..1902c61f2 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/TimestampFormatTrait.swift @@ -0,0 +1,31 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct TimestampFormatTrait: Trait { + + public enum Format: String { + case dateTime = "date-time" + case httpDate = "http-date" + case epochSeconds = "epoch-seconds" + } + + public static var id: ShapeID { .init("smithy.api", "timestampFormat") } + + public let format: Format + + public init(node: Node) throws { + guard let formatString = node.string else { + throw TraitError("TimestampFormatTrait does not have string value") + } + guard let format = Format(rawValue: formatString) else { + throw TraitError("TimestampFormatTrait string value is not valid") + } + self.format = format + } + + public var node: Node { .string(format.rawValue) } +} diff --git a/Sources/Smithy/TraitLibrary/UnitTypeTrait.swift b/Sources/Smithy/TraitLibrary/UnitTypeTrait.swift new file mode 100644 index 000000000..bd033f34e --- /dev/null +++ b/Sources/Smithy/TraitLibrary/UnitTypeTrait.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct UnitTypeTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "Unit") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index 70f12cdd6..c91b16bb8 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -8,6 +8,7 @@ import ArgumentParser import Foundation import struct SmithyCodegenCore.CodeGenerator +import struct SmithyCodegenCore.SwiftSettings @main struct SmithyCodegenCLI: AsyncParsableCommand { @@ -18,6 +19,15 @@ struct SmithyCodegenCLI: AsyncParsableCommand { @Argument(help: "The full or relative path to read the JSON AST model input file.") var modelPath: String + @Option(help: "The sdkId used by the Smithy-based code generator") + var sdkId: String? + + @Option(help: "Set this to true if the client to be generated should be internal-scoped.") + var `internal` = false + + @Option(help: "The comma-separated list of operation IDs to be included in the client.") + var operations: String? + @Option(help: "The full or relative path to write the Schemas output file.") var schemasPath: String? @@ -27,12 +37,26 @@ struct SmithyCodegenCLI: AsyncParsableCommand { @Option(help: "The full or relative path to write the Deserialize output file.") var deserializePath: String? + @Option(help: "The full or relative path to write the TypeRegistry output file.") + var typeRegistryPath: String? + + @Option(help: "The full or relative path to write the Operations output file.") + var operationsPath: String? + func run() async throws { let start = Date() let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() + let operations = (operations ?? "").split(separator: ",").map(String.init) + let settings = try SwiftSettings( + service: service, + sdkId: sdkId, + internal: `internal`, + operations: operations + ) + // Create the model file URL let modelFileURL = URL(fileURLWithPath: modelPath, relativeTo: currentWorkingDirectoryFileURL) guard FileManager.default.fileExists(atPath: modelFileURL.path) else { @@ -48,13 +72,21 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --deserialize-path was supplied, create the Deserialize file URL let deserializeFileURL = resolve(path: deserializePath) + // If --type-registry-path was supplied, create the TypeRegistry file URL + let typeRegistryFileURL = resolve(path: typeRegistryPath) + + // If --operations-path was supplied, create the Operations file URL + let operationsFileURL = resolve(path: operationsPath) + // Use resolved file URLs to run code generator try CodeGenerator( - service: service, + settings: settings, modelFileURL: modelFileURL, schemasFileURL: schemasFileURL, serializeFileURL: serializeFileURL, - deserializeFileURL: deserializeFileURL + deserializeFileURL: deserializeFileURL, + typeRegistryFileURL: typeRegistryFileURL, + operationsFileURL: operationsFileURL ).run() let duration = Date().timeIntervalSince(start) diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 5be1e87dd..e0ffef52f 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -12,11 +12,13 @@ import struct Smithy.ShapeID /// The wrapper for Swift-native code generation. public struct CodeGenerator { - let service: String + let settings: SwiftSettings let modelFileURL: URL let schemasFileURL: URL? let serializeFileURL: URL? let deserializeFileURL: URL? + let typeRegistryFileURL: URL? + let operationsFileURL: URL? /// Creates a code generator. /// - Parameters: @@ -24,17 +26,21 @@ public struct CodeGenerator { /// - modelFileURL: The file URL where the JSON AST model file can be accessed. /// - schemasFileURL: The file URL to which the `Schemas.swift` source file should be written. public init( - service: String, + settings: SwiftSettings, modelFileURL: URL, schemasFileURL: URL?, serializeFileURL: URL?, - deserializeFileURL: URL? + deserializeFileURL: URL?, + typeRegistryFileURL: URL?, + operationsFileURL: URL? ) throws { - self.service = service + self.settings = settings self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL self.serializeFileURL = serializeFileURL self.deserializeFileURL = deserializeFileURL + self.typeRegistryFileURL = typeRegistryFileURL + self.operationsFileURL = operationsFileURL } /// Executes the code generator. @@ -45,14 +51,11 @@ public struct CodeGenerator { let modelData = try Data(contentsOf: modelFileURL) let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) - // Create the service's ShapeID - let serviceID = try ShapeID(service) - // Create the model from the AST let model = try Model(astModel: astModel) // Create a generation context from the model - let ctx = try GenerationContext(serviceID: serviceID, model: model) + let ctx = try GenerationContext(settings: settings, model: model) // If a schemas file URL was provided, generate it if let schemasFileURL { @@ -71,5 +74,17 @@ public struct CodeGenerator { let deserializeContents = try DeserializeCodegen().generate(ctx: ctx) try Data(deserializeContents.utf8).write(to: deserializeFileURL) } + + // If a TypeRegistry file URL was provided, generate it + if let typeRegistryFileURL { + let typeRegistryContents = try TypeRegistryCodegen().generate(ctx: ctx) + try Data(typeRegistryContents.utf8).write(to: typeRegistryFileURL) + } + + // If an Operations file URL was provided, generate it + if let operationsFileURL { + let operationsContents = try OperationsCodegen().generate(ctx: ctx) + try Data(operationsContents.utf8).write(to: operationsFileURL) + } } } diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift index f873ef7b1..2808fbd28 100644 --- a/Sources/SmithyCodegenCore/GenerationContext.swift +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -9,6 +9,7 @@ import struct Smithy.ShapeID /// A type that provides the resources needed to perform Swift code generation. public struct GenerationContext { + public let settings: SwiftSettings public let service: ServiceShape public let model: Model public let symbolProvider: SymbolProvider @@ -19,19 +20,21 @@ public struct GenerationContext { /// - Parameter serviceID: The ``ShapeID`` for the service the model should be pruned to. /// - Parameter model: The ``Model`` to create the generation context from. /// - Throws: ``ModelError`` if the model does not contain exactly one service. - init(serviceID: ShapeID, model: Model) throws { + init(settings: SwiftSettings, model: Model) throws { // Perform model transformations here let finalModel = try model + .withOperations(settings: settings) .withSynthesizedInputsOutputs() - .withDeprecatedShapesRemoved() .withUnionsTargetingUnitAdded() - .optionalizeStructMembers(serviceID: serviceID) - .prune(serviceID: serviceID) + .optionalizeStructMembers(serviceID: settings.serviceID) + .withDeprecatedShapesRemoved() + .prune(serviceID: settings.serviceID) // Initialize using the final, processed model - self.service = try finalModel.expectServiceShape(id: serviceID) + self.settings = settings + self.service = try finalModel.expectServiceShape(id: settings.serviceID) self.model = finalModel - self.symbolProvider = SymbolProvider(service: service, model: finalModel) + self.symbolProvider = SymbolProvider(service: service, settings: settings, model: finalModel) } } diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift index 01c87cbcc..ebe5a8c01 100644 --- a/Sources/SmithyCodegenCore/Model/Model+AST.swift +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -18,7 +18,7 @@ extension Model { /// Compared to the AST model, this model has custom shape types, members are included in the main body of shapes /// along with other shape types, and all Shape IDs are fully-qualified /// (i.e. members have the enclosing shape's namespace & name, along with their own member name.) - /// - Parameter astModel: The JSON AST model to create a `Model` from. + /// - Parameter astModel: The JSON AST model to load into the `Model` being created. convenience init(astModel: ASTModel) throws { // Get all of the members from the AST model, create pairs of ShapeID & MemberShape let idToMemberShapePairs = try astModel.shapes @@ -113,6 +113,8 @@ extension Model { id: shapeID, traits: traits, operationIDs: try astShape.operations?.map { try $0.id } ?? [], + collectionOperationIDs: try astShape.collectionOperations?.map { try $0.id } ?? [], + resourceIDs: try astShape.resources?.map { try $0.id } ?? [], createID: try astShape.create?.id, putID: try astShape.put?.id, readID: try astShape.read?.id, diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift index 4a46c17ea..7c54f161b 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift @@ -34,98 +34,7 @@ extension Model { return sinceDate > cutoff } - var trimmedShapes = nonDeprecatedShapes - var previousTrimmedShapesCount = 0 - - // Now remove any members, lists, and maps that refer to deprecated shapes. - // We repeat this until no additional shapes are removed to ensure that nested - // references to deprecated shapes don't result in an inconsistent model. - repeat { - previousTrimmedShapesCount = trimmedShapes.count - let newTrimmedShapes = try trimmedShapes.filter { (_, shape) in - switch shape { - case let listShape as ListShape: - let id = try listShape.member.targetID - guard id.namespace != "smithy.api" else { return true } - let found = trimmedShapes[id] != nil - return found - case let mapShape as MapShape: - let id = try mapShape.value.targetID - guard id.namespace != "smithy.api" else { return true } - let found = trimmedShapes[id] != nil - return found - case let memberShape as MemberShape: - let id = memberShape.targetID - guard id.namespace != "smithy.api" else { return true } - let found = trimmedShapes[id] != nil - return found - default: - return true - } - } - trimmedShapes = newTrimmedShapes - } while trimmedShapes.count != previousTrimmedShapesCount - - // Finally, go through all the shapes and remove references to removed shapes. - let finalShapes = trimmedShapes.mapValues { shape -> Shape in - switch shape { - case let serviceShape as ServiceShape: - let operationIDs = serviceShape.operationIDs.filter { trimmedShapes[$0] != nil } - let resourceIDs = serviceShape.resourceIDs.filter { trimmedShapes[$0] != nil } - let errorIDs = serviceShape.errorIDs.filter { trimmedShapes[$0] != nil } - return ServiceShape( - id: serviceShape.id, - traits: serviceShape.traits, - operationIDs: operationIDs, - resourceIDs: resourceIDs, - errorIDs: errorIDs - ) - case let resourceShape as ResourceShape: - let operationIDs = resourceShape.operationIDs.filter { trimmedShapes[$0] != nil } - let createID = resourceShape.createID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil - let putID = resourceShape.putID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil - let readID = resourceShape.readID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil - let updateID = resourceShape.updateID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil - let deleteID = resourceShape.deleteID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil - let listID = resourceShape.listID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil - return ResourceShape( - id: resourceShape.id, - traits: resourceShape.traits, - operationIDs: operationIDs, - createID: createID, - putID: putID, - readID: readID, - updateID: updateID, - deleteID: deleteID, - listID: listID - ) - case let operationShape as OperationShape: - let errorIDs = operationShape.errorIDs.filter { trimmedShapes[$0] != nil } - return OperationShape( - id: operationShape.id, - traits: operationShape.traits, - inputID: operationShape.inputID, - outputID: operationShape.outputID, - errorIDs: errorIDs - ) - case let structureShape as StructureShape: - let memberIDs = structureShape.memberIDs.filter { trimmedShapes[$0] != nil } - return StructureShape(id: structureShape.id, traits: structureShape.traits, memberIDs: memberIDs) - case let unionShape as UnionShape: - let memberIDs = unionShape.memberIDs.filter { trimmedShapes[$0] != nil } - return UnionShape(id: unionShape.id, traits: unionShape.traits, memberIDs: memberIDs) - case let enumShape as EnumShape: - let memberIDs = enumShape.memberIDs.filter { trimmedShapes[$0] != nil } - return EnumShape(id: enumShape.id, traits: enumShape.traits, memberIDs: memberIDs) - case let intEnumShape as IntEnumShape: - let memberIDs = intEnumShape.memberIDs.filter { trimmedShapes[$0] != nil } - return IntEnumShape(id: intEnumShape.id, traits: intEnumShape.traits, memberIDs: memberIDs) - default: - return shape - } - } - - // Create the transformed model, and return it to the caller. - return Model(version: self.version, metadata: self.metadata, shapes: finalShapes) + // Trim references to the removed shapes before returning + return try Model(version: version, metadata: metadata, shapes: nonDeprecatedShapes).trimReferences() } } diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Operations.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Operations.swift new file mode 100644 index 000000000..e02a61f3d --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Operations.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension Model { + + /// Reduces the service's operations to those specified in SwiftSettings. + /// + /// If SwiftSettings has a null or empty `operations` param, all operations are retained. + /// - Parameter settings: The SwiftSettings for rendering this service + /// - Returns: A model with operations as specified by SwiftSettings + func withOperations(settings: SwiftSettings) throws -> Model { + + // If settings.operations is empty, leave the model unchanged + guard !settings.operationIDs.isEmpty else { return self } + + // Remove any operation that is not included in the list of operationIDs + let newShapes = shapes.filter { shapeID, shape in + guard shape.type == .operation else { return true } + return settings.operationIDs.contains(shapeID) + } + + // Create a Model, then trim references to the removed operations + return try Model(version: version, metadata: metadata, shapes: newShapes).trimReferences() + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift new file mode 100644 index 000000000..52e80dd53 --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift @@ -0,0 +1,127 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension Model { + + /// Deletes any references to shapes that have been removed from a model. + /// + /// Apply this transform after removing shapes from a model to ensure the model + /// stays consistent and doesn't include any references to missing shapes. + /// + /// This transform will typically be applied within other transforms to make sure + /// the model is consistent and only references existing shapes after a deletion or + /// mutation. + /// - Returns: A model without any references to removed shapes. + func trimReferences() throws -> Model { + var trimmedShapes = self.shapes + var previousTrimmedShapesCount = 0 + + // Now remove any members, lists, and maps that refer to nonexistent shapes. + // We repeat this until no additional shapes are removed to ensure that nested + // references to nonexistent shapes don't result in an inconsistent model. + repeat { + previousTrimmedShapesCount = trimmedShapes.count + let newTrimmedShapes = try trimmedShapes.filter { (_, shape) in + switch shape { + case let listShape as ListShape: + // Keep this list if its member's target is present + let id = try listShape.member.targetID + guard id.namespace != "smithy.api" else { return true } + let found = trimmedShapes[id] != nil + return found + case let mapShape as MapShape: + // Keep this map if its `value` member's target is present + let id = try mapShape.value.targetID + guard id.namespace != "smithy.api" else { return true } + let found = trimmedShapes[id] != nil + return found + case let memberShape as MemberShape: + // Check if this member's target is present, if the target is not in the prelude + let targetPresent = if memberShape.targetID.namespace == "smithy.api" { + true + } else { + trimmedShapes[memberShape.targetID] != nil + } + // Check if this member's container is present + let containerPresent = trimmedShapes[memberShape.containerID] != nil + + // Keep this member if its container and target are both present + return targetPresent && containerPresent + default: + return true + } + } + trimmedShapes = newTrimmedShapes + } while trimmedShapes.count != previousTrimmedShapesCount + + // Finally, go through all the shapes and remove references to removed shapes. + let finalShapes = trimmedShapes.mapValues { shape -> Shape in + switch shape { + case let serviceShape as ServiceShape: + let operationIDs = serviceShape.operationIDs.filter { trimmedShapes[$0] != nil } + let resourceIDs = serviceShape.resourceIDs.filter { trimmedShapes[$0] != nil } + let errorIDs = serviceShape.errorIDs.filter { trimmedShapes[$0] != nil } + return ServiceShape( + id: serviceShape.id, + traits: serviceShape.traits, + operationIDs: operationIDs, + resourceIDs: resourceIDs, + errorIDs: errorIDs + ) + case let resourceShape as ResourceShape: + let operationIDs = resourceShape.operationIDs.filter { trimmedShapes[$0] != nil } + let collectionOperationIDs = resourceShape.collectionOperationIDs.filter { trimmedShapes[$0] != nil } + let resourceIDs = resourceShape.resourceIDs.filter { trimmedShapes[$0] != nil } + let createID = resourceShape.createID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let putID = resourceShape.putID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let readID = resourceShape.readID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let updateID = resourceShape.updateID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let deleteID = resourceShape.deleteID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let listID = resourceShape.listID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + return ResourceShape( + id: resourceShape.id, + traits: resourceShape.traits, + operationIDs: operationIDs, + collectionOperationIDs: collectionOperationIDs, + resourceIDs: resourceIDs, + createID: createID, + putID: putID, + readID: readID, + updateID: updateID, + deleteID: deleteID, + listID: listID + ) + case let operationShape as OperationShape: + let errorIDs = operationShape.errorIDs.filter { trimmedShapes[$0] != nil } + return OperationShape( + id: operationShape.id, + traits: operationShape.traits, + inputID: operationShape.inputID, + outputID: operationShape.outputID, + errorIDs: errorIDs + ) + case let structureShape as StructureShape: + let memberIDs = structureShape.memberIDs.filter { trimmedShapes[$0] != nil } + return StructureShape(id: structureShape.id, traits: structureShape.traits, memberIDs: memberIDs) + case let unionShape as UnionShape: + let memberIDs = unionShape.memberIDs.filter { trimmedShapes[$0] != nil } + return UnionShape(id: unionShape.id, traits: unionShape.traits, memberIDs: memberIDs) + case let enumShape as EnumShape: + let memberIDs = enumShape.memberIDs.filter { trimmedShapes[$0] != nil } + return EnumShape(id: enumShape.id, traits: enumShape.traits, memberIDs: memberIDs) + case let intEnumShape as IntEnumShape: + let memberIDs = intEnumShape.memberIDs.filter { trimmedShapes[$0] != nil } + return IntEnumShape(id: intEnumShape.id, traits: intEnumShape.traits, memberIDs: memberIDs) + default: + return shape + } + } + + // Create the transformed model, and return it to the caller. + return Model(version: self.version, metadata: self.metadata, shapes: finalShapes) + } +} diff --git a/Sources/SmithyCodegenCore/Operations/OperationsCodegen.swift b/Sources/SmithyCodegenCore/Operations/OperationsCodegen.swift new file mode 100644 index 000000000..191054f05 --- /dev/null +++ b/Sources/SmithyCodegenCore/Operations/OperationsCodegen.swift @@ -0,0 +1,42 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +struct OperationsCodegen { + + func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import Smithy") + writer.write("import SmithySerialization") + writer.write("") + + let sortedOperations = ctx.model.allShapesSorted + .filter { $0.type == .operation } + .compactMap { $0 as? OperationShape } + + let clientSymbol = try ctx.symbolProvider.swiftType(shape: ctx.service) + + try writer.openBlock("\(ctx.settings.scope) extension \(clientSymbol) {", "}") { writer in + for operation in sortedOperations { + writer.write("") + let varName = "\(try ctx.symbolProvider.operationMethodName(operation: operation))Operation" + let type = "SmithySerialization.Operation" + let input = try ctx.symbolProvider.swiftType(shape: operation.input) + let output = try ctx.symbolProvider.swiftType(shape: operation.output) + try writer.openBlock("static var \(varName): \(type)<\(input), \(output)> {", "}") { writer in + try writer.openBlock(".init(", ")") { writer in + writer.write("schema: \(try operation.schemaVarName),") + writer.write("serviceSchema: \(try ctx.service.schemaVarName),") + writer.write("inputSchema: \(try operation.input.schemaVarName),") + writer.write("outputSchema: \(try operation.output.schemaVarName),") + writer.write("errorTypeRegistry: \(clientSymbol).errorTypeRegistry") + } + } + } + } + return writer.contents + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ResourceShape.swift b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift index 8c805f170..18670cb3a 100644 --- a/Sources/SmithyCodegenCore/Shape/ResourceShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift @@ -12,6 +12,8 @@ import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy resources. public class ResourceShape: Shape { let operationIDs: [ShapeID] + let collectionOperationIDs: [ShapeID] + let resourceIDs: [ShapeID] let createID: ShapeID? let putID: ShapeID? let readID: ShapeID? @@ -23,6 +25,8 @@ public class ResourceShape: Shape { id: ShapeID, traits: TraitCollection, operationIDs: [ShapeID], + collectionOperationIDs: [ShapeID], + resourceIDs: [ShapeID], createID: ShapeID?, putID: ShapeID?, readID: ShapeID?, @@ -31,6 +35,8 @@ public class ResourceShape: Shape { listID: ShapeID? ) { self.operationIDs = operationIDs + self.collectionOperationIDs = collectionOperationIDs + self.resourceIDs = resourceIDs self.createID = createID self.putID = putID self.readID = readID @@ -41,7 +47,8 @@ public class ResourceShape: Shape { } override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { - let allOps = [createID, putID, readID, updateID, deleteID, listID].compactMap { $0 } + operationIDs + let crudIDs = [createID, putID, readID, updateID, deleteID, listID].compactMap { $0 } + let allOps = crudIDs + operationIDs + collectionOperationIDs + resourceIDs return try Set(allOps.map { try model.expectShape(id: $0) }) } } diff --git a/Sources/SmithyCodegenCore/SwiftSettings.swift b/Sources/SmithyCodegenCore/SwiftSettings.swift new file mode 100644 index 000000000..a97989918 --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftSettings.swift @@ -0,0 +1,42 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +public struct SwiftSettings: Sendable { + let serviceID: ShapeID + let sdkId: String + let `internal`: Bool + let operationIDs: [ShapeID] + + public init( + service: String, + sdkId: String?, + `internal`: Bool = false, + operations: [String] = [] + ) throws { + self.serviceID = try ShapeID(service) + self.sdkId = sdkId ?? serviceID.name + self.`internal` = `internal` + self.operationIDs = try operations.map(ShapeID.init) + } + + var scope: String { + `internal` ? "package" : "public" + } + + var serviceName: String { + let serviceSuffix = " Service" + var deserviced = sdkId + if deserviced.hasSuffix(serviceSuffix) { + deserviced.removeLast(serviceSuffix.count) + } + return deserviced + .toUpperCamelCase() + .replacingOccurrences(of: " ", with: "") + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift index 804d1efd2..6d2c53d86 100644 --- a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -14,21 +14,15 @@ import struct Smithy.ShapeID public struct SymbolProvider { let service: ServiceShape + let settings: SwiftSettings let model: Model - init(service: ServiceShape, model: Model) { + init(service: ServiceShape, settings: SwiftSettings, model: Model) { self.service = service + self.settings = settings self.model = model } - var serviceName: String { - get throws { - return try service.sdkIdStrippingService - .replacingOccurrences(of: " ", with: "") - .replacingOccurrences(of: "Service", with: "") - } - } - public func swiftType(shape: Shape) throws -> String { switch shape.type { case .structure, .union, .enum, .intEnum: @@ -83,11 +77,7 @@ public struct SymbolProvider { case .document: return "Smithy.Document" case .service: - // Returns the type name for the client - guard let serviceShape = shape as? ServiceShape else { - throw SymbolProviderError("Shape has type .service but is not a ServiceShape") - } - return try "\(serviceShape.clientBaseName)Client" + return "\(settings.serviceName)Client" case .member, .operation, .resource: throw SymbolProviderError("Cannot provide Swift symbol for shape type \(shape.type)") } diff --git a/Sources/SmithyCodegenCore/TypeRegistry/TypeRegistryCodegen.swift b/Sources/SmithyCodegenCore/TypeRegistry/TypeRegistryCodegen.swift new file mode 100644 index 000000000..3eb5d3e18 --- /dev/null +++ b/Sources/SmithyCodegenCore/TypeRegistry/TypeRegistryCodegen.swift @@ -0,0 +1,41 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ErrorTrait + +struct TypeRegistryCodegen { + + func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + + writer.write("import SmithySerialization") + writer.write("") + + let serviceType = try ctx.symbolProvider.swiftType(shape: ctx.symbolProvider.service) + try writer.openBlock("extension \(serviceType) {", "}") { writer in + writer.write("") + try writer.openBlock( + "public static let errorTypeRegistry = SmithySerialization.TypeRegistry(", + ")" + ) { writer in + let allErrorShapesSorted = ctx.model.allShapesSorted + .filter { $0.hasTrait(ErrorTrait.self) } + try writer.openBlock("[", "]") { writer in + try allErrorShapesSorted.forEach { errorShape in + try writer.openBlock(".init(", "),") { writer in + let schemaVarName = try errorShape.schemaVarName + writer.write("schema: \(schemaVarName),") + let swiftType = try ctx.symbolProvider.swiftType(shape: errorShape) + writer.write("swiftType: \(swiftType).self") + } + } + } + } + } + return writer.contents + } +} diff --git a/Sources/SmithySerialization/ClientProtocol/ClientProtocol.swift b/Sources/SmithySerialization/ClientProtocol/ClientProtocol.swift new file mode 100644 index 000000000..5097b5ead --- /dev/null +++ b/Sources/SmithySerialization/ClientProtocol/ClientProtocol.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class Smithy.Context +import protocol Smithy.RequestMessage +import protocol Smithy.ResponseMessage +import struct Smithy.ShapeID + +public protocol ClientProtocol: Sendable { + associatedtype RequestType: RequestMessage + associatedtype ResponseType: ResponseMessage + + /// The shape ID of the AWS or Smithy protocol that this type implements. + /// + /// Example: `aws.protocols#awsJson1_0`, `smithy.protocols#rpcv2Cbor` + var id: ShapeID { get } + + /// The codec for this protocol. + var codec: Codec { get } + + func serializeRequest( + operation: Operation, + input: Input, + requestBuilder: RequestType.RequestBuilderType, + context: Context + ) throws + + func deserializeResponse( + operation: Operation, + context: Context, + response: ResponseType + ) async throws -> Output +} diff --git a/Sources/SmithySerialization/ClientProtocol/Operation.swift b/Sources/SmithySerialization/ClientProtocol/Operation.swift new file mode 100644 index 000000000..466c8f674 --- /dev/null +++ b/Sources/SmithySerialization/ClientProtocol/Operation.swift @@ -0,0 +1,43 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public struct Operation { + private let _schema: @Sendable () -> Schema + private let _serviceSchema: @Sendable () -> Schema + private let _inputSchema: @Sendable () -> Schema + private let _outputSchema: @Sendable () -> Schema + private let _errorTypeRegistry: @Sendable () -> TypeRegistry + + public init( + schema: @autoclosure @escaping @Sendable () -> Schema, + serviceSchema: @autoclosure @escaping @Sendable () -> Schema, + inputSchema: @autoclosure @escaping @Sendable () -> Schema, + outputSchema: @autoclosure @escaping @Sendable () -> Schema, + errorTypeRegistry: @autoclosure @escaping @Sendable () -> TypeRegistry + ) { + self._schema = schema + self._serviceSchema = serviceSchema + self._inputSchema = inputSchema + self._outputSchema = outputSchema + self._errorTypeRegistry = errorTypeRegistry + } +} + +extension Operation: OperationProperties { + + public var schema: Schema { _schema() } + + public var serviceSchema: Schema { _serviceSchema() } + + public var inputSchema: Schema { _inputSchema() } + + public var outputSchema: Schema { _outputSchema() } + + public var errorTypeRegistry: TypeRegistry { _errorTypeRegistry() } +} diff --git a/Sources/SmithySerialization/ClientProtocol/OperationProperties.swift b/Sources/SmithySerialization/ClientProtocol/OperationProperties.swift new file mode 100644 index 000000000..53be747b4 --- /dev/null +++ b/Sources/SmithySerialization/ClientProtocol/OperationProperties.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public protocol OperationProperties: Sendable { + var schema: Schema { get } + var serviceSchema: Schema { get } + var inputSchema: Schema { get } + var outputSchema: Schema { get } + var errorTypeRegistry: TypeRegistry { get } +} diff --git a/Sources/SmithySerialization/Codec.swift b/Sources/SmithySerialization/Codec.swift new file mode 100644 index 000000000..9872ed4f4 --- /dev/null +++ b/Sources/SmithySerialization/Codec.swift @@ -0,0 +1,13 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data + +public protocol Codec: Sendable { + func makeSerializer() throws -> any ShapeSerializer + func makeDeserializer(data: Data) throws -> any ShapeDeserializer +} diff --git a/Sources/SmithySerialization/Serialization/ShapeSerializer.swift b/Sources/SmithySerialization/Serialization/ShapeSerializer.swift index b8e91ed12..48d5dc5ef 100644 --- a/Sources/SmithySerialization/Serialization/ShapeSerializer.swift +++ b/Sources/SmithySerialization/Serialization/ShapeSerializer.swift @@ -8,6 +8,7 @@ import struct Foundation.Data import struct Foundation.Date import enum Smithy.ByteStream +import enum Smithy.Prelude import struct Smithy.Schema import protocol Smithy.SmithyDocument @@ -32,7 +33,7 @@ public protocol ShapeSerializer { func writeDataStream(_ schema: Schema, _ value: ByteStream) throws func writeEventStream(_ schema: Schema, _ value: AsyncThrowingStream) throws - var data: Data { get } + var data: Data { get throws } } public extension ShapeSerializer { @@ -96,36 +97,36 @@ public extension ShapeSerializer { func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { switch value.type { case .blob: - try writeBlob(schema, value.asBlob()) + try writeBlob(Prelude.blobSchema, value.asBlob()) case .boolean: - try writeBoolean(schema, value.asBoolean()) + try writeBoolean(Prelude.booleanSchema, value.asBoolean()) case .string: - try writeString(schema, value.asString()) + try writeString(Prelude.stringSchema, value.asString()) case .timestamp: - try writeTimestamp(schema, value.asTimestamp()) + try writeTimestamp(Prelude.timestampSchema, value.asTimestamp()) case .byte: - try writeByte(schema, value.asByte()) + try writeByte(Prelude.byteSchema, value.asByte()) case .short: - try writeShort(schema, value.asShort()) + try writeShort(Prelude.shortSchema, value.asShort()) case .integer: - try writeInteger(schema, value.asInteger()) + try writeInteger(Prelude.integerSchema, value.asInteger()) case .long: - try writeLong(schema, Int(value.asLong())) + try writeLong(Prelude.longSchema, Int(value.asLong())) case .float: - try writeFloat(schema, value.asFloat()) + try writeFloat(Prelude.floatSchema, value.asFloat()) case .double: - try writeDouble(schema, value.asDouble()) + try writeDouble(Prelude.doubleSchema, value.asDouble()) case .bigDecimal: - try writeBigDecimal(schema, value.asBigDecimal()) + try writeBigDecimal(Prelude.bigDecimalSchema, value.asBigDecimal()) case .bigInteger: - try writeBigInteger(schema, value.asBigInteger()) + try writeBigInteger(Prelude.bigIntegerSchema, value.asBigInteger()) case .list, .set: - try writeList(schema, value.asList()) { element, serializer in - try serializer.writeDocument(schema.member, element) + try writeList(Prelude.listDocumentSchema, value.asList()) { element, serializer in + try serializer.writeDocument(Prelude.listDocumentSchema.member, element) } case .map: - try writeMap(schema, value.asStringMap()) { value, serializer in - try serializer.writeDocument(schema.value, value) + try writeMap(Prelude.mapDocumentSchema, value.asStringMap()) { value, serializer in + try serializer.writeDocument(Prelude.mapDocumentSchema.value, value) } case .document, .enum, .intEnum, .structure, .union, .member, .service, .resource, .operation: throw SerializerError("Unsupported or invalid document type: \(value.type)") diff --git a/Sources/SmithySerialization/TypeRegistry/TypeRegistry.swift b/Sources/SmithySerialization/TypeRegistry/TypeRegistry.swift new file mode 100644 index 000000000..35749a592 --- /dev/null +++ b/Sources/SmithySerialization/TypeRegistry/TypeRegistry.swift @@ -0,0 +1,41 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema +import struct Smithy.ShapeID + +public struct TypeRegistry: Sendable { + + public struct Entry: Sendable { + private let _schema: @Sendable () -> Schema + public let swiftType: any DeserializableShape.Type + + public init( + schema: @escaping @Sendable @autoclosure () -> Schema, + swiftType: DeserializableShape.Type + ) { + self._schema = schema + self.swiftType = swiftType + } + + public var schema: Schema { _schema() } + } + + private let idMap: [ShapeID: Entry] + + public init(_ entries: [Entry]) { + self.idMap = Dictionary(uniqueKeysWithValues: entries.map { ($0.schema.id, $0) }) + } + + public subscript(shapeID: ShapeID) -> Entry? { + idMap[shapeID] + } + + public func codeLookup(code: String, matcher: (String, Entry) throws -> Bool) rethrows -> Entry? { + try idMap.values.first { try matcher(code, $0) } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt index 25156c711..91403660d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt @@ -97,8 +97,8 @@ class DirectedSwiftCodegen( LOGGER.info("[${service.id}] Generating additional files") integrations.forEach { it.writeAdditionalFiles(context, ctx, writers) } - LOGGER.info("[${service.id}] Generating Smithy model file info") - SmithyModelFileInfoGenerator(ctx).writeSmithyModelFileInfo() + LOGGER.info("[${service.id}] Generating swift-settings.json") + SwiftSettingsJSONGenerator(ctx).render() } LOGGER.info("[${service.id}] Generating package manifest file") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt deleted file mode 100644 index 6daae0e29..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt +++ /dev/null @@ -1,20 +0,0 @@ -package software.amazon.smithy.swift.codegen - -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator - -class SmithyModelFileInfoGenerator( - val ctx: ProtocolGenerator.GenerationContext, -) { - fun writeSmithyModelFileInfo() { - if (ctx.settings.moduleName.startsWith("Internal")) return - val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" - ctx.delegator.useFileWriter(filename) { writer -> - val service = ctx.settings.service - val path = ctx.settings.modelPath - writer.openBlock("{", "}") { - writer.write("\"service\": \"$service\",") - writer.write("\"path\": \"$path\"") - } - } - } -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt index 9c2397927..b4d1b64c2 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt @@ -29,24 +29,6 @@ import kotlin.jvm.optionals.getOrElse import kotlin.jvm.optionals.getOrNull import kotlin.streams.toList -private const val SERVICE = "service" -private const val MODULE_NAME = "module" -private const val MODULE_VERSION = "moduleVersion" -private const val MODULE_DESCRIPTION = "moduleDescription" -private const val AUTHOR = "author" -private const val HOMEPAGE = "homepage" - -// If SDK_ID is not provided by the service model, the value of sdkId defaults to the Service's shape id name. -private const val SDK_ID = "sdkId" -private const val GIT_REPO = "gitRepo" -private const val SWIFT_VERSION = "swiftVersion" -private const val MERGE_MODELS = "mergeModels" -private const val COPYRIGHT_NOTICE = "copyrightNotice" -private const val VISIBILITY = "visibility" -private const val INTERNAL_CLIENT = "internalClient" -private const val OPERATIONS = "operations" -private const val MODEL_PATH = "modelPath" - // Prioritized list of protocols supported for code generation private val DEFAULT_PROTOCOL_RESOLUTION_PRIORITY = listOf( @@ -77,6 +59,24 @@ class SwiftSettings( val modelPath: String, ) { companion object { + const val SERVICE = "service" + const val MODULE_NAME = "module" + const val MODULE_VERSION = "moduleVersion" + const val MODULE_DESCRIPTION = "moduleDescription" + const val AUTHOR = "author" + const val HOMEPAGE = "homepage" + + // If SDK_ID is not provided by the service model, the value of sdkId defaults to the Service's shape id name. + const val SDK_ID = "sdkId" + const val GIT_REPO = "gitRepo" + const val SWIFT_VERSION = "swiftVersion" + const val MERGE_MODELS = "mergeModels" + const val COPYRIGHT_NOTICE = "copyrightNotice" + const val VISIBILITY = "visibility" + const val INTERNAL_CLIENT = "internalClient" + const val OPERATIONS = "operations" + const val MODEL_PATH = "modelPath" + private val LOGGER: Logger = Logger.getLogger(SwiftSettings::class.java.name) /** diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettingsJSONGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettingsJSONGenerator.kt new file mode 100644 index 000000000..bf1d7ae2f --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettingsJSONGenerator.kt @@ -0,0 +1,27 @@ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator + +class SwiftSettingsJSONGenerator( + val ctx: ProtocolGenerator.GenerationContext, +) { + fun render() { + ctx.delegator.useFileWriter("Sources/${ctx.settings.moduleName}/swift-settings.json") { writer -> + val node = + ObjectNode + .builder() + .withMember(SwiftSettings.SERVICE, ctx.settings.service.toString()) + .withMember(SwiftSettings.MODULE_NAME, ctx.settings.moduleName) + .withMember(SwiftSettings.MODULE_VERSION, ctx.settings.moduleVersion) + .withMember(SwiftSettings.SDK_ID, ctx.settings.sdkId) + .withMember(SwiftSettings.INTERNAL_CLIENT, ctx.settings.internalClient) + .withMember(SwiftSettings.OPERATIONS, ArrayNode.fromStrings(ctx.settings.operations)) + .withMember(SwiftSettings.MODEL_PATH, ctx.settings.modelPath) + .build() + writer.write(Node.prettyPrintJson(node)) + } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt index 980bd1598..db6e7d7c6 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt @@ -18,7 +18,6 @@ import software.amazon.smithy.swift.codegen.model.defaultValue import software.amazon.smithy.swift.codegen.model.getTrait import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes -import software.amazon.smithy.swift.codegen.utils.clientName import software.amazon.smithy.swift.codegen.utils.toLowerCamelCase /** @@ -93,7 +92,7 @@ class EndpointParamsGenerator( parameters: List, ) { writer.apply { - val paramsType = ctx.settings.sdkId.clientName() + "AuthSchemeResolverParameters" + val paramsType = ctx.settings.clientBaseNamePreservingService + "AuthSchemeResolverParameters" openBlock("public init (authSchemeParams: \$L) {", "}", paramsType) { parameters.forEach { val memberName = it.name.toString().toLowerCamelCase() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt index 8063a727a..83bc9383c 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt @@ -18,7 +18,6 @@ import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.shapes.ShapeType import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.TimestampShape @@ -58,7 +57,7 @@ import software.amazon.smithy.swift.codegen.integration.middlewares.SignerMiddle import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpHeaderProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpQueryItemProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpUrlPathProvider -import software.amazon.smithy.swift.codegen.integration.serde.schema.SchemaGenerator +import software.amazon.smithy.swift.codegen.integration.serde.SerdeUtils import software.amazon.smithy.swift.codegen.integration.serde.struct.StructDecodeGenerator import software.amazon.smithy.swift.codegen.integration.serde.struct.StructEncodeGenerator import software.amazon.smithy.swift.codegen.integration.serde.union.UnionDecodeGenerator @@ -73,7 +72,6 @@ import software.amazon.smithy.swift.codegen.model.isOutputEventStream import software.amazon.smithy.swift.codegen.supportsStreamingAndIsRPC import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.utils.ModelFileUtils -import software.amazon.smithy.swift.codegen.utils.SchemaFileUtils import software.amazon.smithy.utils.OptionalUtils import java.util.Optional import java.util.logging.Logger @@ -140,7 +138,7 @@ abstract class HTTPBindingProtocolGenerator( override var serviceErrorProtocolSymbol: Symbol = ClientRuntimeTypes.Http.HttpError override fun generateSerializers(ctx: ProtocolGenerator.GenerationContext) { -// if (usesSchemaBasedSerialization(ctx)) return // temporary condition + val usesSchemaBased = SerdeUtils.useSchemaBased(ctx) // temporary condition // render conformance to HttpRequestBinding for all input shapes val inputShapesWithHttpBindings: MutableSet = mutableSetOf() for (operation in getHttpBindingOperations(ctx)) { @@ -151,9 +149,11 @@ abstract class HTTPBindingProtocolGenerator( continue } val httpBindingResolver = getProtocolHttpBindingResolver(ctx, defaultContentType) - HttpUrlPathProvider.renderUrlPathMiddleware(ctx, operation, httpBindingResolver) - HttpHeaderProvider.renderHeaderMiddleware(ctx, operation, httpBindingResolver, customizations.defaultTimestampFormat) - HttpQueryItemProvider.renderQueryMiddleware(ctx, operation, httpBindingResolver, customizations.defaultTimestampFormat) + if (!usesSchemaBased) { + HttpUrlPathProvider.renderUrlPathMiddleware(ctx, operation, httpBindingResolver) + HttpHeaderProvider.renderHeaderMiddleware(ctx, operation, httpBindingResolver, customizations.defaultTimestampFormat) + HttpQueryItemProvider.renderQueryMiddleware(ctx, operation, httpBindingResolver, customizations.defaultTimestampFormat) + } inputShapesWithHttpBindings.add(inputShapeId) } } @@ -173,6 +173,7 @@ abstract class HTTPBindingProtocolGenerator( .name(symbolName) .build() val httpBodyMembers = httpBodyMembers(ctx, shape) + if (usesSchemaBased) return if (httpBodyMembers.isNotEmpty() || shouldRenderEncodableConformance) { ctx.delegator.useShapeWriter(encodeSymbol) { writer -> writer.openBlock( @@ -189,14 +190,14 @@ abstract class HTTPBindingProtocolGenerator( } override fun generateDeserializers(ctx: ProtocolGenerator.GenerationContext) { -// if (usesSchemaBasedSerialization(ctx)) return // temporary condition + if (SerdeUtils.useSchemaBased(ctx)) return // temporary condition val httpOperations = getHttpBindingOperations(ctx) val httpBindingResolver = getProtocolHttpBindingResolver(ctx, defaultContentType) httpResponseGenerator.render(ctx, httpOperations, httpBindingResolver) } override fun generateCodableConformanceForNestedTypes(ctx: ProtocolGenerator.GenerationContext) { -// if (usesSchemaBasedSerialization(ctx)) return // temporary condition + if (SerdeUtils.useSchemaBased(ctx)) return // temporary condition val nestedShapes = resolveShapesNeedingCodableConformance(ctx) .filter { !it.isEventStreaming } @@ -206,43 +207,11 @@ abstract class HTTPBindingProtocolGenerator( } } - private fun usesSchemaBasedSerialization(ctx: ProtocolGenerator.GenerationContext): Boolean = - // This fun is temporary; it will be eliminated when all services/protocols are moved to schema-based - ctx.service.allTraits.keys - .any { it.name == "rpcv2Cbor" } - - override fun generateSchemas(ctx: ProtocolGenerator.GenerationContext) { - if (!usesSchemaBasedSerialization(ctx)) return // temporary condition - val nestedShapes = - resolveShapesNeedingSchema(ctx) - .filter { it.type != ShapeType.MEMBER } // Member schemas are only rendered in-line - nestedShapes.forEach { renderSchemas(ctx, it) } - } - - private fun renderSchemas( - ctx: ProtocolGenerator.GenerationContext, - shape: Shape, - ) { - val symbol: Symbol = ctx.symbolProvider.toSymbol(shape) - val symbolName = symbol.name - val filename = SchemaFileUtils.filename(ctx.settings, "${shape.id.name}+Schema") - val encodeSymbol = - Symbol - .builder() - .definitionFile(filename) - .name(symbolName) - .build() - ctx.delegator.useShapeWriter(encodeSymbol) { writer -> - SchemaGenerator(ctx, writer).renderSchema(shape) - } - } - fun renderCodableExtension( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { -// if (!usesSchemaBasedSerialization(ctx)) return // temporary condition - if (!shape.hasTrait() && !shape.hasTrait()) { + if (!shape.hasTrait() && !(shape.hasTrait())) { return } val symbol: Symbol = ctx.symbolProvider.toSymbol(shape) @@ -399,74 +368,6 @@ abstract class HTTPBindingProtocolGenerator( return resolved } - private fun resolveShapesNeedingSchema(ctx: ProtocolGenerator.GenerationContext): Set { - val topLevelInputMembers = - getHttpBindingOperations(ctx) - .flatMap { - val inputShape = ctx.model.expectShape(it.input.get()) - inputShape.members() - }.map { ctx.model.expectShape(it.target) } - .toSet() - - val topLevelOutputMembers = - getHttpBindingOperations(ctx) - .map { ctx.model.expectShape(it.output.get()) } - .toSet() - - val topLevelErrorMembers = - getHttpBindingOperations(ctx) - .flatMap { it.errors } - .map { ctx.model.expectShape(it) } - .toSet() - - val topLevelServiceErrorMembers = - ctx.service.errors - .map { ctx.model.expectShape(it) } - .toSet() - - val allTopLevelMembers = - topLevelInputMembers - .union(topLevelOutputMembers) - .union(topLevelErrorMembers) - .union(topLevelServiceErrorMembers) - - return walkNestedShapesRequiringSchema(ctx, allTopLevelMembers) - } - - private fun walkNestedShapesRequiringSchema( - ctx: ProtocolGenerator.GenerationContext, - shapes: Set, - ): Set { - val resolved = mutableSetOf() - val walker = Walker(ctx.model) - - // walk all the shapes in the set and find all other - // structs/unions (or collections thereof) in the graph from that shape - shapes.forEach { shape -> - walker - .iterateShapes(shape) { relationship -> - when (relationship.relationshipType) { - RelationshipType.MEMBER_TARGET, - RelationshipType.STRUCTURE_MEMBER, - RelationshipType.LIST_MEMBER, - RelationshipType.SET_MEMBER, - RelationshipType.MAP_KEY, - RelationshipType.MAP_VALUE, - RelationshipType.UNION_MEMBER, - -> true - else -> false - } - }.forEach { - // Don't generate schemas for Smithy built-in / "prelude" shapes. - // Those are included in runtime. - if (it.id.namespace != "smithy.api") { - resolved.add(it) - } - } - } - return resolved - } - // Checks for @requiresLength trait // Returns true if the operation: // - has a streaming member with @httpPayload trait @@ -530,7 +431,7 @@ abstract class HTTPBindingProtocolGenerator( operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, resolver.determineRequestContentType(operation)), ) - operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider)) + operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx)) operationMiddleware.appendMiddleware( operation, ContentLengthMiddleware( @@ -653,6 +554,8 @@ abstract class HTTPBindingProtocolGenerator( } override fun generateMessageMarshallable(ctx: ProtocolGenerator.GenerationContext) { + val usesSchemaBased = SerdeUtils.useSchemaBased(ctx) // temporary condition + if (usesSchemaBased) return val streamingShapes = inputStreamingShapes(ctx) val messageMarshallableGenerator = MessageMarshallableGenerator(ctx, defaultContentType) streamingShapes.forEach { streamingMember -> @@ -661,6 +564,8 @@ abstract class HTTPBindingProtocolGenerator( } override fun generateMessageUnmarshallable(ctx: ProtocolGenerator.GenerationContext) { + val usesSchemaBased = SerdeUtils.useSchemaBased(ctx) // temporary condition + if (usesSchemaBased) return val streamingShapes = outputStreamingShapes(ctx) val messageUnmarshallableGenerator = MessageUnmarshallableGenerator(ctx, customizations) streamingShapes.forEach { streamingMember -> diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt index bfcf3479b..fa558d524 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt @@ -122,8 +122,6 @@ interface ProtocolGenerator { */ fun generateCodableConformanceForNestedTypes(ctx: GenerationContext) - fun generateSchemas(ctx: GenerationContext) - /** * * Generate unit tests for the protocol diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt index a250a673f..8759f7f77 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt @@ -17,6 +17,7 @@ import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.httpResponse.bindingTraits.HTTPResponseTraitPayload import software.amazon.smithy.swift.codegen.integration.httpResponse.bindingTraits.HTTPResponseTraitResponseCode import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.integration.serde.SerdeUtils import software.amazon.smithy.swift.codegen.integration.serde.readwrite.AWSProtocol import software.amazon.smithy.swift.codegen.integration.serde.readwrite.awsProtocol import software.amazon.smithy.swift.codegen.integration.serde.struct.readerSymbol @@ -34,6 +35,7 @@ class HTTPResponseBindingOutputGenerator( httpBindingResolver: HttpBindingResolver, defaultTimestampFormat: TimestampFormatTrait.Format, ) { + if (SerdeUtils.useSchemaBased(ctx)) return if (op.output.isEmpty) { return } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt index 2f90f68a0..01090271a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt @@ -4,8 +4,6 @@ import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait import software.amazon.smithy.aws.traits.protocols.RestJson1Trait import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.DocumentShape import software.amazon.smithy.model.shapes.EnumShape @@ -31,8 +29,7 @@ import software.amazon.smithy.swift.codegen.supportsStreamingAndIsRPC import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes class OperationInputBodyMiddleware( - val model: Model, - val symbolProvider: SymbolProvider, + val ctx: ProtocolGenerator.GenerationContext, private val alwaysSendBody: Boolean = false, ) : MiddlewareRenderable { override val name = "OperationInputBodyMiddleware" @@ -54,9 +51,9 @@ class OperationInputBodyMiddleware( ) { val writingClosureUtils = WritingClosureUtils(ctx, writer) val nodeInfoUtils = NodeInfoUtils(ctx, writer, ctx.service.requestWireProtocol) - val inputShape = MiddlewareShapeUtils.inputShape(model, op) - val inputSymbol = symbolProvider.toSymbol(inputShape) - val outputSymbol = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) + val inputShape = MiddlewareShapeUtils.inputShape(ctx.model, op) + val inputSymbol = ctx.symbolProvider.toSymbol(inputShape) + val outputSymbol = MiddlewareShapeUtils.outputSymbol(ctx.symbolProvider, ctx.model, op) val writerSymbol = ctx.service.writerSymbol var payloadShape = inputShape var keyPath = "\\.self" @@ -187,11 +184,12 @@ class OperationInputBodyMiddleware( defaultBody, ) } else { - addBodyMiddleware(writer, inputSymbol, outputSymbol, writerSymbol, rootNodeInfo, payloadWritingClosure) + addBodyMiddleware(ctx, writer, inputSymbol, outputSymbol, writerSymbol, rootNodeInfo, payloadWritingClosure) } } private fun addBodyMiddleware( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, inputSymbol: Symbol, outputSymbol: Symbol, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt new file mode 100644 index 000000000..ff17f11c4 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/SerdeUtils.kt @@ -0,0 +1,13 @@ +package software.amazon.smithy.swift.codegen.integration.serde + +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator + +class SerdeUtils { + companion object { + fun useSchemaBased(ctx: ProtocolGenerator.GenerationContext) = + // This fun is temporary; it will be eliminated when all services/protocols are moved to schema-based + // Right now this function always returns false. Will return true for certain protocols as they + // are implemented. + false + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt deleted file mode 100644 index dd666794b..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt +++ /dev/null @@ -1,77 +0,0 @@ -package software.amazon.smithy.swift.codegen.integration.serde.schema - -import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes -import kotlin.jvm.optionals.getOrNull - -class SchemaGenerator( - val ctx: ProtocolGenerator.GenerationContext, - val writer: SwiftWriter, -) { - fun renderSchema(shape: Shape) { - writer.openBlock( - "var \$L: \$N {", - "}", - shape.schemaVar(writer), - SmithyTypes.Schema, - ) { - renderSchemaStruct(shape) - writer.unwrite(",\n") - writer.write("") - } - } - - private fun renderSchemaStruct( - shape: Shape, - index: Int? = null, - ) { - writer.openBlock(".init(", "),") { - writer.write( - "id: \$L,", - shapeID(shape.id), - ) - writer.write("type: .\$L,", shape.type) - val relevantTraits = shape.allTraits.filter { permittedTraitIDs.contains(it.key.toString()) } - if (relevantTraits.isNotEmpty()) { - writer.openBlock("traits: [", "],") { - relevantTraits.forEach { trait -> - writer.write( - "\$L: \$L,", - shapeID(trait.key), - trait.value.toNode().toSwiftNode(writer), - ) - } - } - } - if (shape.members().isNotEmpty()) { - writer.openBlock("members: [", "],") { - shape.members().withIndex().forEach { renderSchemaStruct(it.value, it.index) } - } - } - targetShape(shape)?.let { - writer.write("target: \$L,", it.schemaVar(writer)) - } - index?.let { - writer.write("index: \$L,", it) - } - writer.unwrite(",\n") - writer.write("") - } - } - - private fun shapeID(id: ShapeId): String = - writer.format( - ".init(\$S, \$S\$L)", - id.namespace, - id.name, - id.member.getOrNull()?.let { writer.format(", \$S", it) } ?: "", - ) - - private fun targetShape(shape: Shape): Shape? = memberShape(shape)?.let { ctx.model.expectShape(it.target) } - - private fun memberShape(shape: Shape): MemberShape? = shape.asMemberShape().getOrNull() -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt deleted file mode 100644 index 74af64df7..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt +++ /dev/null @@ -1,48 +0,0 @@ -package software.amazon.smithy.swift.codegen.integration.serde.schema - -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes -import kotlin.jvm.optionals.getOrNull - -fun Shape.schemaVar(writer: SwiftWriter): String = - if (this.id.namespace == "smithy.api") { - this.id.preludeSchemaVarName(writer) - } else { - this.id.schemaVarName() - } - -private fun ShapeId.preludeSchemaVarName(writer: SwiftWriter): String { - val propertyName = - when (this.name) { - "Unit" -> "unitSchema" - "String" -> "stringSchema" - "Blob" -> "blobSchema" - "Integer" -> "integerSchema" - "Timestamp" -> "timestampSchema" - "Boolean" -> "booleanSchema" - "Float" -> "floatSchema" - "Double" -> "doubleSchema" - "Long" -> "longSchema" - "Short" -> "shortSchema" - "Byte" -> "byteSchema" - "PrimitiveInteger" -> "primitiveIntegerSchema" - "PrimitiveBoolean" -> "primitiveBooleanSchema" - "PrimitiveFloat" -> "primitiveFloatSchema" - "PrimitiveDouble" -> "primitiveDoubleSchema" - "PrimitiveLong" -> "primitiveLongSchema" - "PrimitiveShort" -> "primitiveShortSchema" - "PrimitiveByte" -> "primitiveByteSchema" - "Document" -> "documentSchema" - else -> throw Exception("Unhandled prelude type converted to schemaVar: ${this.name}") - } - return writer.format("\$N.\$L", SmithyTypes.Prelude, propertyName) -} - -private fun ShapeId.schemaVarName(): String { - assert(this.member.getOrNull() == null) - val namespacePortion = this.namespace.replace(".", "_") - val namePortion = this.name - return "schema__${namespacePortion}__$namePortion" -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt deleted file mode 100644 index 91a446af6..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt +++ /dev/null @@ -1,12 +0,0 @@ -package software.amazon.smithy.swift.codegen.integration.serde.schema - -val permittedTraitIDs: Set = - setOf( - "smithy.api#sparse", - "smithy.api#enumValue", - "smithy.api#jsonName", - "smithy.api#required", - "smithy.api#default", - "smithy.api#timestampFormat", - "smithy.api#httpPayload", - ) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SwiftNodeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SwiftNodeUtils.kt deleted file mode 100644 index b76c3624d..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SwiftNodeUtils.kt +++ /dev/null @@ -1,44 +0,0 @@ -package software.amazon.smithy.swift.codegen.integration.serde.schema - -import software.amazon.smithy.model.node.ArrayNode -import software.amazon.smithy.model.node.BooleanNode -import software.amazon.smithy.model.node.Node -import software.amazon.smithy.model.node.NullNode -import software.amazon.smithy.model.node.NumberNode -import software.amazon.smithy.model.node.ObjectNode -import software.amazon.smithy.model.node.StringNode -import software.amazon.smithy.swift.codegen.SwiftWriter - -fun Node.toSwiftNode(writer: SwiftWriter): String = - when (this) { - is ObjectNode -> { - if (members.isEmpty()) { - writer.format("[:]") - } else { - val contents = - members.map { - writer.format("\$S:\$L", it.key, it.value.toSwiftNode(writer)) - } - writer.format("[\$L]", contents.joinToString(",")) - } - } - is ArrayNode -> { - val contents = elements.map { it.toSwiftNode(writer) } - writer.format("[\$L]", contents.joinToString(",")) - } - is StringNode -> { - writer.format("\$S", value) - } - is NumberNode -> { - writer.format("\$L", value) - } - is BooleanNode -> { - writer.format("\$L", value) - } - is NullNode -> { - writer.format("nil") - } - else -> { - throw Exception("Unknown node type") - } - } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt index 5057ffb56..f403c9ece 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt @@ -46,7 +46,7 @@ class RpcV2CborProtocolGenerator( super.addProtocolSpecificMiddleware(ctx, operation) operationMiddleware.removeMiddleware(operation, "OperationInputBodyMiddleware") - operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider, true)) + operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx, true)) val hasEventStreamResponse = ctx.model.expectShape(operation.outputShape).hasTrait() val hasEventStreamRequest = ctx.model.expectShape(operation.inputShape).hasTrait() @@ -82,7 +82,10 @@ class RpcV2CborProtocolGenerator( operationMiddleware.appendMiddleware(operation, CborValidateResponseHeaderMiddleware()) if (operation.hasHttpBody(ctx)) { - operationMiddleware.appendMiddleware(operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, contentTypeValue, true)) + operationMiddleware.appendMiddleware( + operation, + ContentTypeMiddleware(ctx.model, ctx.symbolProvider, contentTypeValue, true), + ) } // Only set Content-Length header if the request input shape doesn't have an event stream diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt deleted file mode 100644 index 240b99803..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt +++ /dev/null @@ -1,17 +0,0 @@ -package software.amazon.smithy.swift.codegen.utils - -import software.amazon.smithy.swift.codegen.SwiftSettings - -class SchemaFileUtils { - companion object { - fun filename( - settings: SwiftSettings, - filename: String, - ): String = - if (settings.mergeModels) { - "Sources/${settings.moduleName}/Schemas.swift" - } else { - "Sources/${settings.moduleName}/schemas/$filename.swift" - } - } -} From 52a642c5104aacfab4ee17acaf7971bd3e64d005 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 25 Feb 2026 23:05:32 -0600 Subject: [PATCH 2/4] Fix SmithyCodegenCoreTests --- .../SmithyCodegenCoreTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift index 131e07c78..3c9ba9ed5 100644 --- a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift +++ b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift @@ -22,12 +22,18 @@ class SmithyCodegenCoreTests: XCTestCase { // can debug a code generation step that is failing during builds. func test_generates_rpcv2_cbor_protocol() throws { let tempDirURL = FileManager.default.temporaryDirectory - let generator = try CodeGenerator( + let settings = try SwiftSettings( service: "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", + sdkId: "RpcV2Protocol" + ) + let generator = try CodeGenerator( + settings: settings, modelFileURL: Bundle.module.url(forResource: "smithy-rpcv2-cbor", withExtension: "json")!, schemasFileURL: tempDirURL.appendingPathComponent("Schemas.swift"), serializeFileURL: tempDirURL.appendingPathComponent("Serialize.swift"), - deserializeFileURL: tempDirURL.appendingPathComponent("Deserialize.swift") + deserializeFileURL: tempDirURL.appendingPathComponent("Deserialize.swift"), + typeRegistryFileURL: tempDirURL.appendingPathComponent("TypeRegistry.swift"), + operationsFileURL: tempDirURL.appendingPathComponent("Operations.swift") ) try generator.run() } From 3eb8e8208b30c4f6f2c43ff55ca43de9e894c526 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Thu, 26 Feb 2026 12:53:07 -0600 Subject: [PATCH 3/4] Add support for service closure renames --- Sources/SmithyCodegenCore/Model/Model+AST.swift | 5 ++++- .../ModelTransformer/Model+TrimReferences.swift | 3 ++- .../SmithyCodegenCore/Serialize/SerializeCodegen.swift | 6 ++++++ Sources/SmithyCodegenCore/Shape/ServiceShape.swift | 5 ++++- .../SmithyCodegenCore/SymbolProvider/SymbolProvider.swift | 8 ++++---- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift index ebe5a8c01..4e7d86e90 100644 --- a/Sources/SmithyCodegenCore/Model/Model+AST.swift +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -105,7 +105,10 @@ extension Model { traits: traits, operationIDs: try astShape.operations?.map { try $0.id } ?? [], resourceIDs: try astShape.resources?.map { try $0.id } ?? [], - errorIDs: try astShape.errors?.map { try $0.id } ?? [] + errorIDs: try astShape.errors?.map { try $0.id } ?? [], + renames: Dictionary(uniqueKeysWithValues: try astShape.rename?.map { + (try ShapeID($0.key), $0.value) + } ?? []) ) return (shapeID, shape) case .resource: diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift index 52e80dd53..3cc18aa9d 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift @@ -70,7 +70,8 @@ extension Model { traits: serviceShape.traits, operationIDs: operationIDs, resourceIDs: resourceIDs, - errorIDs: errorIDs + errorIDs: errorIDs, + renames: serviceShape.renames ) case let resourceShape as ResourceShape: let operationIDs = resourceShape.operationIDs.filter { trimmedShapes[$0] != nil } diff --git a/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift b/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift index 374ddf960..9db5be166 100644 --- a/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift +++ b/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift @@ -44,6 +44,12 @@ package struct SerializeCodegen { try writer.openBlock("{ memberSchema, \(varName), serializer in", "}") { writer in writer.write("switch memberSchema.index {") for (index, member) in try members(of: shape).enumerated() { + + // Event stream errors don't have a case in the Swift union, so don't try to + // serialize the error member + if try shape.hasTrait(StreamingTrait.self) && member.target.hasTrait(ErrorTrait.self) { + continue + } writer.write("case \(index):") writer.indent() if shape.type == .structure { diff --git a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift index 7a6b4fdc5..ef1f1f483 100644 --- a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift @@ -15,17 +15,20 @@ public class ServiceShape: Shape { let operationIDs: [ShapeID] let resourceIDs: [ShapeID] let errorIDs: [ShapeID] + let renames: [ShapeID: String] public init( id: ShapeID, traits: TraitCollection, operationIDs: [ShapeID], resourceIDs: [ShapeID], - errorIDs: [ShapeID] + errorIDs: [ShapeID], + renames: [ShapeID: String] ) { self.operationIDs = operationIDs self.resourceIDs = resourceIDs self.errorIDs = errorIDs + self.renames = renames super.init(id: id, type: .service, traits: traits) } diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift index 6d2c53d86..dbf1cc06f 100644 --- a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -26,15 +26,15 @@ public struct SymbolProvider { public func swiftType(shape: Shape) throws -> String { switch shape.type { case .structure, .union, .enum, .intEnum: - let base = shape.id.name + let baseName = (service.renames[shape.id] ?? shape.id.name).capitalized.escapingReservedWords if shape.isTopLevel { - return base.capitalized.escapingReservedWords + return baseName } else if shape.type == .intEnum { // The NestedShapeTransformer in main codegen inadvertently excludes intEnum // so it is not namespaced here. All other shape types are in the namespace. - return base.capitalized.escapingReservedWords + return baseName } else { - return try "\(modelNamespace).\(base.capitalized.escapingReservedWords)" + return try "\(modelNamespace).\(baseName)" } case .list, .set: guard let listShape = shape as? ListShape else { From 2c80da4df23b015cb6ac30a1c5107d441046231e Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Thu, 26 Feb 2026 13:27:36 -0600 Subject: [PATCH 4/4] Abolish CI timeout --- .github/workflows/continuous-integration.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 015b69d6e..0e6f8db59 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -66,7 +66,6 @@ jobs: - name: Build & Run smithy-swift Kotlin Unit Tests run: ./gradlew build - name: Build & Run smithy-swift Swift Unit Tests - timeout-minutes: 15 run: | set -o pipefail && \ NSUnbufferedIO=YES xcodebuild \