Skip to content

feat: Add operation and type registry codegen#1033

Open
jbelkins wants to merge 4 commits intoepic/sbsfrom
jbe/operations_and_type_registry
Open

feat: Add operation and type registry codegen#1033
jbelkins wants to merge 4 commits intoepic/sbsfrom
jbe/operations_and_type_registry

Conversation

@jbelkins
Copy link
Contributor

@jbelkins jbelkins commented Feb 26, 2026

Description of changes

Primary changes:

  • Add an Operation type, and code generation for a service's operations
  • Add a type registry type, and code generation for a service's error types
  • Adds a ClientProtocol type, which will be used by the Orchestrator to serialize & deserialize requests & responses.

Also:

  • Remove code from the Smithy-based code generator that was previously used to generate schemas.
  • Added a model transformer to generate clients with only specific operations. Used to support internal service clients.
  • Change smithy-model-info.json to swift-settings.json, which includes the same params as the Smithy-based code generator.
  • Add support for service closure renames (used in RestJson1 protocol tests).

Scope

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

) {
writer.apply {
val paramsType = ctx.settings.sdkId.clientName() + "AuthSchemeResolverParameters"
val paramsType = ctx.settings.clientBaseNamePreservingService + "AuthSchemeResolverParameters"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ensure the endpoint params name used currently is preserved after the previous sdkId refactor.

httpBindingResolver: HttpBindingResolver,
defaultTimestampFormat: TimestampFormatTrait.Format,
) {
if (SerdeUtils.useSchemaBased(ctx)) return
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't generate any of the Codable, URLPathProvider, QueryStringProvider, etc. extensions if this is a schema-based service.

class OperationInputBodyMiddleware(
val model: Model,
val symbolProvider: SymbolProvider,
val ctx: ProtocolGenerator.GenerationContext,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simplified interface by taking ctx instead of separate model & symbol provider.

ctx: ProtocolGenerator.GenerationContext,
writer: SwiftWriter,
inputSymbol: Symbol,
outputSymbol: Symbol,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 4 files below were used to generate schemas using the Smithy-based code generator. Since those will be generated by the Swift-native code generator instead, they are deleted here.

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" }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The function above was moved to SerdeUtils.

The functions below (generateSchemas() and various helpers) are no longer needed since schemas are generated in the native-Swift code generator.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previously used in the generation of schemas.

LOGGER.info("[${service.id}] Generating Smithy model file info")
SmithyModelFileInfoGenerator(ctx).writeSmithyModelFileInfo()
LOGGER.info("[${service.id}] Generating swift-settings.json")
SwiftSettingsJSONGenerator(ctx).render()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed smithy-model-info.json to swift-settings.json and aligned its contents with this code generator's SwiftSettings type.

@@ -77,6 +59,24 @@ class SwiftSettings(
val modelPath: String,
) {
companion object {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The strings below have been exposed so the swift-settings.json writer can access them.


class SwiftSettingsJSONGenerator(
val ctx: ProtocolGenerator.GenerationContext,
) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using Smithy's Node type to convert to JSON, this file writes the swift-settings.json file for the service.


public static var unitSchema: Schema {
Schema(id: .init("smithy.api", "Unit"), type: .structure)
Schema(id: .init("smithy.api", "Unit"), type: .structure, traits: [UnitTypeTrait()])
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add the UnitTypeTrait to the smithy.api#Unit schema.

SensitiveTrait.id,
SparseTrait.id,
TimestampFormatTrait.id,
UnitTypeTrait.id, // UnitTypeTrait will only ever appear in Prelude.unitSchema
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Above, new trait types are added to the runtime "allow list".

import ArgumentParser
import Foundation
import struct SmithyCodegenCore.CodeGenerator
import struct SmithyCodegenCore.SwiftSettings
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This file adds new params to support various new code generator features, plus the operations & type registry Swift files.

/// 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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this file, we add support for the rename field on Service, and the collectionOperationIDs and resourceIDs fields on Resource, which are needed to support all AWS services & protocol tests.

@@ -34,98 +34,7 @@ extension Model {
return sinceDate > cutoff
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code immediately below was moved to its own source file in Sources/SmithyCodegenCore/ModelTransformer/Model+TrimReferences.swift.

// SPDX-License-Identifier: Apache-2.0
//

extension Model {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This model transformer removes unspecified operations from the service. If no operations are specified, all operations are preserved.

This is used to support internal service clients.

// SPDX-License-Identifier: Apache-2.0
//

extension Model {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was previously a helper function in the Model+Deprecated transform, but has been separated out into its own reusable transform.

It's used to "clean up" a model after it has had shapes removed, by also removing any references or shapes that depended on the removed shapes.

// SPDX-License-Identifier: Apache-2.0
//

struct OperationsCodegen {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here, we code-generate a service's operations as a set of internal-scoped static variables namespaced under the client.

// serialize the error member
if try shape.hasTrait(StreamingTrait.self) && member.target.hasTrait(ErrorTrait.self) {
continue
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The above logic fixes an issue when serializing an event stream that contains error types.

@@ -12,6 +12,8 @@ import struct Smithy.TraitCollection
/// A ``Shape`` subclass specialized for Smithy resources.
public class ResourceShape: Shape {
let operationIDs: [ShapeID]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here & below, added support for the collectionOperations and resources fields, which are used by some AWS services.

let operationIDs: [ShapeID]
let resourceIDs: [ShapeID]
let errorIDs: [ShapeID]
let renames: [ShapeID: String]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added support here for service-closure renames, which are used by RestJson1 protocol tests.

.replacingOccurrences(of: "Service", with: "")
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logic above was incorrect, and has been moved (with corrections) to SwiftSettings.

switch shape.type {
case .structure, .union, .enum, .intEnum:
let base = shape.id.name
let baseName = (service.renames[shape.id] ?? shape.id.name).capitalized.escapingReservedWords
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we check if a service-closure rename has been provided for this type, and if so, we use that as the base for the type name.

throw SymbolProviderError("Shape has type .service but is not a ServiceShape")
}
return try "\(serviceShape.clientBaseName)Client"
return "\(settings.serviceName)Client"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Service name is now provided by SwiftSettings.


import struct Smithy.ErrorTrait

struct TypeRegistryCodegen {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The (very simple!) code below generates a type registry containing all of this service's errors. Used for deserializing errored responses.


// Perform model transformations here
let finalModel = try model
.withOperations(settings: settings)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this model transform to support internal clients.


import struct Smithy.ShapeID

public struct SwiftSettings: Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This type captures the values encoded in the swift-settings.json file.

import protocol Smithy.ResponseMessage
import struct Smithy.ShapeID

public protocol ClientProtocol<RequestType, ResponseType>: Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Each AWS protocol (and RPCv2CBOR) will provide an implementation of this protocol, which will be used by the Orchestrator to perform serde.


import struct Smithy.Schema

public struct Operation<Input: SerializableStruct, Output: DeserializableStruct> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Operation structure contains references to input/output types, schemas, and the error type registry for a given operation.

An instance of this type is code-generated for every operation, and is used during request/response processing.


import struct Smithy.Schema

public protocol OperationProperties: Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This protocol allows type-erased access to the schemas & type registry in an Operation.

@@ -96,36 +97,36 @@ public extension ShapeSerializer {
func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws {
switch value.type {
case .blob:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Per the Smithy spec, documents & their components should be serialized & deserialized using prelude schemas.

The appropriate schemas are substituted below.

import struct Smithy.Schema
import struct Smithy.ShapeID

public struct TypeRegistry: Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The TypeRegistry allows for a Shape ID or other schema property to be used to lookup the corresponding Swift type.

For now this is used for error deserialization, but will be used more broadly later for future Smithy features.


import struct Foundation.Data

public protocol Codec: Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This protocol is used for obtaining the correct ShapeSerializer or ShapeDeserializer for a given protocol.

deserializeFileURL: tempDirURL.appendingPathComponent("Deserialize.swift")
deserializeFileURL: tempDirURL.appendingPathComponent("Deserialize.swift"),
typeRegistryFileURL: tempDirURL.appendingPathComponent("TypeRegistry.swift"),
operationsFileURL: tempDirURL.appendingPathComponent("Operations.swift")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update this test for new Swift generated files, and to use Swift settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant