From 3e4f83338e58b2190dbf1e7698a1817097c6bafc Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 4 Nov 2025 15:23:33 -0600 Subject: [PATCH 01/29] Disable input/output serializers --- Sources/Smithy/Schema/Node.swift | 109 ++++++++++++++++++ Sources/Smithy/Schema/Prelude.swift | 85 ++++++++++++++ Sources/Smithy/Schema/Schema.swift | 34 ++++++ .../{Document => Schema}/ShapeType.swift | 0 .../swift/codegen/DirectedSwiftCodegen.kt | 1 + .../HTTPBindingProtocolGenerator.kt | 105 ++++++++++++++++- .../codegen/integration/ProtocolGenerator.kt | 2 + .../serde/schema/SchemaGenerator.kt | 76 ++++++++++++ .../serde/schema/SchemaShapeUtils.kt | 45 ++++++++ .../serde/schema/SwiftNodeUtils.kt | 44 +++++++ .../swiftmodules/SmithyReadWriteTypes.kt | 5 + .../swift/codegen/swiftmodules/SmithyTypes.kt | 30 ++++- .../swift/codegen/swiftmodules/SwiftTypes.kt | 1 + .../swift/codegen/utils/SchemaFileUtils.kt | 17 +++ 14 files changed, 548 insertions(+), 6 deletions(-) create mode 100644 Sources/Smithy/Schema/Node.swift create mode 100644 Sources/Smithy/Schema/Prelude.swift create mode 100644 Sources/Smithy/Schema/Schema.swift rename Sources/Smithy/{Document => Schema}/ShapeType.swift (100%) create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SwiftNodeUtils.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt diff --git a/Sources/Smithy/Schema/Node.swift b/Sources/Smithy/Schema/Node.swift new file mode 100644 index 000000000..55f2c8618 --- /dev/null +++ b/Sources/Smithy/Schema/Node.swift @@ -0,0 +1,109 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Contains the value of a Smithy Node. +/// +/// Smithy node data is basically the same as the data that can be stored in JSON. +/// The root of a Smithy node may be of any case, i.e. unlike JSON, the root element is not limited to object or list. +/// +/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values +public enum Node: Sendable { + case object([String: Node]) + case list([Node]) + case string(String) + case number(Double) + case boolean(Bool) + case null +} + +public extension Node { + + /// Returns the object dictionary if this Node is `.object`, else returns `nil`. + var object: [String: Node]? { + guard case .object(let value) = self else { return nil } + return value + } + + /// Returns the array of `Node` if this node is `.list`, else returns `nil`. + var list: [Node]? { + guard case .list(let value) = self else { return nil } + return value + } + + /// Returns the string if this node is `.string`, else returns `nil`. + var string: String? { + guard case .string(let value) = self else { return nil } + return value + } + + /// Returns the Double if this node is `.number`, else returns `nil`. + var number: Double? { + guard case .number(let value) = self else { return nil } + return value + } + + /// Returns the `Bool` value if this node is `.boolean`, else returns `nil`. + var boolean: Bool? { + guard case .boolean(let value) = self else { return nil } + return value + } + + /// Returns `true` if this node is `.null`, else returns `false`. + var null: Bool { + guard case .null = self else { return false } + return true + } +} + +extension Node: ExpressibleByDictionaryLiteral { + + public init(dictionaryLiteral elements: (String, Node)...) { + self = .object(Dictionary(uniqueKeysWithValues: elements)) + } +} + +extension Node: ExpressibleByArrayLiteral { + + public init(arrayLiteral elements: Node...) { + self = .list(elements) + } +} + +extension Node: ExpressibleByStringLiteral { + + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension Node: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: IntegerLiteralType) { + self = .number(Double(value)) + } +} + +extension Node: ExpressibleByFloatLiteral { + + public init(floatLiteral value: FloatLiteralType) { + self = .number(Double(value)) + } +} + +extension Node: ExpressibleByBooleanLiteral { + + public init(booleanLiteral value: BooleanLiteralType) { + self = .boolean(value) + } +} + +extension Node: ExpressibleByNilLiteral { + + public init(nilLiteral: ()) { + self = .null + } +} diff --git a/Sources/Smithy/Schema/Prelude.swift b/Sources/Smithy/Schema/Prelude.swift new file mode 100644 index 000000000..79775e127 --- /dev/null +++ b/Sources/Smithy/Schema/Prelude.swift @@ -0,0 +1,85 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Below are schemas for all model shapes defined in the Smithy 2.0 prelude. +// Schemas for custom Smithy types may use these schemas in their definitions. + +public var unitSchema: Schema { + Schema(id: "smithy.api#Unit", type: .structure) +} + +public var booleanSchema: Schema { + Schema(id: "smithy.api#Boolean", type: .boolean) +} + +public var stringSchema: Schema { + Schema(id: "smithy.api#String", type: .string) +} + +public var integerSchema: Schema { + Schema(id: "smithy.api#Integer", type: .integer) +} + +public var blobSchema: Schema { + Schema(id: "smithy.api#Blob", type: .blob) +} + +public var timestampSchema: Schema { + Schema(id: "smithy.api#Timestamp", type: .timestamp) +} + +public var byteSchema: Schema { + Schema(id: "smithy.api#Byte", type: .byte) +} + +public var shortSchema: Schema { + Schema(id: "smithy.api#Short", type: .short) +} + +public var longSchema: Schema { + Schema(id: "smithy.api#Long", type: .long) +} + +public var floatSchema: Schema { + Schema(id: "smithy.api#Float", type: .float) +} + +public var doubleSchema: Schema { + Schema(id: "smithy.api#Double", type: .double) +} + +public var documentSchema: Schema { + Schema(id: "smithy.api#PrimitiveDocument", type: .document) +} + +public var primitiveBooleanSchema: Schema { + Schema(id: "smithy.api#PrimitiveBoolean", type: .boolean, traits: ["smithy.api#default": false]) +} + +public var primitiveIntegerSchema: Schema { + Schema(id: "smithy.api#PrimitiveInteger", type: .integer, traits: ["smithy.api#default": 0]) +} + +public var primitiveByteSchema: Schema { + Schema(id: "smithy.api#PrimitiveByte", type: .byte, traits: ["smithy.api#default": 0]) +} + +public var primitiveShortSchema: Schema { + Schema(id: "smithy.api#PrimitiveShort", type: .short, traits: ["smithy.api#default": 0]) +} + +public var primitiveLongSchema: Schema { + Schema(id: "smithy.api#PrimitiveLong", type: .long, traits: ["smithy.api#default": 0]) +} + +public var primitiveFloatSchema: Schema { + Schema(id: "smithy.api#PrimitiveFloat", type: .float, traits: ["smithy.api#default": 0]) +} + +public var primitiveDoubleSchema: Schema { + Schema(id: "smithy.api#PrimitiveDouble", type: .double, traits: ["smithy.api#default": 0]) +} diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift new file mode 100644 index 000000000..14644b702 --- /dev/null +++ b/Sources/Smithy/Schema/Schema.swift @@ -0,0 +1,34 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public class Schema { + public let id: String + public let type: ShapeType + public let traits: [String: Node] + public let members: [Schema] + public let memberName: String? + public let target: Schema? + public let index: Int + + public init( + id: String, + type: ShapeType, + traits: [String: Node] = [:], + members: [Schema] = [], + memberName: String? = nil, + target: Schema? = nil, + index: Int = -1 + ) { + self.id = id + self.type = type + self.traits = traits + self.members = members + self.memberName = memberName + self.target = target + self.index = index + } +} diff --git a/Sources/Smithy/Document/ShapeType.swift b/Sources/Smithy/Schema/ShapeType.swift similarity index 100% rename from Sources/Smithy/Document/ShapeType.swift rename to Sources/Smithy/Schema/ShapeType.swift 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 19f04bf0c..4123503ba 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 @@ -81,6 +81,7 @@ class DirectedSwiftCodegen( generateMessageMarshallable(ctx) generateMessageUnmarshallable(ctx) generateCodableConformanceForNestedTypes(ctx) + generateSchemas(ctx) initializeMiddleware(ctx) 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 3b277f731..001aa70b9 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,6 +18,7 @@ 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,6 +59,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.struct.StructDecodeGenerator import software.amazon.smithy.swift.codegen.integration.serde.struct.StructEncodeGenerator import software.amazon.smithy.swift.codegen.integration.serde.union.UnionDecodeGenerator @@ -72,6 +74,7 @@ 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 @@ -139,6 +142,7 @@ abstract class HTTPBindingProtocolGenerator( override var serviceErrorProtocolSymbol: Symbol = ClientRuntimeTypes.Http.HttpError override fun generateSerializers(ctx: ProtocolGenerator.GenerationContext) { + if (usesSchemaBasedSerialization(ctx)) { return } // temporary condition // render conformance to HttpRequestBinding for all input shapes val inputShapesWithHttpBindings: MutableSet = mutableSetOf() for (operation in getHttpBindingOperations(ctx)) { @@ -201,10 +205,40 @@ 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" || it.name == "awsJson1_0" || it.name == "awsJson1_1" } + + 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()) { return } @@ -250,11 +284,11 @@ abstract class HTTPBindingProtocolGenerator( } private fun resolveInputShapes(ctx: ProtocolGenerator.GenerationContext): Map> { - var shapesInfo: MutableMap> = mutableMapOf() + val shapesInfo: MutableMap> = mutableMapOf() val operations = getHttpBindingOperations(ctx) for (operation in operations) { val inputType = ctx.model.expectShape(operation.input.get()) - var metadata = + val metadata = mapOf( Pair(ShapeMetadata.OPERATION_SHAPE, operation), Pair(ShapeMetadata.SERVICE_VERSION, ctx.service.version), @@ -363,6 +397,73 @@ 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 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 fa558d524..bfcf3479b 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,6 +122,8 @@ 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/serde/schema/SchemaGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt new file mode 100644 index 000000000..0f1b98533 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt @@ -0,0 +1,76 @@ +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.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: \$S,", shape.id.toString()) + 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( + "\$S: \$L,", + trait.key.toString(), + trait.value.toNode().toSwiftNode(writer), + ) + } + writer.unwrite(",\n") + writer.write("") + } + } + if (shape.members().isNotEmpty()) { + writer.openBlock("members: [", "],") { + shape.members().withIndex().forEach { renderSchemaStruct(it.value, it.index) } + writer.unwrite(",\n") + writer.write("") + } + } + shape.id.member + .getOrNull() + ?.let { writer.write("memberName: \$S,", it) } + targetShape(shape)?.let { writer.write("target: \$L,", it.schemaVar(writer)) } + index?.let { writer.write("index: \$L,", it) } + writer.unwrite(",\n") + writer.write("") + } + } + + private fun targetShape(shape: Shape): Shape? = memberShape(shape)?.let { ctx.model.expectShape(it.target) } + + private fun memberShape(shape: Shape): MemberShape? = shape.asMemberShape().getOrNull() +} + +private 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/SchemaShapeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt new file mode 100644 index 000000000..104f5a0f5 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt @@ -0,0 +1,45 @@ +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 = + when (this.name) { + "Unit" -> writer.format("\$N", SmithyTypes.unitSchema) + "String" -> writer.format("\$N", SmithyTypes.stringSchema) + "Blob" -> writer.format("\$N", SmithyTypes.blobSchema) + "Integer" -> writer.format("\$N", SmithyTypes.integerSchema) + "Timestamp" -> writer.format("\$N", SmithyTypes.timestampSchema) + "Boolean" -> writer.format("\$N", SmithyTypes.booleanSchema) + "Float" -> writer.format("\$N", SmithyTypes.floatSchema) + "Double" -> writer.format("\$N", SmithyTypes.doubleSchema) + "Long" -> writer.format("\$N", SmithyTypes.longSchema) + "Short" -> writer.format("\$N", SmithyTypes.shortSchema) + "Byte" -> writer.format("\$N", SmithyTypes.byteSchema) + "PrimitiveInteger" -> writer.format("\$N", SmithyTypes.primitiveIntegerSchema) + "PrimitiveBoolean" -> writer.format("\$N", SmithyTypes.primitiveBooleanSchema) + "PrimitiveFloat" -> writer.format("\$N", SmithyTypes.primitiveFloatSchema) + "PrimitiveDouble" -> writer.format("\$N", SmithyTypes.primitiveDoubleSchema) + "PrimitiveLong" -> writer.format("\$N", SmithyTypes.primitiveLongSchema) + "PrimitiveShort" -> writer.format("\$N", SmithyTypes.primitiveShortSchema) + "PrimitiveByte" -> writer.format("\$N", SmithyTypes.primitiveByteSchema) + "Document" -> writer.format("\$N", SmithyTypes.documentSchema) + else -> throw Exception("Unhandled prelude type converted to schemaVar: ${this.name}") + } + +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/SwiftNodeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SwiftNodeUtils.kt new file mode 100644 index 000000000..b76c3624d --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SwiftNodeUtils.kt @@ -0,0 +1,44 @@ +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/swiftmodules/SmithyReadWriteTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt index 070e26034..d383db5a1 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt @@ -27,6 +27,11 @@ object SmithyReadWriteTypes { val WritingClosures = runtimeSymbol("WritingClosures", SwiftDeclaration.ENUM) val ReadingClosureBox = runtimeSymbol("ReadingClosureBox", SwiftDeclaration.STRUCT) val WritingClosureBox = runtimeSymbol("WritingClosureBox", SwiftDeclaration.STRUCT) + val ShapeSerializer = runtimeSymbol("ShapeSerializer", SwiftDeclaration.PROTOCOL) + val ShapeDeserializer = runtimeSymbol("ShapeDeserializer", SwiftDeclaration.PROTOCOL) + val SerializableStruct = runtimeSymbol("SerializableStruct", SwiftDeclaration.PROTOCOL) + val DeserializableStruct = runtimeSymbol("DeserializableStruct", SwiftDeclaration.PROTOCOL) + val Unit = runtimeSymbol("Unit", SwiftDeclaration.STRUCT) } private fun runtimeSymbol( diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt index 4e8bd7caf..d9641d472 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt @@ -21,16 +21,38 @@ object SmithyTypes { val LogAgent = runtimeSymbol("LogAgent", SwiftDeclaration.PROTOCOL) val RequestMessageSerializer = runtimeSymbol("RequestMessageSerializer", SwiftDeclaration.PROTOCOL) val URIQueryItem = runtimeSymbol("URIQueryItem", SwiftDeclaration.STRUCT) + val Schema = runtimeSymbol("Schema", SwiftDeclaration.CLASS) + val unitSchema = runtimeSymbol("unitSchema", SwiftDeclaration.VAR) + val stringSchema = runtimeSymbol("stringSchema", SwiftDeclaration.VAR) + val blobSchema = runtimeSymbol("blobSchema", SwiftDeclaration.VAR) + val integerSchema = runtimeSymbol("integerSchema", SwiftDeclaration.VAR) + val timestampSchema = runtimeSymbol("timestampSchema", SwiftDeclaration.VAR) + val booleanSchema = runtimeSymbol("booleanSchema", SwiftDeclaration.VAR) + val floatSchema = runtimeSymbol("floatSchema", SwiftDeclaration.VAR) + val doubleSchema = runtimeSymbol("doubleSchema", SwiftDeclaration.VAR) + val longSchema = runtimeSymbol("longSchema", SwiftDeclaration.VAR) + val shortSchema = runtimeSymbol("shortSchema", SwiftDeclaration.VAR) + val byteSchema = runtimeSymbol("byteSchema", SwiftDeclaration.VAR) + val primitiveBooleanSchema = runtimeSymbol("primitiveBooleanSchema", SwiftDeclaration.VAR) + val primitiveFloatSchema = runtimeSymbol("primitiveFloatSchema", SwiftDeclaration.VAR) + val primitiveDoubleSchema = runtimeSymbol("primitiveDoubleSchema", SwiftDeclaration.VAR) + val primitiveLongSchema = runtimeSymbol("primitiveLongSchema", SwiftDeclaration.VAR) + val primitiveIntegerSchema = runtimeSymbol("primitiveIntegerSchema", SwiftDeclaration.VAR) + val primitiveShortSchema = runtimeSymbol("primitiveShortSchema", SwiftDeclaration.VAR) + val primitiveByteSchema = runtimeSymbol("primitiveByteSchema", SwiftDeclaration.VAR) + val documentSchema = runtimeSymbol("documentSchema", SwiftDeclaration.VAR) } private fun runtimeSymbol( name: String, - declaration: SwiftDeclaration? = null, + declaration: SwiftDeclaration?, + additionalImports: List = emptyList(), + spiName: List = emptyList(), ): Symbol = SwiftSymbol.make( name, declaration, - SwiftDependency.SMITHY, - emptyList(), - emptyList(), + SwiftDependency.SMITHY.takeIf { additionalImports.isEmpty() }, + additionalImports, + spiName, ) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt index 79dc669b1..24d437fe4 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.swift.codegen.SwiftDeclaration import software.amazon.smithy.swift.codegen.SwiftDependency object SwiftTypes { + val Void = builtInSymbol("Void", SwiftDeclaration.STRUCT) val StringList = SwiftSymbol.make( "[String]", 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 new file mode 100644 index 000000000..240b99803 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt @@ -0,0 +1,17 @@ +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 12155d2cbbde54c53166c365bb218051d945a29d Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 5 Nov 2025 09:21:51 -0600 Subject: [PATCH 02/29] add ShapeID, doc comments --- Package.swift | 4 + Sources/Smithy/Schema/Node.swift | 2 +- Sources/Smithy/Schema/Prelude.swift | 117 +++++++++--------- Sources/Smithy/Schema/Schema.swift | 54 ++++++-- Sources/Smithy/Schema/ShapeID.swift | 42 +++++++ Sources/Smithy/Schema/ShapeType.swift | 3 +- Tests/SmithyTests/ShapeIDTests.swift | 22 ++++ .../HTTPBindingProtocolGenerator.kt | 2 + .../serde/schema/SchemaGenerator.kt | 50 ++++---- .../serde/schema/SchemaShapeUtils.kt | 44 +++---- .../integration/serde/schema/SchemaTraits.kt | 12 ++ .../swift/codegen/swiftmodules/SmithyTypes.kt | 20 +-- 12 files changed, 240 insertions(+), 132 deletions(-) create mode 100644 Sources/Smithy/Schema/ShapeID.swift create mode 100644 Tests/SmithyTests/ShapeIDTests.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt diff --git a/Package.swift b/Package.swift index 755b40342..940b1e6bf 100644 --- a/Package.swift +++ b/Package.swift @@ -268,6 +268,10 @@ let package = Package( ], resources: [ .process("Resources") ] ), + .testTarget( + name: "SmithyTests", + dependencies: ["Smithy"] + ), .testTarget( name: "SmithyCBORTests", dependencies: ["SmithyCBOR", "ClientRuntime", "SmithyTestUtil"] diff --git a/Sources/Smithy/Schema/Node.swift b/Sources/Smithy/Schema/Node.swift index 55f2c8618..ce6c2d64e 100644 --- a/Sources/Smithy/Schema/Node.swift +++ b/Sources/Smithy/Schema/Node.swift @@ -8,7 +8,7 @@ /// Contains the value of a Smithy Node. /// /// Smithy node data is basically the same as the data that can be stored in JSON. -/// The root of a Smithy node may be of any case, i.e. unlike JSON, the root element is not limited to object or list. +/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. /// /// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values public enum Node: Sendable { diff --git a/Sources/Smithy/Schema/Prelude.swift b/Sources/Smithy/Schema/Prelude.swift index 79775e127..4658ccfc2 100644 --- a/Sources/Smithy/Schema/Prelude.swift +++ b/Sources/Smithy/Schema/Prelude.swift @@ -8,78 +8,83 @@ // Below are schemas for all model shapes defined in the Smithy 2.0 prelude. // Schemas for custom Smithy types may use these schemas in their definitions. -public var unitSchema: Schema { - Schema(id: "smithy.api#Unit", type: .structure) -} +public enum Prelude { -public var booleanSchema: Schema { - Schema(id: "smithy.api#Boolean", type: .boolean) -} + public static var unitSchema: Schema { + Schema(id: .init("smithy.api", "Unit"), type: .structure) + } -public var stringSchema: Schema { - Schema(id: "smithy.api#String", type: .string) -} + public static var booleanSchema: Schema { + Schema(id: .init("smithy.api", "Boolean"), type: .boolean) + } -public var integerSchema: Schema { - Schema(id: "smithy.api#Integer", type: .integer) -} + public static var stringSchema: Schema { + Schema(id: .init("smithy.api", "String"), type: .string) + } -public var blobSchema: Schema { - Schema(id: "smithy.api#Blob", type: .blob) -} + public static var integerSchema: Schema { + Schema(id: .init("smithy.api", "Integer"), type: .integer) + } -public var timestampSchema: Schema { - Schema(id: "smithy.api#Timestamp", type: .timestamp) -} + public static var blobSchema: Schema { + Schema(id: .init("smithy.api", "Blob"), type: .blob) + } -public var byteSchema: Schema { - Schema(id: "smithy.api#Byte", type: .byte) -} + public static var timestampSchema: Schema { + Schema(id: .init("smithy.api", "Timestamp"), type: .timestamp) + } -public var shortSchema: Schema { - Schema(id: "smithy.api#Short", type: .short) -} + public static var byteSchema: Schema { + Schema(id: .init("smithy.api", "Byte"), type: .byte) + } -public var longSchema: Schema { - Schema(id: "smithy.api#Long", type: .long) -} + public static var shortSchema: Schema { + Schema(id: .init("smithy.api", "Short"), type: .short) + } -public var floatSchema: Schema { - Schema(id: "smithy.api#Float", type: .float) -} + public static var longSchema: Schema { + Schema(id: .init("smithy.api", "Long"), type: .long) + } -public var doubleSchema: Schema { - Schema(id: "smithy.api#Double", type: .double) -} + public static var floatSchema: Schema { + Schema(id: .init("smithy.api", "Float"), type: .float) + } -public var documentSchema: Schema { - Schema(id: "smithy.api#PrimitiveDocument", type: .document) -} + public static var doubleSchema: Schema { + Schema(id: .init("smithy.api", "Double"), type: .double) + } -public var primitiveBooleanSchema: Schema { - Schema(id: "smithy.api#PrimitiveBoolean", type: .boolean, traits: ["smithy.api#default": false]) -} + public static var documentSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveDocument"), type: .document) + } -public var primitiveIntegerSchema: Schema { - Schema(id: "smithy.api#PrimitiveInteger", type: .integer, traits: ["smithy.api#default": 0]) -} + public static var primitiveBooleanSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveBoolean"), type: .boolean, traits: [defaultTraitID: false]) + } -public var primitiveByteSchema: Schema { - Schema(id: "smithy.api#PrimitiveByte", type: .byte, traits: ["smithy.api#default": 0]) -} + public static var primitiveIntegerSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveInteger"), type: .integer, traits: [defaultTraitID: 0]) + } -public var primitiveShortSchema: Schema { - Schema(id: "smithy.api#PrimitiveShort", type: .short, traits: ["smithy.api#default": 0]) -} + public static var primitiveByteSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveByte"), type: .byte, traits: [defaultTraitID: 0]) + } -public var primitiveLongSchema: Schema { - Schema(id: "smithy.api#PrimitiveLong", type: .long, traits: ["smithy.api#default": 0]) -} + public static var primitiveShortSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveShort"), type: .short, traits: [defaultTraitID: 0]) + } -public var primitiveFloatSchema: Schema { - Schema(id: "smithy.api#PrimitiveFloat", type: .float, traits: ["smithy.api#default": 0]) -} + public static var primitiveLongSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveLong"), type: .long, traits: [defaultTraitID: 0]) + } -public var primitiveDoubleSchema: Schema { - Schema(id: "smithy.api#PrimitiveDouble", type: .double, traits: ["smithy.api#default": 0]) + public static var primitiveFloatSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveFloat"), type: .float, traits: [defaultTraitID: 0]) + } + + public static var primitiveDoubleSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [defaultTraitID: 0]) + } } + +private let defaultTraitID = ShapeID("smithy.api", "default") diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift index 14644b702..ff94fd859 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -5,21 +5,50 @@ // SPDX-License-Identifier: Apache-2.0 // +/// A structure which describes selected Smithy model information for a Smithy model shape. +/// +/// Typically, the Schema contains only modeled info & properties that are relevant to +/// serialization, transport bindings, and other functions performed by the SDK. public class Schema { - public let id: String + + /// The Smithy shape ID for the shape described by this schema. + public let id: ShapeID + + /// The type of the shape being described. public let type: ShapeType - public let traits: [String: Node] + + /// A dictionary of the described shape's trait shape IDs to Nodes with trait data. + /// + /// Not all traits for a shape will be represented in the schema; + /// typically the Schema contains only the traits relevant to the client-side SDK. + public let traits: [ShapeID: Node] + + /// The member schemas for this schema, if any. + /// + /// Typically only a schema of type Structure, Union, Enum, IntEnum, List or Map will have members. public let members: [Schema] - public let memberName: String? + + /// The target schema for this schema. Will only be used when this is a member schema. public let target: Schema? - public let index: Int + /// The index of this schema, if it represents a Smithy member. + /// + /// For a member schema, index will be set to its index in the members array. + /// For other types of schema, index will be `-1`. + /// + /// This index is intended for use as a performance enhancement when looking up member schemas + /// during deserialization. + public let index: Int + + /// Creates a new Schema using the passed parameters. + /// + /// No validation is performed on the parameters since calls to this initializer + /// are almost always code-generated from a previously validated Smithy model. public init( - id: String, + id: ShapeID, type: ShapeType, - traits: [String: Node] = [:], + traits: [ShapeID: Node] = [:], members: [Schema] = [], - memberName: String? = nil, target: Schema? = nil, index: Int = -1 ) { @@ -27,8 +56,17 @@ public class Schema { self.type = type self.traits = traits self.members = members - self.memberName = memberName self.target = target self.index = index } } + +public extension Schema { + + /// The member name for this schema, if any. + /// + /// Member name is computed from the schema's ID. + var memberName: String? { + id.member + } +} diff --git a/Sources/Smithy/Schema/ShapeID.swift b/Sources/Smithy/Schema/ShapeID.swift new file mode 100644 index 000000000..b78a4d3bb --- /dev/null +++ b/Sources/Smithy/Schema/ShapeID.swift @@ -0,0 +1,42 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Represents a single Smithy shape ID. +/// +/// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id). +public struct ShapeID: Hashable { + public let namespace: String + public let name: String + public let member: String? + + /// Creates a Shape ID for a Smithy shape. + /// + /// This initializer does no validation of length or of allowed characters in the Shape ID; + /// that is to be ensured by the caller (typically calls to this initializer will be code-generated + /// from previously validated Smithy models.) + /// - Parameters: + /// - namespace: The namespace for this shape, i.e. `smithy.api`. + /// - name: The name for this shape, i.e. `Integer`. + /// - member: The optional member name for this shape. + public init(_ namespace: String, _ name: String, _ member: String? = nil) { + self.namespace = namespace + self.name = name + self.member = member + } +} + +extension ShapeID: CustomStringConvertible { + + /// Returns the absolute Shape ID in a single, printable string. + public var description: String { + if let member = self.member { + return "\(namespace)#\(name)$\(member)" + } else { + return "\(namespace)#\(name)" + } + } +} diff --git a/Sources/Smithy/Schema/ShapeType.swift b/Sources/Smithy/Schema/ShapeType.swift index 5b3de70a1..310a24fe8 100644 --- a/Sources/Smithy/Schema/ShapeType.swift +++ b/Sources/Smithy/Schema/ShapeType.swift @@ -5,8 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -/// Reproduces the cases in Smithy `ShapeType`. -/// https://github.com/smithy-lang/smithy/blob/main/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +/// Reproduces the cases in Smithy [ShapeType](https://github.com/smithy-lang/smithy/blob/main/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java). public enum ShapeType { case blob case boolean diff --git a/Tests/SmithyTests/ShapeIDTests.swift b/Tests/SmithyTests/ShapeIDTests.swift new file mode 100644 index 000000000..d4b28de16 --- /dev/null +++ b/Tests/SmithyTests/ShapeIDTests.swift @@ -0,0 +1,22 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import Smithy + +class ShapeIDTests: XCTestCase { + + func test_description_noMember() { + let subject = ShapeID("smithy.test", "TestShape") + XCTAssertEqual(subject.description, "smithy.test#TestShape") + } + + func test_description_withMember() { + let subject = ShapeID("smithy.test", "TestShape", "TestMember") + XCTAssertEqual(subject.description, "smithy.test#TestShape$TestMember") + } +} 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 001aa70b9..278497c00 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 @@ -191,12 +191,14 @@ abstract class HTTPBindingProtocolGenerator( } override fun generateDeserializers(ctx: ProtocolGenerator.GenerationContext) { + if (usesSchemaBasedSerialization(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 val nestedShapes = resolveShapesNeedingCodableConformance(ctx) .filter { !it.isEventStreaming } 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 index 0f1b98533..234f8f59f 100644 --- 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 @@ -2,6 +2,7 @@ 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 @@ -26,51 +27,50 @@ class SchemaGenerator( private fun renderSchemaStruct(shape: Shape, index: Int? = null) { writer.openBlock(".init(", "),") { - writer.write("id: \$S,", shape.id.toString()) + 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( - "\$S: \$L,", - trait.key.toString(), + "\$L: \$L,", + shapeID(trait.key), trait.value.toNode().toSwiftNode(writer), ) } - writer.unwrite(",\n") - writer.write("") } } if (shape.members().isNotEmpty()) { writer.openBlock("members: [", "],") { shape.members().withIndex().forEach { renderSchemaStruct(it.value, it.index) } - writer.unwrite(",\n") - writer.write("") } } - shape.id.member - .getOrNull() - ?.let { writer.write("memberName: \$S,", it) } - targetShape(shape)?.let { writer.write("target: \$L,", it.schemaVar(writer)) } - index?.let { writer.write("index: \$L,", it) } + targetShape(shape)?.let { + writer.write("target: \$L,", it.schemaVar(writer)) + } + index?.let { + writer.write("index: \$L,", it) + } writer.unwrite(",\n") writer.write("") } } - private fun targetShape(shape: Shape): Shape? = memberShape(shape)?.let { ctx.model.expectShape(it.target) } + 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 memberShape(shape: Shape): MemberShape? = shape.asMemberShape().getOrNull() -} + private fun targetShape(shape: Shape): Shape? = + memberShape(shape)?.let { ctx.model.expectShape(it.target) } -private 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", - ) + 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 index 104f5a0f5..ac2f7b2bb 100644 --- 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 @@ -13,29 +13,31 @@ fun Shape.schemaVar(writer: SwiftWriter): String = this.id.schemaVarName() } -private fun ShapeId.preludeSchemaVarName(writer: SwiftWriter): String = - when (this.name) { - "Unit" -> writer.format("\$N", SmithyTypes.unitSchema) - "String" -> writer.format("\$N", SmithyTypes.stringSchema) - "Blob" -> writer.format("\$N", SmithyTypes.blobSchema) - "Integer" -> writer.format("\$N", SmithyTypes.integerSchema) - "Timestamp" -> writer.format("\$N", SmithyTypes.timestampSchema) - "Boolean" -> writer.format("\$N", SmithyTypes.booleanSchema) - "Float" -> writer.format("\$N", SmithyTypes.floatSchema) - "Double" -> writer.format("\$N", SmithyTypes.doubleSchema) - "Long" -> writer.format("\$N", SmithyTypes.longSchema) - "Short" -> writer.format("\$N", SmithyTypes.shortSchema) - "Byte" -> writer.format("\$N", SmithyTypes.byteSchema) - "PrimitiveInteger" -> writer.format("\$N", SmithyTypes.primitiveIntegerSchema) - "PrimitiveBoolean" -> writer.format("\$N", SmithyTypes.primitiveBooleanSchema) - "PrimitiveFloat" -> writer.format("\$N", SmithyTypes.primitiveFloatSchema) - "PrimitiveDouble" -> writer.format("\$N", SmithyTypes.primitiveDoubleSchema) - "PrimitiveLong" -> writer.format("\$N", SmithyTypes.primitiveLongSchema) - "PrimitiveShort" -> writer.format("\$N", SmithyTypes.primitiveShortSchema) - "PrimitiveByte" -> writer.format("\$N", SmithyTypes.primitiveByteSchema) - "Document" -> writer.format("\$N", SmithyTypes.documentSchema) +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) 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 new file mode 100644 index 000000000..91a446af6 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt @@ -0,0 +1,12 @@ +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/swiftmodules/SmithyTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt index d9641d472..560632fdb 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt @@ -22,25 +22,7 @@ object SmithyTypes { val RequestMessageSerializer = runtimeSymbol("RequestMessageSerializer", SwiftDeclaration.PROTOCOL) val URIQueryItem = runtimeSymbol("URIQueryItem", SwiftDeclaration.STRUCT) val Schema = runtimeSymbol("Schema", SwiftDeclaration.CLASS) - val unitSchema = runtimeSymbol("unitSchema", SwiftDeclaration.VAR) - val stringSchema = runtimeSymbol("stringSchema", SwiftDeclaration.VAR) - val blobSchema = runtimeSymbol("blobSchema", SwiftDeclaration.VAR) - val integerSchema = runtimeSymbol("integerSchema", SwiftDeclaration.VAR) - val timestampSchema = runtimeSymbol("timestampSchema", SwiftDeclaration.VAR) - val booleanSchema = runtimeSymbol("booleanSchema", SwiftDeclaration.VAR) - val floatSchema = runtimeSymbol("floatSchema", SwiftDeclaration.VAR) - val doubleSchema = runtimeSymbol("doubleSchema", SwiftDeclaration.VAR) - val longSchema = runtimeSymbol("longSchema", SwiftDeclaration.VAR) - val shortSchema = runtimeSymbol("shortSchema", SwiftDeclaration.VAR) - val byteSchema = runtimeSymbol("byteSchema", SwiftDeclaration.VAR) - val primitiveBooleanSchema = runtimeSymbol("primitiveBooleanSchema", SwiftDeclaration.VAR) - val primitiveFloatSchema = runtimeSymbol("primitiveFloatSchema", SwiftDeclaration.VAR) - val primitiveDoubleSchema = runtimeSymbol("primitiveDoubleSchema", SwiftDeclaration.VAR) - val primitiveLongSchema = runtimeSymbol("primitiveLongSchema", SwiftDeclaration.VAR) - val primitiveIntegerSchema = runtimeSymbol("primitiveIntegerSchema", SwiftDeclaration.VAR) - val primitiveShortSchema = runtimeSymbol("primitiveShortSchema", SwiftDeclaration.VAR) - val primitiveByteSchema = runtimeSymbol("primitiveByteSchema", SwiftDeclaration.VAR) - val documentSchema = runtimeSymbol("documentSchema", SwiftDeclaration.VAR) + val Prelude = runtimeSymbol("Prelude", SwiftDeclaration.ENUM) } private fun runtimeSymbol( From 6e2519b490c1ce0a1bd0fef3e821e6d956677d08 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 5 Nov 2025 10:17:42 -0600 Subject: [PATCH 03/29] Re-enable serde code --- .../amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt | 2 -- .../codegen/integration/HTTPBindingProtocolGenerator.kt | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) 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 4123503ba..94a56b8bc 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 @@ -72,7 +72,6 @@ class DirectedSwiftCodegen( LOGGER.info("Generating Swift client for service ${directive.settings().service}") - var shouldGenerateTestTarget = false context.protocolGenerator?.apply { val ctx = ProtocolGenerator.GenerationContext(settings, model, service, symbolProvider, integrations, this.protocol, writers) LOGGER.info("[${service.id}] Generating serde for protocol ${this.protocol}") @@ -87,7 +86,6 @@ class DirectedSwiftCodegen( LOGGER.info("[${service.id}] Generating unit tests for protocol ${this.protocol}") val numProtocolUnitTestsGenerated = generateProtocolUnitTests(ctx) - shouldGenerateTestTarget = (numProtocolUnitTestsGenerated > 0) LOGGER.info("[${service.id}] Generated $numProtocolUnitTestsGenerated tests for protocol ${this.protocol}") 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 278497c00..edce23ef9 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 @@ -142,7 +142,7 @@ abstract class HTTPBindingProtocolGenerator( override var serviceErrorProtocolSymbol: Symbol = ClientRuntimeTypes.Http.HttpError override fun generateSerializers(ctx: ProtocolGenerator.GenerationContext) { - if (usesSchemaBasedSerialization(ctx)) { return } // temporary condition +// if (usesSchemaBasedSerialization(ctx)) { return } // temporary condition // render conformance to HttpRequestBinding for all input shapes val inputShapesWithHttpBindings: MutableSet = mutableSetOf() for (operation in getHttpBindingOperations(ctx)) { @@ -191,14 +191,14 @@ abstract class HTTPBindingProtocolGenerator( } override fun generateDeserializers(ctx: ProtocolGenerator.GenerationContext) { - if (usesSchemaBasedSerialization(ctx)) { return } // temporary condition +// if (usesSchemaBasedSerialization(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 (usesSchemaBasedSerialization(ctx)) { return } // temporary condition val nestedShapes = resolveShapesNeedingCodableConformance(ctx) .filter { !it.isEventStreaming } @@ -240,7 +240,7 @@ abstract class HTTPBindingProtocolGenerator( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { - if (!usesSchemaBasedSerialization(ctx)) { return } // temporary condition +// if (!usesSchemaBasedSerialization(ctx)) { return } // temporary condition if (!shape.hasTrait() && !shape.hasTrait()) { return } From c32b39b7c4f045af670c5e9b942258266b3193c5 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 5 Nov 2025 11:12:14 -0600 Subject: [PATCH 04/29] Cleanup --- Sources/Smithy/Schema/Schema.swift | 12 ++--- Sources/Smithy/Schema/ShapeID.swift | 6 +-- Sources/Smithy/Schema/ShapeType.swift | 2 +- .../HTTPBindingProtocolGenerator.kt | 31 ++++++------ .../serde/schema/SchemaGenerator.kt | 11 +++-- .../serde/schema/SchemaShapeUtils.kt | 47 ++++++++++--------- .../swiftmodules/SmithyReadWriteTypes.kt | 5 -- 7 files changed, 57 insertions(+), 57 deletions(-) diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift index ff94fd859..f8cac05fb 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -5,24 +5,24 @@ // SPDX-License-Identifier: Apache-2.0 // -/// A structure which describes selected Smithy model information for a Smithy model shape. +/// A class which describes selected Smithy model information for a Smithy model shape. /// /// Typically, the Schema contains only modeled info & properties that are relevant to /// serialization, transport bindings, and other functions performed by the SDK. -public class Schema { +public final class Schema: Sendable { /// The Smithy shape ID for the shape described by this schema. public let id: ShapeID - + /// The type of the shape being described. public let type: ShapeType - + /// A dictionary of the described shape's trait shape IDs to Nodes with trait data. /// /// Not all traits for a shape will be represented in the schema; /// typically the Schema contains only the traits relevant to the client-side SDK. public let traits: [ShapeID: Node] - + /// The member schemas for this schema, if any. /// /// Typically only a schema of type Structure, Union, Enum, IntEnum, List or Map will have members. @@ -39,7 +39,7 @@ public class Schema { /// This index is intended for use as a performance enhancement when looking up member schemas /// during deserialization. public let index: Int - + /// Creates a new Schema using the passed parameters. /// /// No validation is performed on the parameters since calls to this initializer diff --git a/Sources/Smithy/Schema/ShapeID.swift b/Sources/Smithy/Schema/ShapeID.swift index b78a4d3bb..647bbfeb8 100644 --- a/Sources/Smithy/Schema/ShapeID.swift +++ b/Sources/Smithy/Schema/ShapeID.swift @@ -8,11 +8,11 @@ /// Represents a single Smithy shape ID. /// /// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id). -public struct ShapeID: Hashable { +public struct ShapeID: Sendable, Hashable { public let namespace: String public let name: String public let member: String? - + /// Creates a Shape ID for a Smithy shape. /// /// This initializer does no validation of length or of allowed characters in the Shape ID; @@ -30,7 +30,7 @@ public struct ShapeID: Hashable { } extension ShapeID: CustomStringConvertible { - + /// Returns the absolute Shape ID in a single, printable string. public var description: String { if let member = self.member { diff --git a/Sources/Smithy/Schema/ShapeType.swift b/Sources/Smithy/Schema/ShapeType.swift index 310a24fe8..43a97cc5b 100644 --- a/Sources/Smithy/Schema/ShapeType.swift +++ b/Sources/Smithy/Schema/ShapeType.swift @@ -6,7 +6,7 @@ // /// Reproduces the cases in Smithy [ShapeType](https://github.com/smithy-lang/smithy/blob/main/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java). -public enum ShapeType { +public enum ShapeType: Sendable { case blob case boolean case string 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 edce23ef9..cfd98792a 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 @@ -142,7 +142,7 @@ abstract class HTTPBindingProtocolGenerator( override var serviceErrorProtocolSymbol: Symbol = ClientRuntimeTypes.Http.HttpError override fun generateSerializers(ctx: ProtocolGenerator.GenerationContext) { -// if (usesSchemaBasedSerialization(ctx)) { return } // temporary condition +// if (usesSchemaBasedSerialization(ctx)) return // temporary condition // render conformance to HttpRequestBinding for all input shapes val inputShapesWithHttpBindings: MutableSet = mutableSetOf() for (operation in getHttpBindingOperations(ctx)) { @@ -191,14 +191,14 @@ abstract class HTTPBindingProtocolGenerator( } override fun generateDeserializers(ctx: ProtocolGenerator.GenerationContext) { -// if (usesSchemaBasedSerialization(ctx)) { return } // temporary condition +// if (usesSchemaBasedSerialization(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 (usesSchemaBasedSerialization(ctx)) return // temporary condition val nestedShapes = resolveShapesNeedingCodableConformance(ctx) .filter { !it.isEventStreaming } @@ -209,12 +209,14 @@ 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" || it.name == "awsJson1_0" || it.name == "awsJson1_1" } + 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 + 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) } } @@ -240,7 +242,7 @@ abstract class HTTPBindingProtocolGenerator( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { -// if (!usesSchemaBasedSerialization(ctx)) { return } // temporary condition +// if (!usesSchemaBasedSerialization(ctx)) return // temporary condition if (!shape.hasTrait() && !shape.hasTrait()) { return } @@ -400,12 +402,13 @@ abstract class HTTPBindingProtocolGenerator( } 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 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) 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 index 234f8f59f..dd666794b 100644 --- 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 @@ -25,7 +25,10 @@ class SchemaGenerator( } } - private fun renderSchemaStruct(shape: Shape, index: Int? = null) { + private fun renderSchemaStruct( + shape: Shape, + index: Int? = null, + ) { writer.openBlock(".init(", "),") { writer.write( "id: \$L,", @@ -68,9 +71,7 @@ class SchemaGenerator( id.member.getOrNull()?.let { writer.format(", \$S", it) } ?: "", ) - private fun targetShape(shape: Shape): Shape? = - memberShape(shape)?.let { ctx.model.expectShape(it.target) } + private fun targetShape(shape: Shape): Shape? = memberShape(shape)?.let { ctx.model.expectShape(it.target) } - private fun memberShape(shape: Shape): MemberShape? = - shape.asMemberShape().getOrNull() + 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 index ac2f7b2bb..74af64df7 100644 --- 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 @@ -14,28 +14,29 @@ fun Shape.schemaVar(writer: SwiftWriter): String = } 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}") - } + 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) } @@ -43,5 +44,5 @@ private fun ShapeId.schemaVarName(): String { assert(this.member.getOrNull() == null) val namespacePortion = this.namespace.replace(".", "_") val namePortion = this.name - return "schema__${namespacePortion}__${namePortion}" + return "schema__${namespacePortion}__$namePortion" } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt index d383db5a1..070e26034 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt @@ -27,11 +27,6 @@ object SmithyReadWriteTypes { val WritingClosures = runtimeSymbol("WritingClosures", SwiftDeclaration.ENUM) val ReadingClosureBox = runtimeSymbol("ReadingClosureBox", SwiftDeclaration.STRUCT) val WritingClosureBox = runtimeSymbol("WritingClosureBox", SwiftDeclaration.STRUCT) - val ShapeSerializer = runtimeSymbol("ShapeSerializer", SwiftDeclaration.PROTOCOL) - val ShapeDeserializer = runtimeSymbol("ShapeDeserializer", SwiftDeclaration.PROTOCOL) - val SerializableStruct = runtimeSymbol("SerializableStruct", SwiftDeclaration.PROTOCOL) - val DeserializableStruct = runtimeSymbol("DeserializableStruct", SwiftDeclaration.PROTOCOL) - val Unit = runtimeSymbol("Unit", SwiftDeclaration.STRUCT) } private fun runtimeSymbol( From 21ed6f74ca48307181121e04ee9996a9254458aa Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 11:58:30 -0600 Subject: [PATCH 05/29] feat: Create code generator plugin --- Package.swift | 15 ++++ .../SmithyCodeGeneratorPlugin.swift | 68 +++++++++++++++++++ .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 62 +++++++++++++++++ .../swift/codegen/DirectedSwiftCodegen.kt | 3 + .../codegen/SmithyModelFileInfoGenerator.kt | 25 +++++++ 5 files changed, 173 insertions(+) create mode 100644 Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift create mode 100644 Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt diff --git a/Package.swift b/Package.swift index 940b1e6bf..c0dfb0a98 100644 --- a/Package.swift +++ b/Package.swift @@ -53,10 +53,12 @@ let package = Package( .library(name: "SmithyCBOR", targets: ["SmithyCBOR"]), .library(name: "SmithyWaitersAPI", targets: ["SmithyWaitersAPI"]), .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]), + .plugin(name: "SmithyCodeGenerator", targets: ["SmithyCodeGenerator"]), ], dependencies: { var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.54.2"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"), ] @@ -258,6 +260,19 @@ let package = Package( .target( name: "SmithyWaitersAPI" ), + .plugin( + name: "SmithyCodeGenerator", + capability: .buildTool(), + dependencies: [ + "SmithyCodegenCLI", + ] + ), + .executableTarget( + name: "SmithyCodegenCLI", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift new file mode 100644 index 000000000..116d4b9d0 --- /dev/null +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -0,0 +1,68 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import class Foundation.FileManager +import class Foundation.JSONDecoder +import struct Foundation.URL +import PackagePlugin + +@main +struct SmithyCodeGeneratorPlugin: BuildToolPlugin { + + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + // This plugin only runs for package targets that can have source files. + guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] } + + // Retrieve the `SmithyCodegenCLI` tool from the plugin's tools. + let smithyCodegenCLITool = try context.tool(named: "SmithyCodegenCLI") + + // Construct a build command for each source file with a particular suffix. + return try sourceFiles.map(\.path).compactMap { + try createBuildCommand(name: target.name, for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path) + } + } + + private func createBuildCommand( + name: String, + for inputPath: Path, + 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 } + + let currentWorkingDirectoryFileURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + // Get the smithy model path. + let modelInfoData = try Data(contentsOf: URL(fileURLWithPath: inputPath.string)) + let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData) + let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path) + let modelPath = Path(modelPathURL.path) + + // Construct the schemas.swift path. + let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") + + // Construct the build command that invokes SmithyCodegenCLI. + return .buildCommand( + displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", + executable: generatorToolPath, + arguments: [ + "--schemas-path", schemasSwiftPath, + modelPath + ], + inputFiles: [inputPath, modelPath], + outputFiles: [schemasSwiftPath] + ) + } +} + +/// Codable structure for reading the contents of `smithy-model-info.json` +private struct SmithyModelInfo: Decodable { + /// The path to the model, from the root of the target's project. Required. + let path: String +} diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift new file mode 100644 index 000000000..bf8598d60 --- /dev/null +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ArgumentParser +import Foundation +import SmithyCodegenCore + +@main +struct SmithyCodegenCLI: AsyncParsableCommand { + + @Argument(help: "The full or relative path to the JSON model file.") + var modelPath: String + + @Option(help: "The full or relative path to write the schemas output file.") + var schemasPath: String? + + func run() async throws { + let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() + print("Current working directory: \(currentWorkingDirectoryFileURL.path)") + + // Create the model file URL + let modelFileURL = URL(fileURLWithPath: modelPath, relativeTo: currentWorkingDirectoryFileURL) + guard FileManager.default.fileExists(atPath: modelFileURL.path) else { + throw SmithyCodegenCLIError(localizedDescription: "no file at model path \(modelFileURL.path)") + } + print("Model file path: \(modelFileURL.path)") + + // If --schemas-path was supplied, create the schema file URL + let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) + + // Use resolved file URLs to run code generator + try CodeGenerator(modelFileURL: modelFileURL, schemasFileURL: schemasFileURL).run() + } + + private func currentWorkingDirectoryFileURL() -> URL { + // Get the current working directory as a file URL + var currentWorkingDirectoryPath = FileManager.default.currentDirectoryPath + if !currentWorkingDirectoryPath.hasSuffix("/") { + currentWorkingDirectoryPath.append("/") + } + return URL(fileURLWithPath: currentWorkingDirectoryPath) + } + + private func resolve(paramName: String, path: String?) -> URL? { + if let path { + let fileURL = URL(fileURLWithPath: path, relativeTo: currentWorkingDirectoryFileURL()) + print("Resolved \(paramName): \(fileURL.path)") + return fileURL + } else { + print("\(paramName) not provided, skipping generation") + return nil + } + } +} + +struct SmithyCodegenCLIError: Error { + let localizedDescription: String +} 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 94a56b8bc..154348e74 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 @@ -107,6 +107,9 @@ class DirectedSwiftCodegen( DependencyJSONGenerator(ctx).writePackageJSON(writers.dependencies) } + LOGGER.info("Generating Smithy model file info") + SmithyModelFileInfoGenerator(ctx).writeSmithyModelFileInfo() + LOGGER.info("Flushing swift writers") writers.flushWriters() } 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 new file mode 100644 index 000000000..0662d8d7d --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt @@ -0,0 +1,25 @@ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.aws.traits.ServiceTrait +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.model.getTrait + +class SmithyModelFileInfoGenerator( + val ctx: ProtocolGenerator.GenerationContext, +) { + fun writeSmithyModelFileInfo() { + ctx.service.getTrait()?.let { serviceTrait -> + val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" + val modelFileName = + serviceTrait + .sdkId + .lowercase() + .replace(",", "") + .replace(" ", "-") + val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" + ctx.delegator.useFileWriter(filename) { writer -> + writer.write("{\"path\":\"$contents\"}") + } + } + } +} From 4ddfc98eff019e8a6e873b5f60fd40126971b16d Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:03:30 -0600 Subject: [PATCH 06/29] Fix swiftlint --- .../SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 116d4b9d0..180e3bb5e 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -23,7 +23,12 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Construct a build command for each source file with a particular suffix. return try sourceFiles.map(\.path).compactMap { - try createBuildCommand(name: target.name, for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path) + try createBuildCommand( + name: target.name, + for: $0, + in: context.pluginWorkDirectory, + with: smithyCodegenCLITool.path + ) } } From c57b182dedc3264bdb6befd0cc877e9d53aaf93f Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:08:32 -0600 Subject: [PATCH 07/29] Remove code generator reference --- Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index bf8598d60..d604f4e8b 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -31,9 +31,10 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) + print("Schemas file path: \(schemasFileURL?.path ?? "nil")") - // Use resolved file URLs to run code generator - try CodeGenerator(modelFileURL: modelFileURL, schemasFileURL: schemasFileURL).run() + // All file URLs needed for code generation have now been resolved. + // Implement code generation here. } private func currentWorkingDirectoryFileURL() -> URL { From 9bd4df2998800c8f48b10e73710cada2ee068456 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:43:30 -0600 Subject: [PATCH 08/29] Remove import --- Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index d604f4e8b..03da98c58 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -7,7 +7,6 @@ import ArgumentParser import Foundation -import SmithyCodegenCore @main struct SmithyCodegenCLI: AsyncParsableCommand { From f1ef4e852e295bd9a4d473d51e167d47942a8b0e Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:51:34 -0600 Subject: [PATCH 09/29] Create empty Schemas swift file --- Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index 03da98c58..b8b247155 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -30,10 +30,13 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) - print("Schemas file path: \(schemasFileURL?.path ?? "nil")") // All file URLs needed for code generation have now been resolved. // Implement code generation here. + if let schemasFileURL { + print("Schemas file path: \(schemasFileURL)") + FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) + } } private func currentWorkingDirectoryFileURL() -> URL { From e2ded31b64dd8194dd26f81c8f9123ea7c9039cb Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Fri, 12 Dec 2025 13:41:46 -0600 Subject: [PATCH 10/29] feat: Read JSON AST models into memory --- Package.swift | 4 ++ .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 15 ++++--- Sources/SmithyCodegenCore/AST/ASTError.swift | 14 ++++++ Sources/SmithyCodegenCore/AST/ASTMember.swift | 12 +++++ Sources/SmithyCodegenCore/AST/ASTModel.swift | 13 ++++++ Sources/SmithyCodegenCore/AST/ASTNode.swift | 45 +++++++++++++++++++ .../SmithyCodegenCore/AST/ASTReference.swift | 11 +++++ Sources/SmithyCodegenCore/AST/ASTShape.swift | 33 ++++++++++++++ Sources/SmithyCodegenCore/AST/ASTType.swift | 38 ++++++++++++++++ Sources/SmithyCodegenCore/CodeGenerator.swift | 33 ++++++++++++++ 10 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 Sources/SmithyCodegenCore/AST/ASTError.swift create mode 100644 Sources/SmithyCodegenCore/AST/ASTMember.swift create mode 100644 Sources/SmithyCodegenCore/AST/ASTModel.swift create mode 100644 Sources/SmithyCodegenCore/AST/ASTNode.swift create mode 100644 Sources/SmithyCodegenCore/AST/ASTReference.swift create mode 100644 Sources/SmithyCodegenCore/AST/ASTShape.swift create mode 100644 Sources/SmithyCodegenCore/AST/ASTType.swift create mode 100644 Sources/SmithyCodegenCore/CodeGenerator.swift diff --git a/Package.swift b/Package.swift index 70d3a7cc8..828214760 100644 --- a/Package.swift +++ b/Package.swift @@ -303,9 +303,13 @@ let package = Package( .executableTarget( name: "SmithyCodegenCLI", dependencies: [ + "SmithyCodegenCore", .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), + .target( + name: "SmithyCodegenCore" + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index b8b247155..bf0b608c8 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -7,6 +7,7 @@ import ArgumentParser import Foundation +import struct SmithyCodegenCore.CodeGenerator @main struct SmithyCodegenCLI: AsyncParsableCommand { @@ -17,6 +18,9 @@ struct SmithyCodegenCLI: AsyncParsableCommand { @Option(help: "The full or relative path to write the schemas output file.") var schemasPath: String? + @Option(help: "The full or relative path to write the struct consumers output file.") + var structConsumersPath: String? + func run() async throws { let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() print("Current working directory: \(currentWorkingDirectoryFileURL.path)") @@ -31,12 +35,11 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) - // All file URLs needed for code generation have now been resolved. - // Implement code generation here. - if let schemasFileURL { - print("Schemas file path: \(schemasFileURL)") - FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) - } + // Use resolved file URLs to run code generator + try CodeGenerator( + modelFileURL: modelFileURL, + schemasFileURL: schemasFileURL + ).run() } private func currentWorkingDirectoryFileURL() -> URL { diff --git a/Sources/SmithyCodegenCore/AST/ASTError.swift b/Sources/SmithyCodegenCore/AST/ASTError.swift new file mode 100644 index 000000000..3e32bc097 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ASTError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/AST/ASTMember.swift b/Sources/SmithyCodegenCore/AST/ASTMember.swift new file mode 100644 index 000000000..5d3a30c35 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#ast-member +struct ASTMember: Decodable { + let target: String + let traits: [String: ASTNode]? +} diff --git a/Sources/SmithyCodegenCore/AST/ASTModel.swift b/Sources/SmithyCodegenCore/AST/ASTModel.swift new file mode 100644 index 000000000..07de8bf89 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -0,0 +1,13 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#top-level-properties +struct ASTModel: Decodable { + let smithy: String + let metadata: ASTNode? + let shapes: [String: ASTShape] +} diff --git a/Sources/SmithyCodegenCore/AST/ASTNode.swift b/Sources/SmithyCodegenCore/AST/ASTNode.swift new file mode 100644 index 000000000..a3d6fd58b --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTNode.swift @@ -0,0 +1,45 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Contains the value of a Smithy Node, as used in a JSON AST. +/// +/// Smithy node data is basically the same as the data that can be stored in JSON. +/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. +/// +/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values +enum ASTNode { + case object([String: ASTNode]) + case list([ASTNode]) + case string(String) + case number(Double) + case boolean(Bool) + case null +} + +extension ASTNode: Decodable { + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let bool = try? container.decode(Bool.self) { + self = .boolean(bool) + } else if let int = try? container.decode(Int.self) { + self = .number(Double(int)) + } else if let double = try? container.decode(Double.self) { + self = .number(double) + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let array = try? container.decode([ASTNode].self) { + self = .list(array) + } else if let dictionary = try? container.decode([String: ASTNode].self) { + self = .object(dictionary) + } else { + throw ASTError("Undecodable value in AST node") + } + } +} diff --git a/Sources/SmithyCodegenCore/AST/ASTReference.swift b/Sources/SmithyCodegenCore/AST/ASTReference.swift new file mode 100644 index 000000000..4cdce691a --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTReference.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#ast-shape-reference +struct ASTReference: Decodable { + let target: String +} diff --git a/Sources/SmithyCodegenCore/AST/ASTShape.swift b/Sources/SmithyCodegenCore/AST/ASTShape.swift new file mode 100644 index 000000000..abeb8d425 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -0,0 +1,33 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#json-ast +// This Swift type captures fields for all AST shape types +struct ASTShape: Decodable { + let type: ASTType + let traits: [String: ASTNode]? + let member: ASTMember? + let key: ASTMember? + let value: ASTMember? + let members: [String: ASTMember]? + let version: String? + let operations: [ASTReference]? + let resources: [ASTReference]? + let errors: [ASTReference]? + let rename: [String: String]? + let identifiers: [String: ASTReference]? + let properties: [String: ASTReference]? + let create: ASTReference? + let put: ASTReference? + let read: ASTReference? + let update: ASTReference? + let delete: ASTReference? + let list: ASTReference? + let collectionOperations: [ASTReference]? + let input: ASTReference? + let output: ASTReference? +} diff --git a/Sources/SmithyCodegenCore/AST/ASTType.swift b/Sources/SmithyCodegenCore/AST/ASTType.swift new file mode 100644 index 000000000..adf1e6b52 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTType.swift @@ -0,0 +1,38 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/model.html#shape-types +enum ASTType: String, Decodable { + // These cases are all the standard Smithy shape types + case blob + case boolean + case string + case timestamp + case byte + case short + case integer + case long + case float + case document + case double + case bigDecimal + case bigInteger + case `enum` + case intEnum + case list + case set + case map + case structure + case union + case member + case service + case resource + case operation + + // Special for AST, added 'apply' case + case apply +} diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift new file mode 100644 index 000000000..0a8e91b6e --- /dev/null +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -0,0 +1,33 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import class Foundation.JSONDecoder +import struct Foundation.URL + + +public struct CodeGenerator { + let modelFileURL: URL + let schemasFileURL: URL? + + public init( + modelFileURL: URL, + schemasFileURL: URL?, + ) { + self.modelFileURL = modelFileURL + self.schemasFileURL = schemasFileURL + } + + public func run() throws { + // Load the AST from the model file + let modelData = try Data(contentsOf: modelFileURL) + let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) + + // In the future, AST will be used to create a Model + // Model will be used to generate code + } +} From f979613860dc565cc855515c73ccff3f62d083c8 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Fri, 12 Dec 2025 13:54:51 -0600 Subject: [PATCH 11/29] Fix trailing comma --- Sources/SmithyCodegenCore/CodeGenerator.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 0a8e91b6e..66b7c2791 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -9,14 +9,13 @@ import struct Foundation.Data import class Foundation.JSONDecoder import struct Foundation.URL - public struct CodeGenerator { let modelFileURL: URL let schemasFileURL: URL? public init( modelFileURL: URL, - schemasFileURL: URL?, + schemasFileURL: URL? ) { self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL From d5a27661bd3722a7e0493f0a08351b7a57baae8f Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Fri, 12 Dec 2025 14:26:27 -0600 Subject: [PATCH 12/29] re-add blank schemas file --- Sources/SmithyCodegenCore/CodeGenerator.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 66b7c2791..24f7b056c 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -6,6 +6,7 @@ // import struct Foundation.Data +import class Foundation.FileManager import class Foundation.JSONDecoder import struct Foundation.URL @@ -26,7 +27,15 @@ public struct CodeGenerator { let modelData = try Data(contentsOf: modelFileURL) let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) - // In the future, AST will be used to create a Model - // Model will be used to generate code + // In the future, AST will be used to create a Model. + // Model will be used to generate code. + + // This code simply writes an empty schemas file, since it is expected to exist after the + // code generator plugin runs. + // + // Actual code generation will be implemented here later. + if let schemasFileURL { + FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) + } } } From e59b27edbe78558643028ed97f394fb2bb8c19e4 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 17 Dec 2025 10:46:34 -0600 Subject: [PATCH 13/29] feat: Add generation context --- Package.swift | 5 +- Sources/Smithy/Schema/Schema.swift | 24 ++- Sources/Smithy/Schema/ShapeID.swift | 42 ----- Sources/Smithy/ShapeID.swift | 90 ++++++++++ Sources/Smithy/{Schema => }/ShapeType.swift | 0 Sources/SmithyCodegenCore/AST/ASTMember.swift | 4 +- Sources/SmithyCodegenCore/AST/ASTModel.swift | 4 +- Sources/SmithyCodegenCore/AST/ASTShape.swift | 4 +- .../AST/{ASTNode.swift => Node+AST.swift} | 23 +-- Sources/SmithyCodegenCore/CodeGenerator.swift | 10 +- .../SmithyCodegenCore/GenerationContext.swift | 27 +++ .../SmithyCodegenCore/Model/Model+AST.swift | 162 ++++++++++++++++++ Sources/SmithyCodegenCore/Model/Model.swift | 28 +++ .../SmithyCodegenCore/Model/ModelError.swift | 14 ++ .../SmithyCodegenCore/Model/ShapeID+AST.swift | 17 ++ .../Model/ShapeType+AST.swift | 68 ++++++++ .../SmithyCodegenCore/Shape/EnumShape.swift | 28 +++ .../SmithyCodegenCore/Shape/HasMembers.swift | 11 ++ .../Shape/IntEnumShape.swift | 28 +++ .../SmithyCodegenCore/Shape/ListShape.swift | 32 ++++ .../SmithyCodegenCore/Shape/MapShape.swift | 36 ++++ .../SmithyCodegenCore/Shape/MemberShape.swift | 23 +++ .../Shape/OperationShape.swift | 46 +++++ .../Shape/ServiceShape.swift | 23 +++ .../Shape/Shape+Prelude.swift | 128 ++++++++++++++ Sources/SmithyCodegenCore/Shape/Shape.swift | 78 +++++++++ .../Shape/StructureShape.swift | 28 +++ .../SmithyCodegenCore/Shape/UnionShape.swift | 28 +++ .../SymbolProvider/SymbolProvider.swift | 129 ++++++++++++++ .../SymbolProvider/SymbolProviderError.swift | 14 ++ 30 files changed, 1074 insertions(+), 80 deletions(-) delete mode 100644 Sources/Smithy/Schema/ShapeID.swift create mode 100644 Sources/Smithy/ShapeID.swift rename Sources/Smithy/{Schema => }/ShapeType.swift (100%) rename Sources/SmithyCodegenCore/AST/{ASTNode.swift => Node+AST.swift} (55%) create mode 100644 Sources/SmithyCodegenCore/GenerationContext.swift create mode 100644 Sources/SmithyCodegenCore/Model/Model+AST.swift create mode 100644 Sources/SmithyCodegenCore/Model/Model.swift create mode 100644 Sources/SmithyCodegenCore/Model/ModelError.swift create mode 100644 Sources/SmithyCodegenCore/Model/ShapeID+AST.swift create mode 100644 Sources/SmithyCodegenCore/Model/ShapeType+AST.swift create mode 100644 Sources/SmithyCodegenCore/Shape/EnumShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/HasMembers.swift create mode 100644 Sources/SmithyCodegenCore/Shape/IntEnumShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/ListShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/MapShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/MemberShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/OperationShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/ServiceShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift create mode 100644 Sources/SmithyCodegenCore/Shape/Shape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/StructureShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/UnionShape.swift create mode 100644 Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift create mode 100644 Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift diff --git a/Package.swift b/Package.swift index 828214760..e0474d213 100644 --- a/Package.swift +++ b/Package.swift @@ -308,7 +308,10 @@ let package = Package( ] ), .target( - name: "SmithyCodegenCore" + name: "SmithyCodegenCore", + dependencies: [ + "Smithy", + ] ), .testTarget( name: "ClientRuntimeTests", diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift index f8cac05fb..f82adc1a7 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -/// A class which describes selected Smithy model information for a Smithy model shape. +/// A class which describes selected, modeled information for a Smithy shape. /// /// Typically, the Schema contains only modeled info & properties that are relevant to /// serialization, transport bindings, and other functions performed by the SDK. @@ -29,7 +29,13 @@ public final class Schema: Sendable { public let members: [Schema] /// The target schema for this schema. Will only be used when this is a member schema. - public let target: Schema? + public var target: Schema? { + _target() + } + + /// Target schema is passed as an autoclosure so that schemas with self-referencing targets will not cause + /// an infinite loop when accessed. + private let _target: @Sendable () -> Schema? /// The index of this schema, if it represents a Smithy member. /// @@ -49,24 +55,14 @@ public final class Schema: Sendable { type: ShapeType, traits: [ShapeID: Node] = [:], members: [Schema] = [], - target: Schema? = nil, + target: @Sendable @escaping @autoclosure () -> Schema? = nil, index: Int = -1 ) { self.id = id self.type = type self.traits = traits self.members = members - self.target = target + self._target = target self.index = index } } - -public extension Schema { - - /// The member name for this schema, if any. - /// - /// Member name is computed from the schema's ID. - var memberName: String? { - id.member - } -} diff --git a/Sources/Smithy/Schema/ShapeID.swift b/Sources/Smithy/Schema/ShapeID.swift deleted file mode 100644 index 647bbfeb8..000000000 --- a/Sources/Smithy/Schema/ShapeID.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -/// Represents a single Smithy shape ID. -/// -/// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id). -public struct ShapeID: Sendable, Hashable { - public let namespace: String - public let name: String - public let member: String? - - /// Creates a Shape ID for a Smithy shape. - /// - /// This initializer does no validation of length or of allowed characters in the Shape ID; - /// that is to be ensured by the caller (typically calls to this initializer will be code-generated - /// from previously validated Smithy models.) - /// - Parameters: - /// - namespace: The namespace for this shape, i.e. `smithy.api`. - /// - name: The name for this shape, i.e. `Integer`. - /// - member: The optional member name for this shape. - public init(_ namespace: String, _ name: String, _ member: String? = nil) { - self.namespace = namespace - self.name = name - self.member = member - } -} - -extension ShapeID: CustomStringConvertible { - - /// Returns the absolute Shape ID in a single, printable string. - public var description: String { - if let member = self.member { - return "\(namespace)#\(name)$\(member)" - } else { - return "\(namespace)#\(name)" - } - } -} diff --git a/Sources/Smithy/ShapeID.swift b/Sources/Smithy/ShapeID.swift new file mode 100644 index 000000000..a429b0fed --- /dev/null +++ b/Sources/Smithy/ShapeID.swift @@ -0,0 +1,90 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Represents a Smithy shape ID. +/// +/// The id that ShapeID is created from is presumed to be properly formed, since this type will usually +/// be constructed from previously validated models. +/// +/// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id). +public struct ShapeID: Hashable, Sendable { + public let namespace: String + public let name: String + public let member: String? + + /// Creates a Shape ID for a Smithy shape. + /// + /// This initializer does no validation of length or of allowed characters in the Shape ID; + /// that is to be ensured by the caller (typically calls to this initializer will be code-generated + /// from previously validated Smithy models.) + /// - Parameters: + /// - namespace: The namespace for this shape, i.e. `smithy.api`. + /// - name: The name for this shape, i.e. `Integer`. + /// - member: The optional member name for this shape. + public init(_ namespace: String, _ name: String, _ member: String? = nil) { + self.namespace = namespace + self.name = name + self.member = member + } + + public init(_ id: String) throws { + let splitOnPound = id.split(separator: "#") + guard splitOnPound.count == 2 else { + throw ShapeIDError("id \"\(id)\" does not have a #") + } + guard let namespace = splitOnPound.first, !namespace.isEmpty else { + throw ShapeIDError("id \"\(id)\" does not have a nonempty namespace") + } + self.namespace = String(namespace) + let splitOnDollar = splitOnPound.last!.split(separator: "$") + switch splitOnDollar.count { + case 2: + self.name = String(splitOnDollar.first!) + self.member = String(splitOnDollar.last!) + case 1: + self.name = String(splitOnDollar.first!) + self.member = nil + default: + throw ShapeIDError("id \"\(id)\" has more than one $") + } + } + + public init(id: ShapeID, member: String) { + self.namespace = id.namespace + self.name = id.name + self.member = member + } + + public var id: String { + if let member { + return "\(namespace)#\(name)$\(member)" + } else { + return "\(namespace)#\(name)" + } + } +} + +extension ShapeID: Comparable { + + public static func < (lhs: ShapeID, rhs: ShapeID) -> Bool { + return lhs.id.lowercased() < rhs.id.lowercased() + } +} + +extension ShapeID: CustomStringConvertible { + + /// Returns the absolute Shape ID in a single, printable string. + public var description: String { id } +} + +public struct ShapeIDError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/Smithy/Schema/ShapeType.swift b/Sources/Smithy/ShapeType.swift similarity index 100% rename from Sources/Smithy/Schema/ShapeType.swift rename to Sources/Smithy/ShapeType.swift diff --git a/Sources/SmithyCodegenCore/AST/ASTMember.swift b/Sources/SmithyCodegenCore/AST/ASTMember.swift index 5d3a30c35..6602103b2 100644 --- a/Sources/SmithyCodegenCore/AST/ASTMember.swift +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -5,8 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#ast-member struct ASTMember: Decodable { let target: String - let traits: [String: ASTNode]? + let traits: [String: Node]? } diff --git a/Sources/SmithyCodegenCore/AST/ASTModel.swift b/Sources/SmithyCodegenCore/AST/ASTModel.swift index 07de8bf89..c3c13d8ee 100644 --- a/Sources/SmithyCodegenCore/AST/ASTModel.swift +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -5,9 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#top-level-properties struct ASTModel: Decodable { let smithy: String - let metadata: ASTNode? + let metadata: Node? let shapes: [String: ASTShape] } diff --git a/Sources/SmithyCodegenCore/AST/ASTShape.swift b/Sources/SmithyCodegenCore/AST/ASTShape.swift index abeb8d425..a2ffd146b 100644 --- a/Sources/SmithyCodegenCore/AST/ASTShape.swift +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -5,11 +5,13 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#json-ast // This Swift type captures fields for all AST shape types struct ASTShape: Decodable { let type: ASTType - let traits: [String: ASTNode]? + let traits: [String: Node]? let member: ASTMember? let key: ASTMember? let value: ASTMember? diff --git a/Sources/SmithyCodegenCore/AST/ASTNode.swift b/Sources/SmithyCodegenCore/AST/Node+AST.swift similarity index 55% rename from Sources/SmithyCodegenCore/AST/ASTNode.swift rename to Sources/SmithyCodegenCore/AST/Node+AST.swift index a3d6fd58b..72f2e3239 100644 --- a/Sources/SmithyCodegenCore/AST/ASTNode.swift +++ b/Sources/SmithyCodegenCore/AST/Node+AST.swift @@ -5,24 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // -/// Contains the value of a Smithy Node, as used in a JSON AST. -/// -/// Smithy node data is basically the same as the data that can be stored in JSON. -/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. -/// -/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values -enum ASTNode { - case object([String: ASTNode]) - case list([ASTNode]) - case string(String) - case number(Double) - case boolean(Bool) - case null -} +import enum Smithy.Node -extension ASTNode: Decodable { +extension Node: Decodable { - init(from decoder: any Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null @@ -34,9 +21,9 @@ extension ASTNode: Decodable { self = .number(double) } else if let string = try? container.decode(String.self) { self = .string(string) - } else if let array = try? container.decode([ASTNode].self) { + } else if let array = try? container.decode([Node].self) { self = .list(array) - } else if let dictionary = try? container.decode([String: ASTNode].self) { + } else if let dictionary = try? container.decode([String: Node].self) { self = .object(dictionary) } else { throw ASTError("Undecodable value in AST node") diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 24f7b056c..68db4cf42 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -27,8 +27,14 @@ public struct CodeGenerator { let modelData = try Data(contentsOf: modelFileURL) let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) - // In the future, AST will be used to create a Model. - // Model will be used to generate code. + // Create the model from the AST + let model = try Model(astModel: astModel) + + // Create a generation context from the model + let _ = try GenerationContext(model: model) + + // Generation context will be used here in the future + // to generate needed files. // This code simply writes an empty schemas file, since it is expected to exist after the // code generator plugin runs. diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift new file mode 100644 index 000000000..8cc9626d6 --- /dev/null +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -0,0 +1,27 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct GenerationContext { + public let service: ServiceShape + public let model: Model + public let symbolProvider: SymbolProvider + + /// Creates a ``GenerationContext`` from a model. + /// + /// The model must contain exactly one service. + /// - Parameter model: The ``Model`` to create the generation context from. + /// - Throws: ``ModelError`` if the model does not contain exactly one service. + init(model: Model) throws { + let services = model.shapes.values.filter { $0.type == .service } + guard services.count == 1, let service = services.first as? ServiceShape else { + throw ModelError("Model contains \(services.count) services") + } + self.service = service + self.model = model + self.symbolProvider = SymbolProvider(service: service, model: model) + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift new file mode 100644 index 000000000..5154a0eea --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -0,0 +1,162 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +extension Model { + + /// Creates a Smithy model from a JSON AST 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 be 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 + .flatMap { try Self.memberShapePairs(id: $0.key, astShape: $0.value) } + let memberShapes = Dictionary(uniqueKeysWithValues: idToMemberShapePairs) + + // Get all of the non-members from the AST model, create pairs of ShapeID & various shape subclasses + let idToShapePairs = try astModel.shapes + .map { try Self.shapePair(id: $0.key, astShape: $0.value, memberShapes: memberShapes) } + + // Combine all shapes (member & nonmember) into one large Dict for inclusion in the model + let shapes = Dictionary(uniqueKeysWithValues: idToShapePairs + idToMemberShapePairs) + + // Initialize the properties of self + self.init(version: astModel.smithy, metadata: astModel.metadata, shapes: shapes) + + // self is now initialized, set all of the Shapes with references back to this model + self.shapes.values.forEach { $0.model = self } + + // Verify that there is exactly one Service + let services = self.shapes.values.filter { $0.type == .service } + guard services.count == 1 else { throw ModelError("Model has \(services.count) services") } + } + + private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, MemberShape)] { + var baseMembers = (astShape.members ?? [:]) + + // If this AST shape is an array, add a member for its element + if let member = astShape.member { + baseMembers["member"] = member + } + + // If this AST shape is a map, add members for its key & value + if let key = astShape.key { + baseMembers["key"] = key + } + if let value = astShape.value { + baseMembers["value"] = value + } + + // Map the AST members to ShapeID-to-MemberShape pairs & return the list of pairs + return try baseMembers.map { astMember in + // Create a ShapeID for this member + let memberID = ShapeID(id: try ShapeID(id), member: astMember.key) + + // Create traits for this member + let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value) } + let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? []) + + // Create a Shape ID for this member's target + let targetID = try ShapeID(astMember.value.target) + + // Create the ShapeID-to-MemberShape pair + return (memberID, MemberShape(id: memberID, traits: traits, targetID: targetID)) + } + } + + private static func shapePair(id: String, astShape: ASTShape, memberShapes: [ShapeID: MemberShape]) throws -> (ShapeID, Shape) { + // Create the ShapeID for this shape from the AST shape's string ID. + let shapeID = try ShapeID(id) + + // Create model traits from the AST traits. + let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value) } ?? [] + let traits = Dictionary(uniqueKeysWithValues: idToTraitPairs) + + // Based on the AST shape type, create the appropriate Shape type. + switch astShape.type { + case .service: + let shape = ServiceShape( + id: shapeID, + traits: traits, + errorIDs: try astShape.errors?.map { try $0.id } ?? [] + ) + return (shapeID, shape) + case .operation: + let shape = OperationShape( + id: shapeID, + traits: traits, + input: try astShape.input?.id, + output: try astShape.output?.id + ) + return (shapeID, shape) + case .structure: + let shape = StructureShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .union: + let shape = UnionShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .enum: + let shape = EnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .intEnum: + let shape = IntEnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .list, .set: + let shape = ListShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .map: + let shape = MapShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + default: + let shape = Shape( + id: shapeID, + type: try astShape.type.modelType, + traits: traits + ) + + // Return the ShapeID-to-Shape pair. + return (shapeID, shape) + } + } + + private static func memberIDs(for shapeID: ShapeID, memberShapes: [ShapeID: MemberShape]) -> [ShapeID] { + // Given all the member shapes in this model, select the ones for the passed shape ID + // and return their IDs in sorted order. + memberShapes.keys.filter { + $0.namespace == shapeID.namespace && $0.name == shapeID.name && $0.member != nil + }.sorted() + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift new file mode 100644 index 000000000..2ee603c47 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +public class Model { + public let version: String + public let metadata: Node? + public let shapes: [ShapeID: Shape] + + init(version: String, metadata: Node?, shapes: [ShapeID: Shape]) { + self.version = version + self.metadata = metadata + self.shapes = shapes + } + + func expectShape(id: ShapeID) throws -> Shape { + guard let shape = shapes[id] else { + throw ModelError("ShapeID \(id) was expected in model but not found") + } + return shape + } +} diff --git a/Sources/SmithyCodegenCore/Model/ModelError.swift b/Sources/SmithyCodegenCore/Model/ModelError.swift new file mode 100644 index 000000000..97d45fdb5 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ModelError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ModelError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift new file mode 100644 index 000000000..e6d67a6a7 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension ASTReference { + + var id: ShapeID { + get throws { + return try ShapeID(target) + } + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift new file mode 100644 index 000000000..3f8332c5b --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -0,0 +1,68 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.ShapeType + +extension ASTType { + + var modelType: ShapeType { + get throws { + switch self { + case .blob: + return .blob + case .boolean: + return .boolean + case .string: + return .string + case .timestamp: + return .timestamp + case .byte: + return .byte + case .short: + return .short + case .integer: + return .integer + case .long: + return .long + case .float: + return .float + case .document: + return .document + case .double: + return .double + case .bigDecimal: + return .bigDecimal + case .bigInteger: + return .bigInteger + case .`enum`: + return .`enum` + case .intEnum: + return .intEnum + case .list: + return .list + case .set: + return .set + case .map: + return .map + case .structure: + return .structure + case .union: + return .union + case .member: + return .member + case .service: + return .service + case .resource: + return .resource + case .operation: + return .operation + case .apply: + throw ModelError("\"apply\" AST shapes not implemented") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift new file mode 100644 index 000000000..8440ab2a9 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/EnumShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy enums. +public class EnumShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .enum, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/HasMembers.swift b/Sources/SmithyCodegenCore/Shape/HasMembers.swift new file mode 100644 index 000000000..dd2a32e3d --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/HasMembers.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Protocol provided as a convenience to get members from Shapes that have them. +protocol HasMembers { + var members: [MemberShape] { get } +} diff --git a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift new file mode 100644 index 000000000..51e03be42 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy intEnums. +public class IntEnumShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .intEnum, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ListShape.swift b/Sources/SmithyCodegenCore/Shape/ListShape.swift new file mode 100644 index 000000000..d5f6717e2 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ListShape.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy lists. +public class ListShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .list, traits: traits) + } + + public var member: MemberShape { + model.shapes[.init(id: id, member: "member")]! as! MemberShape + } + + public var members: [MemberShape] { + [member] + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/MapShape.swift b/Sources/SmithyCodegenCore/Shape/MapShape.swift new file mode 100644 index 000000000..1d6d0728c --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/MapShape.swift @@ -0,0 +1,36 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy maps. +public class MapShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .list, traits: traits) + } + + public var key: MemberShape { + model.shapes[.init(id: id, member: "key")]! as! MemberShape + } + + public var value: MemberShape { + model.shapes[.init(id: id, member: "value")]! as! MemberShape + } + + public var members: [MemberShape] { + return [key, value] + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/MemberShape.swift b/Sources/SmithyCodegenCore/Shape/MemberShape.swift new file mode 100644 index 000000000..31ab9c551 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/MemberShape.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +/// A ``Shape`` subclass specialized for Smithy members. +public class MemberShape: Shape { + let targetID: ShapeID + + init(id: ShapeID, traits: [ShapeID : Node], targetID: ShapeID) { + self.targetID = targetID + super.init(id: id, type: .member, traits: traits) + } + + public var target: Shape { + return model.shapes[targetID] ?? Shape.prelude[targetID]! + } +} diff --git a/Sources/SmithyCodegenCore/Shape/OperationShape.swift b/Sources/SmithyCodegenCore/Shape/OperationShape.swift new file mode 100644 index 000000000..53a582b2f --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/OperationShape.swift @@ -0,0 +1,46 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy operations. +class OperationShape: Shape { + let inputShapeID: ShapeID? + let outputShapeID: ShapeID? + + public init(id: ShapeID, traits: [ShapeID: Node], input: ShapeID?, output: ShapeID?) { + self.inputShapeID = input + self.outputShapeID = output + super.init(id: id, type: .operation, traits: traits) + } + + public var input: Shape { + if let inputShapeID { + return model.shapes[inputShapeID]!.adding(traits: [.init("smithy.api", "input"): [:]]) + } else { + let traits: [ShapeID: Node] = [ + .init("smithy.api", "input"): [:], + .init("swift.synthetic", "operationName"): .string(id.id), + ] + return Shape.unit.adding(traits: traits) + } + } + + public var output: Shape { + if let outputShapeID { + return model.shapes[outputShapeID]!.adding(traits: [.init("smithy.api", "output"): [:]]) + } else { + let traits: [ShapeID: Node] = [ + .init("smithy.api", "input"): [:], + .init("swift.synthetic", "operationName"): .string(id.id), + ] + return Shape.unit.adding(traits: traits) + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift new file mode 100644 index 000000000..179419e69 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +/// A ``Shape`` subclass specialized for Smithy services. +public class ServiceShape: Shape { + let errorIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID : Node], errorIDs: [ShapeID]) { + self.errorIDs = errorIDs + super.init(id: id, type: .service, traits: traits) + } + + public var errors: [Shape] { + errorIDs.compactMap { model.shapes[$0] } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift new file mode 100644 index 000000000..9d5635159 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift @@ -0,0 +1,128 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Prelude +import struct Smithy.ShapeID + +extension Shape { + + static var prelude: [ShapeID: Shape] {[ + unit.id: unit, + boolean.id: boolean, + string.id: string, + integer.id: integer, + blob.id: blob, + timestamp.id: timestamp, + byte.id: byte, + short.id: short, + long.id: long, + float.id: float, + double.id: double, + document.id: document, + primitiveBoolean.id: primitiveBoolean, + primitiveInteger.id: primitiveInteger, + primitiveByte.id: primitiveByte, + primitiveLong.id: primitiveLong, + primitiveFloat.id: primitiveFloat, + primitiveDouble.id: primitiveDouble, + ]} + + static var unit: Shape { + let schema = Smithy.Prelude.unitSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var boolean: Shape { + let schema = Smithy.Prelude.booleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var string: Shape { + let schema = Smithy.Prelude.stringSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var integer: Shape { + let schema = Smithy.Prelude.integerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var blob: Shape { + let schema = Smithy.Prelude.blobSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var timestamp: Shape { + let schema = Smithy.Prelude.timestampSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var byte: Shape { + let schema = Smithy.Prelude.byteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var short: Shape { + let schema = Smithy.Prelude.shortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var long: Shape { + let schema = Smithy.Prelude.longSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var float: Shape { + let schema = Smithy.Prelude.floatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var double: Shape { + let schema = Smithy.Prelude.doubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var document: Shape { + let schema = Smithy.Prelude.documentSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveBoolean: Shape { + let schema = Smithy.Prelude.primitiveBooleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveInteger: Shape { + let schema = Smithy.Prelude.primitiveIntegerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveByte: Shape { + let schema = Smithy.Prelude.primitiveByteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveShort: Shape { + let schema = Smithy.Prelude.primitiveShortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveLong: Shape { + let schema = Smithy.Prelude.primitiveLongSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveFloat: Shape { + let schema = Smithy.Prelude.primitiveFloatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveDouble: Shape { + let schema = Smithy.Prelude.primitiveDoubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/Shape.swift b/Sources/SmithyCodegenCore/Shape/Shape.swift new file mode 100644 index 000000000..35f541bec --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -0,0 +1,78 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import enum Smithy.Node + +public class Shape { + public let id: ShapeID + public let type: ShapeType + public let traits: [ShapeID: Node] + weak var model: Model! + + public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node]) { + self.id = id + self.type = type + self.traits = traits + } + + public func hasTrait(_ traitID: ShapeID) -> Bool { + traits[traitID] != nil + } + + public func getTrait(_ traitID: ShapeID) -> Node? { + traits[traitID] + } + + public func adding(traits newTraits: [ShapeID: Node]) -> Shape { + let combinedTraits = traits.merging(newTraits) { _, new in new } + let new = Shape(id: id, type: type, traits: combinedTraits) + new.model = model + return new + } + + public var descendants: Set { + var c = Set() + descendants(&c) + return c + } + + private func descendants(_ descendants: inout Set) { + let shapes = candidates(for: self) + for shape in shapes { + if descendants.contains(shape) { continue } + descendants.insert(shape) + shape.descendants(&descendants) + } + } + + func candidates(for shape: Shape) -> [Shape] { + [] // default. May be overridden by Shape subclasses. + } +} + +extension Shape: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Shape: Equatable { + + public static func == (lhs: Shape, rhs: Shape) -> Bool { + lhs.id == rhs.id + } +} + +extension Shape: Comparable { + + public static func < (lhs: Shape, rhs: Shape) -> Bool { + lhs.id < rhs.id + } +} diff --git a/Sources/SmithyCodegenCore/Shape/StructureShape.swift b/Sources/SmithyCodegenCore/Shape/StructureShape.swift new file mode 100644 index 000000000..2f5f33340 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/StructureShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy structures. +public class StructureShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .structure, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/UnionShape.swift b/Sources/SmithyCodegenCore/Shape/UnionShape.swift new file mode 100644 index 000000000..7668563fb --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/UnionShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy unions. +public class UnionShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .union, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift new file mode 100644 index 000000000..4eca55b5b --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -0,0 +1,129 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.NSRange +import class Foundation.NSRegularExpression +import struct Smithy.ShapeID + +public struct SymbolProvider { + let service: ServiceShape + let model: Model + + init(service: ServiceShape, model: Model) { + self.service = service + self.model = model + } + + var serviceName: String { + get throws { + guard service.type == .service else { + throw SymbolProviderError("Called serviceName on non-service shape") + } + guard case .object(let serviceInfo) = service.getTrait(.init("aws.api", "service")) else { + throw SymbolProviderError("No service trait on service") + } + guard case .string(let sdkID) = serviceInfo["sdkId"] else { + throw SymbolProviderError("No sdkId on service trait") + } + return sdkID.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "Service", with: "") + } + } + + private var inputTraitID = ShapeID("smithy.api", "input") + private var outputTraitID = ShapeID("smithy.api", "output") + private var errorTraitID = ShapeID("smithy.api", "error") + private var operationNameTraitID = ShapeID("swift.synthetic", "operationName") + + public func swiftType(shape: Shape) throws -> String { + if case .string(let name) = shape.getTrait(operationNameTraitID), shape.hasTrait(inputTraitID) { + return "\(name)Input" + } else if shape.hasTrait(inputTraitID) { + guard let operation = model.shapes.values + .filter({ $0.type == .operation }) + .map({ $0 as! OperationShape }) + .first(where: { $0.inputShapeID == shape.id }) + else { throw SymbolProviderError("Operation for input \(shape.id) not found") } + return "\(operation.id.name)Input" + } else if + case .string(let name) = shape.getTrait(operationNameTraitID), shape.hasTrait(outputTraitID) { + return "\(name)Output" + } else if shape.hasTrait(outputTraitID) { + guard let operation = model.shapes.values + .filter({ $0.type == .operation }) + .map({ $0 as! OperationShape }) + .first(where: { $0.outputShapeID == shape.id }) + else { throw SymbolProviderError("Operation for output \(shape.id) not found") } + return "\(operation.id.name)Output" + } else if shape.hasTrait(errorTraitID) { + return shape.id.name + } else { + return try "\(serviceName)ClientTypes.\(shape.id.name)" + } + } + + public func propertyName(shapeID: ShapeID) throws -> String { + guard let member = shapeID.member else { throw SymbolProviderError("Shape ID has no member name") } + return member.toLowerCamelCase() + } + + public func enumCaseName(shapeID: ShapeID) throws -> String { + try propertyName(shapeID: shapeID).toLowerCamelCase().lowercased() + } +} + +private extension String { + + func toLowerCamelCase() -> String { + let words = splitOnWordBoundaries() // Split into words + let firstWord = words.first!.lowercased() // make first word lowercase + return firstWord + words.dropFirst().joined() // join lowercased first word to remainder + } + + func splitOnWordBoundaries() -> [String] { + // TODO: when nonsupporting platforms are dropped, convert this to Swift-native regex + // adapted from Java v2 SDK CodegenNamingUtils.splitOnWordBoundaries + var result = self + + // all non-alphanumeric characters: "acm-success"-> "acm success" + let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") + result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" + let underscoreRegex = try! NSRegularExpression(pattern: "_") + result = underscoreRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " _ ") + + // if a number has a standalone v or V in front of it, separate it out + let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") + result = smallVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 v$2") + + let largeVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})V([0-9]+)") + result = largeVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 V$2") + + // add a space between camelCased words + let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") + result = camelCaseSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // add a space after acronyms + let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") + result = acronymSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // add space after a number in the middle of a word + let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") + result = spaceAfterNumberRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // remove extra spaces - multiple consecutive ones or those and the beginning/end of words + let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") + result = removeExtraSpaceRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + .trimmingCharacters(in: .whitespaces) + + return result.components(separatedBy: " ") + } + + var range: NSRange { + NSRange(location: 0, length: count) + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift new file mode 100644 index 000000000..2d17de6bb --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SymbolProviderError: Error { + let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} From f9f6d0d4f58779c97cddabdcc6b24b381e420f06 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 17 Dec 2025 10:54:58 -0600 Subject: [PATCH 14/29] revert schema --- Sources/Smithy/Schema/Schema.swift | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift index f82adc1a7..f8cac05fb 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -/// A class which describes selected, modeled information for a Smithy shape. +/// A class which describes selected Smithy model information for a Smithy model shape. /// /// Typically, the Schema contains only modeled info & properties that are relevant to /// serialization, transport bindings, and other functions performed by the SDK. @@ -29,13 +29,7 @@ public final class Schema: Sendable { public let members: [Schema] /// The target schema for this schema. Will only be used when this is a member schema. - public var target: Schema? { - _target() - } - - /// Target schema is passed as an autoclosure so that schemas with self-referencing targets will not cause - /// an infinite loop when accessed. - private let _target: @Sendable () -> Schema? + public let target: Schema? /// The index of this schema, if it represents a Smithy member. /// @@ -55,14 +49,24 @@ public final class Schema: Sendable { type: ShapeType, traits: [ShapeID: Node] = [:], members: [Schema] = [], - target: @Sendable @escaping @autoclosure () -> Schema? = nil, + target: Schema? = nil, index: Int = -1 ) { self.id = id self.type = type self.traits = traits self.members = members - self._target = target + self.target = target self.index = index } } + +public extension Schema { + + /// The member name for this schema, if any. + /// + /// Member name is computed from the schema's ID. + var memberName: String? { + id.member + } +} From 268c9526a7bb163feb183852aa13f98636e64d28 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 17 Dec 2025 11:20:02 -0600 Subject: [PATCH 15/29] Fix lint --- .../DefaultSDKRuntimeConfiguration.swift | 2 +- .../Networking/Http/CRT/CRTClientEngine.swift | 2 +- .../Http/CRT/HTTP2Stream+ByteStream.swift | 2 +- .../URLSession/FoundationStreamBridge.swift | 4 +-- .../URLSession/URLSessionHTTPClient.swift | 6 ++--- Sources/SmithyCodegenCore/CodeGenerator.swift | 2 +- .../SmithyCodegenCore/GenerationContext.swift | 2 +- .../SmithyCodegenCore/Model/Model+AST.swift | 6 +++-- .../SmithyCodegenCore/Shape/EnumShape.swift | 2 +- .../Shape/IntEnumShape.swift | 2 +- .../SmithyCodegenCore/Shape/ListShape.swift | 1 + .../SmithyCodegenCore/Shape/MapShape.swift | 4 +-- .../SmithyCodegenCore/Shape/MemberShape.swift | 2 +- .../Shape/ServiceShape.swift | 2 +- Sources/SmithyCodegenCore/Shape/Shape.swift | 2 +- .../Shape/StructureShape.swift | 2 +- .../SmithyCodegenCore/Shape/UnionShape.swift | 2 +- .../SymbolProvider/SymbolProvider.swift | 26 +++++++++++-------- .../SmithySwiftNIO/SwiftNIOHTTPClient.swift | 6 ++--- 19 files changed, 42 insertions(+), 35 deletions(-) diff --git a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift index 4868cc7ee..f6dd0d46a 100644 --- a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift +++ b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift @@ -13,10 +13,10 @@ import protocol SmithyHTTPAuthAPI.AuthSchemeResolver import protocol SmithyHTTPAuthAPI.AuthSchemeResolverParameters import struct SmithyRetries.DefaultRetryStrategy import struct SmithyRetries.ExponentialBackoffStrategy -import SmithyTelemetryAPI import protocol SmithyRetriesAPI.RetryErrorInfoProvider import protocol SmithyRetriesAPI.RetryStrategy import struct SmithyRetriesAPI.RetryStrategyOptions +import SmithyTelemetryAPI public struct DefaultSDKRuntimeConfiguration { diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 1b61a9467..8658bb00d 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -19,8 +19,8 @@ import protocol SmithyHTTPAPI.HTTPClient import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPResponse import enum SmithyHTTPAPI.HTTPStatusCode -import class SmithyHTTPClientAPI.HttpTelemetry import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry import class SmithyStreams.BufferedStream import SmithyTelemetryAPI #if os(Linux) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift index 7d871a63e..492f9cde9 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift @@ -8,8 +8,8 @@ import AwsCommonRuntimeKit import struct Smithy.Attributes import enum Smithy.ByteStream -import class SmithyHTTPClientAPI.HttpTelemetry import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry extension HTTP2Stream { /// Returns the recommended size, in bytes, for the data to write diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift b/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift index a5c420e18..54044b683 100644 --- a/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift +++ b/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift @@ -13,8 +13,6 @@ import class Foundation.DispatchQueue import class Foundation.InputStream import class Foundation.NSObject import class Foundation.OutputStream -import class SmithyHTTPClientAPI.HttpTelemetry -import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys import class Foundation.RunLoop import class Foundation.Stream import protocol Foundation.StreamDelegate @@ -24,6 +22,8 @@ import class Foundation.Timer import struct Smithy.Attributes import protocol Smithy.LogAgent import protocol Smithy.ReadableStream +import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry /// Reads data from a smithy-swift native `ReadableStream` and streams the data through to a Foundation `InputStream`. /// diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift index 2a0c6ea20..aab80dc2a 100644 --- a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift +++ b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift @@ -18,8 +18,6 @@ import class Foundation.NSRecursiveLock import var Foundation.NSURLAuthenticationMethodClientCertificate import var Foundation.NSURLAuthenticationMethodServerTrust import struct Foundation.TimeInterval -import class SmithyHTTPClientAPI.HttpTelemetry -import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys import class Foundation.URLAuthenticationChallenge import struct Foundation.URLComponents import class Foundation.URLCredential @@ -29,7 +27,6 @@ import class Foundation.URLResponse import class Foundation.URLSession import class Foundation.URLSessionConfiguration import protocol Foundation.URLSessionDataDelegate -import SmithyTelemetryAPI import class Foundation.URLSessionDataTask import class Foundation.URLSessionTask import class Foundation.URLSessionTaskMetrics @@ -44,7 +41,10 @@ import protocol SmithyHTTPAPI.HTTPClient import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPResponse import enum SmithyHTTPAPI.HTTPStatusCode +import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry import class SmithyStreams.BufferedStream +import SmithyTelemetryAPI /// A client that can be used to make requests to AWS services using `Foundation`'s `URLSession` HTTP client. /// diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 68db4cf42..51d3eba50 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -31,7 +31,7 @@ public struct CodeGenerator { let model = try Model(astModel: astModel) // Create a generation context from the model - let _ = try GenerationContext(model: model) + _ = try GenerationContext(model: model) // Generation context will be used here in the future // to generate needed files. diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift index 8cc9626d6..4e3d7269e 100644 --- a/Sources/SmithyCodegenCore/GenerationContext.swift +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -9,7 +9,7 @@ public struct GenerationContext { public let service: ServiceShape public let model: Model public let symbolProvider: SymbolProvider - + /// Creates a ``GenerationContext`` from a model. /// /// The model must contain exactly one service. diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift index 5154a0eea..74264c1df 100644 --- a/Sources/SmithyCodegenCore/Model/Model+AST.swift +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -9,7 +9,7 @@ import enum Smithy.Node import struct Smithy.ShapeID extension Model { - + /// Creates a Smithy model from a JSON AST model. /// /// Compared to the AST model, this model has custom shape types, members are included in the main body of shapes @@ -73,7 +73,9 @@ extension Model { } } - private static func shapePair(id: String, astShape: ASTShape, memberShapes: [ShapeID: MemberShape]) throws -> (ShapeID, Shape) { + private static func shapePair( + id: String, astShape: ASTShape, memberShapes: [ShapeID: MemberShape] + ) throws -> (ShapeID, Shape) { // Create the ShapeID for this shape from the AST shape's string ID. let shapeID = try ShapeID(id) diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift index 8440ab2a9..b21948702 100644 --- a/Sources/SmithyCodegenCore/Shape/EnumShape.swift +++ b/Sources/SmithyCodegenCore/Shape/EnumShape.swift @@ -19,7 +19,7 @@ public class EnumShape: Shape, HasMembers { } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } + return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast } override func candidates(for shape: Shape) -> [Shape] { diff --git a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift index 51e03be42..43703dc00 100644 --- a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift +++ b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift @@ -19,7 +19,7 @@ public class IntEnumShape: Shape, HasMembers { } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } + return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast } override func candidates(for shape: Shape) -> [Shape] { diff --git a/Sources/SmithyCodegenCore/Shape/ListShape.swift b/Sources/SmithyCodegenCore/Shape/ListShape.swift index d5f6717e2..4dd331cf5 100644 --- a/Sources/SmithyCodegenCore/Shape/ListShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ListShape.swift @@ -19,6 +19,7 @@ public class ListShape: Shape, HasMembers { } public var member: MemberShape { + // swiftlint:disable:next force_cast model.shapes[.init(id: id, member: "member")]! as! MemberShape } diff --git a/Sources/SmithyCodegenCore/Shape/MapShape.swift b/Sources/SmithyCodegenCore/Shape/MapShape.swift index 1d6d0728c..fa6d9bc36 100644 --- a/Sources/SmithyCodegenCore/Shape/MapShape.swift +++ b/Sources/SmithyCodegenCore/Shape/MapShape.swift @@ -19,11 +19,11 @@ public class MapShape: Shape, HasMembers { } public var key: MemberShape { - model.shapes[.init(id: id, member: "key")]! as! MemberShape + model.shapes[.init(id: id, member: "key")]! as! MemberShape // swiftlint:disable:this force_cast } public var value: MemberShape { - model.shapes[.init(id: id, member: "value")]! as! MemberShape + model.shapes[.init(id: id, member: "value")]! as! MemberShape // swiftlint:disable:this force_cast } public var members: [MemberShape] { diff --git a/Sources/SmithyCodegenCore/Shape/MemberShape.swift b/Sources/SmithyCodegenCore/Shape/MemberShape.swift index 31ab9c551..29abb70ca 100644 --- a/Sources/SmithyCodegenCore/Shape/MemberShape.swift +++ b/Sources/SmithyCodegenCore/Shape/MemberShape.swift @@ -12,7 +12,7 @@ import struct Smithy.ShapeID public class MemberShape: Shape { let targetID: ShapeID - init(id: ShapeID, traits: [ShapeID : Node], targetID: ShapeID) { + init(id: ShapeID, traits: [ShapeID: Node], targetID: ShapeID) { self.targetID = targetID super.init(id: id, type: .member, traits: traits) } diff --git a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift index 179419e69..101257cfd 100644 --- a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift @@ -12,7 +12,7 @@ import struct Smithy.ShapeID public class ServiceShape: Shape { let errorIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID : Node], errorIDs: [ShapeID]) { + public init(id: ShapeID, traits: [ShapeID: Node], errorIDs: [ShapeID]) { self.errorIDs = errorIDs super.init(id: id, type: .service, traits: traits) } diff --git a/Sources/SmithyCodegenCore/Shape/Shape.swift b/Sources/SmithyCodegenCore/Shape/Shape.swift index 35f541bec..d955faf05 100644 --- a/Sources/SmithyCodegenCore/Shape/Shape.swift +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -5,9 +5,9 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType -import enum Smithy.Node public class Shape { public let id: ShapeID diff --git a/Sources/SmithyCodegenCore/Shape/StructureShape.swift b/Sources/SmithyCodegenCore/Shape/StructureShape.swift index 2f5f33340..122b795d8 100644 --- a/Sources/SmithyCodegenCore/Shape/StructureShape.swift +++ b/Sources/SmithyCodegenCore/Shape/StructureShape.swift @@ -19,7 +19,7 @@ public class StructureShape: Shape, HasMembers { } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } + return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast } override func candidates(for shape: Shape) -> [Shape] { diff --git a/Sources/SmithyCodegenCore/Shape/UnionShape.swift b/Sources/SmithyCodegenCore/Shape/UnionShape.swift index 7668563fb..f6b7ba3e5 100644 --- a/Sources/SmithyCodegenCore/Shape/UnionShape.swift +++ b/Sources/SmithyCodegenCore/Shape/UnionShape.swift @@ -19,7 +19,7 @@ public class UnionShape: Shape, HasMembers { } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } + return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast } override func candidates(for shape: Shape) -> [Shape] { diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift index 4eca55b5b..9ce726e89 100644 --- a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -44,7 +44,7 @@ public struct SymbolProvider { } else if shape.hasTrait(inputTraitID) { guard let operation = model.shapes.values .filter({ $0.type == .operation }) - .map({ $0 as! OperationShape }) + .map({ $0 as! OperationShape }) // swiftlint:disable:this force_cast .first(where: { $0.inputShapeID == shape.id }) else { throw SymbolProviderError("Operation for input \(shape.id) not found") } return "\(operation.id.name)Input" @@ -54,7 +54,7 @@ public struct SymbolProvider { } else if shape.hasTrait(outputTraitID) { guard let operation = model.shapes.values .filter({ $0.type == .operation }) - .map({ $0 as! OperationShape }) + .map({ $0 as! OperationShape }) // swiftlint:disable:this force_cast .first(where: { $0.outputShapeID == shape.id }) else { throw SymbolProviderError("Operation for output \(shape.id) not found") } return "\(operation.id.name)Output" @@ -89,34 +89,25 @@ private extension String { var result = self // all non-alphanumeric characters: "acm-success"-> "acm success" - let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" - let underscoreRegex = try! NSRegularExpression(pattern: "_") result = underscoreRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " _ ") // if a number has a standalone v or V in front of it, separate it out - let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") result = smallVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 v$2") - - let largeVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})V([0-9]+)") result = largeVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 V$2") // add a space between camelCased words - let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") result = camelCaseSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") // add a space after acronyms - let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") result = acronymSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") // add space after a number in the middle of a word - let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") result = spaceAfterNumberRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") // remove extra spaces - multiple consecutive ones or those and the beginning/end of words - let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") result = removeExtraSpaceRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") .trimmingCharacters(in: .whitespaces) @@ -127,3 +118,16 @@ private extension String { NSRange(location: 0, length: count) } } + +// Regexes used in splitOnWordBoundaries() above. +// force_try linter rule is disabled since these are just created from static strings. +// swiftlint:disable force_try +private let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") +private let underscoreRegex = try! NSRegularExpression(pattern: "_") +private let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") +private let largeVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})V([0-9]+)") +private let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") +private let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") +private let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") +private let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") +// swiftlint:enable force_try diff --git a/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift b/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift index 69761212c..0dde0b521 100644 --- a/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift +++ b/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift @@ -6,6 +6,9 @@ // import AsyncHTTPClient +import struct Foundation.Date +import struct Foundation.URLComponents +import struct Foundation.URLQueryItem import NIOCore import NIOHTTP1 import NIOPosix @@ -15,9 +18,6 @@ import SmithyHTTPAPI import SmithyHTTPClientAPI import SmithyStreams import SmithyTelemetryAPI -import struct Foundation.Date -import struct Foundation.URLComponents -import struct Foundation.URLQueryItem /// AsyncHTTPClient-based HTTP client implementation that conforms to SmithyHTTPAPI.HTTPClient /// This implementation is thread-safe and supports concurrent request execution. From d89742a4a6335fd246e9853c4c7bd4e23bf2e15e Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 17 Dec 2025 11:27:59 -0600 Subject: [PATCH 16/29] Mode ShapeID --- Sources/Smithy/{ => Schema}/ShapeID.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/Smithy/{ => Schema}/ShapeID.swift (100%) diff --git a/Sources/Smithy/ShapeID.swift b/Sources/Smithy/Schema/ShapeID.swift similarity index 100% rename from Sources/Smithy/ShapeID.swift rename to Sources/Smithy/Schema/ShapeID.swift From ed8cc785d837cfb9054e8370ee6ac1ac6ee46592 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 19 Jan 2026 15:39:55 -0600 Subject: [PATCH 17/29] Add full-featured code generator --- Package.swift | 14 +- .../SmithyCodeGeneratorPlugin.swift | 19 +- .../Implementations/BigIntegerDocument.swift | 8 +- Sources/Smithy/{Schema => }/Node.swift | 2 +- Sources/Smithy/Schema/Prelude.swift | 2 +- Sources/Smithy/Schema/Schema.swift | 51 +- Sources/Smithy/{Schema => }/ShapeID.swift | 17 +- Sources/Smithy/Trait/Trait.swift | 19 + Sources/Smithy/Trait/TraitCollection.swift | 65 + Sources/Smithy/Trait/TraitError.swift | 14 + .../AWSQueryCompatibleTrait.swift | 14 + .../TraitLibrary/AWSQueryErrorTrait.swift | 29 + .../TraitLibrary/AddedDefaultTrait.swift | 14 + .../TraitLibrary/AllSupportedTraits.swift | 28 + .../TraitLibrary/ClientOptionalTrait.swift | 16 + .../Smithy/TraitLibrary/DefaultTrait.swift | 16 + Sources/Smithy/TraitLibrary/ErrorTrait.swift | 14 + Sources/Smithy/TraitLibrary/InputTrait.swift | 16 + Sources/Smithy/TraitLibrary/OutputTrait.swift | 16 + .../Smithy/TraitLibrary/SensitiveTrait.swift | 14 + .../Smithy/TraitLibrary/ServiceTrait.swift | 24 + Sources/Smithy/TraitLibrary/SparseTrait.swift | 14 + .../TraitLibrary/TargetsUnitTrait.swift | 16 + .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 38 +- Sources/SmithyCodegenCore/AST/ASTShape.swift | 2 + Sources/SmithyCodegenCore/CodeGenerator.swift | 23 +- Sources/SmithyCodegenCore/CodegenError.swift | 14 + .../SmithyCodegenCore/GenerationContext.swift | 27 +- Sources/SmithyCodegenCore/HasShapeID.swift | 30 + .../SmithyCodegenCore/Model/Model+AST.swift | 65 +- Sources/SmithyCodegenCore/Model/Model.swift | 54 + .../ModelTransformer/Model+Box.swift | 51 + .../ModelTransformer/Model+Deprecated.swift | 116 + .../ModelTransformer/Model+InputOutput.swift | 95 + .../Model+PruneToService.swift | 28 + .../ModelTransformer/Model+Union.swift | 44 + .../NullableIndex/NullableIndex.swift | 76 + .../Resources/DefaultSwiftHeader.txt | 8 + .../Schemas/SchemasCodegen.swift | 108 + .../Schemas/Shape+Schema.swift | 61 + .../SmithyCodegenCore/Shape/EnumShape.swift | 16 +- .../SmithyCodegenCore/Shape/HasMembers.swift | 2 +- .../Shape/IntEnumShape.swift | 11 +- .../SmithyCodegenCore/Shape/ListShape.swift | 18 +- .../SmithyCodegenCore/Shape/MapShape.swift | 25 +- .../SmithyCodegenCore/Shape/MemberShape.swift | 23 +- .../Shape/OperationShape.swift | 53 +- .../Shape/ResourceShape.swift | 46 + .../Shape/ServiceShape.swift | 64 +- Sources/SmithyCodegenCore/Shape/Shape.swift | 66 +- .../Shape/StructureShape.swift | 11 +- .../SmithyCodegenCore/Shape/UnionShape.swift | 11 +- .../SwiftRendering/Node+Rendered.swift | 32 + .../SwiftRendering/String+Utils.swift | 32 + .../SwiftRendering/SwiftWriter.swift | 74 + .../SymbolProvider/String+ReservedWords.swift | 125 + .../SymbolProvider/SymbolProvider.swift | 129 +- .../TraitLibrary/DeprecatedTrait.swift | 26 + .../TraitLibrary/EnumTrait.swift | 43 + .../TraitLibrary/StreamingTrait.swift | 18 + .../TraitLibrary/UsedAsInputTrait.swift | 20 + .../TraitLibrary/UsedAsOutputTrait.swift | 20 + .../Resources/smithy-rpcv2-cbor.json | 3622 +++++++++++++++++ .../SmithyCodegenCoreTests.swift | 36 + .../swift/codegen/DirectedSwiftCodegen.kt | 4 +- .../codegen/SmithyModelFileInfoGenerator.kt | 21 +- .../smithy/swift/codegen/SwiftSettings.kt | 31 +- .../swift/codegen/SwiftSymbolProvider.kt | 4 +- .../HttpProtocolClientGenerator.kt | 5 +- .../integration/HttpProtocolServiceClient.kt | 2 +- .../integration/HttpProtocolTestGenerator.kt | 1 - .../SmithyHTTPBindingProtocolGenerator.kt | 1 - .../swift/codegen/utils/ServiceShapeUtils.kt | 9 + .../codegencomponents/SwiftSettingsTest.kt | 1 + 74 files changed, 5652 insertions(+), 232 deletions(-) rename Sources/Smithy/{Schema => }/Node.swift (98%) rename Sources/Smithy/{Schema => }/ShapeID.swift (86%) create mode 100644 Sources/Smithy/Trait/Trait.swift create mode 100644 Sources/Smithy/Trait/TraitCollection.swift create mode 100644 Sources/Smithy/Trait/TraitError.swift create mode 100644 Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/AllSupportedTraits.swift create mode 100644 Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/DefaultTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/ErrorTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/InputTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/OutputTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/SensitiveTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/ServiceTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/SparseTrait.swift create mode 100644 Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift create mode 100644 Sources/SmithyCodegenCore/CodegenError.swift create mode 100644 Sources/SmithyCodegenCore/HasShapeID.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift create mode 100644 Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift create mode 100644 Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt create mode 100644 Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift create mode 100644 Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift create mode 100644 Sources/SmithyCodegenCore/Shape/ResourceShape.swift create mode 100644 Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift create mode 100644 Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift create mode 100644 Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift create mode 100644 Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift create mode 100644 Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift create mode 100644 Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift create mode 100644 Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift create mode 100644 Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift create mode 100644 Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift create mode 100644 Tests/SmithyCodegenCoreTests/Resources/smithy-rpcv2-cbor.json create mode 100644 Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/ServiceShapeUtils.kt diff --git a/Package.swift b/Package.swift index feefbf16f..8fb28f78b 100644 --- a/Package.swift +++ b/Package.swift @@ -121,6 +121,7 @@ let package = Package( "SmithyChecksumsAPI", "SmithyChecksums", "SmithyCBOR", + "SmithySerialization", .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"), // Only include these on macOS, iOS, tvOS, watchOS, and macCatalyst (visionOS and Linux are excluded) .product( @@ -285,8 +286,8 @@ let package = Package( .target( name: "SmithyCBOR", dependencies: [ - "SmithyReadWrite", - "SmithyTimestamps", + "Smithy", + "SmithySerialization", .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift") ] ), @@ -311,7 +312,9 @@ let package = Package( name: "SmithyCodegenCore", dependencies: [ "Smithy", - ] + "SmithySerialization", + ], + resources: [ .process("Resources") ] ), .testTarget( name: "ClientRuntimeTests", @@ -400,5 +403,10 @@ let package = Package( name: "SmithyStreamsTests", dependencies: ["SmithyStreams", "Smithy"] ), + .testTarget( + name: "SmithyCodegenCoreTests", + dependencies: ["SmithyCodegenCore"], + resources: [ .process("Resources") ] + ), ].compactMap { $0 } ) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 180e3bb5e..ae41dae07 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -43,13 +43,16 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { let currentWorkingDirectoryFileURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - // Get the smithy model path. + // Get the smithy-model-info.json file contents. 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. + let service = smithyModelInfo.service let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path) let modelPath = Path(modelPathURL.path) - // Construct the schemas.swift path. + // Construct the Schemas.swift path. let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") // Construct the build command that invokes SmithyCodegenCLI. @@ -57,17 +60,23 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", executable: generatorToolPath, arguments: [ - "--schemas-path", schemasSwiftPath, - modelPath + service, + modelPath, + "--schemas-path", schemasSwiftPath ], inputFiles: [inputPath, modelPath], - outputFiles: [schemasSwiftPath] + outputFiles: [ + schemasSwiftPath, + ] ) } } /// Codable 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 path to the model, from the root of the target's project. Required. let path: String } diff --git a/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift b/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift index 6c6d80e4c..ecc3781c9 100644 --- a/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift +++ b/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift @@ -8,9 +8,9 @@ @_spi(SmithyDocumentImpl) public struct BigIntegerDocument: SmithyDocument { public var type: ShapeType { .bigInteger } - let value: Int + let value: Int64 - public init(value: Int) { + public init(value: Int64) { self.value = value } @@ -29,7 +29,7 @@ public struct BigIntegerDocument: SmithyDocument { } public func asInteger() throws -> Int { - value + Int(value) } public func asLong() throws -> Int64 { @@ -45,7 +45,7 @@ public struct BigIntegerDocument: SmithyDocument { } public func asBigInteger() throws -> Int64 { - Int64(value) + value } public func asBigDecimal() throws -> Double { diff --git a/Sources/Smithy/Schema/Node.swift b/Sources/Smithy/Node.swift similarity index 98% rename from Sources/Smithy/Schema/Node.swift rename to Sources/Smithy/Node.swift index ce6c2d64e..721e168c1 100644 --- a/Sources/Smithy/Schema/Node.swift +++ b/Sources/Smithy/Node.swift @@ -11,7 +11,7 @@ /// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. /// /// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values -public enum Node: Sendable { +public enum Node: Sendable, Hashable { case object([String: Node]) case list([Node]) case string(String) diff --git a/Sources/Smithy/Schema/Prelude.swift b/Sources/Smithy/Schema/Prelude.swift index 4658ccfc2..f60f4476a 100644 --- a/Sources/Smithy/Schema/Prelude.swift +++ b/Sources/Smithy/Schema/Prelude.swift @@ -55,7 +55,7 @@ public enum Prelude { } public static var documentSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveDocument"), type: .document) + Schema(id: .init("smithy.api", "Document"), type: .document) } public static var primitiveBooleanSchema: Schema { diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift index f8cac05fb..1471c4e10 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -5,11 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // -/// A class which describes selected Smithy model information for a Smithy model shape. +/// A class which describes selected, modeled information for a Smithy shape. /// /// Typically, the Schema contains only modeled info & properties that are relevant to /// serialization, transport bindings, and other functions performed by the SDK. -public final class Schema: Sendable { +public struct Schema: Sendable { /// The Smithy shape ID for the shape described by this schema. public let id: ShapeID @@ -21,7 +21,7 @@ public final class Schema: Sendable { /// /// Not all traits for a shape will be represented in the schema; /// typically the Schema contains only the traits relevant to the client-side SDK. - public let traits: [ShapeID: Node] + public let traits: TraitCollection /// The member schemas for this schema, if any. /// @@ -29,7 +29,13 @@ public final class Schema: Sendable { public let members: [Schema] /// The target schema for this schema. Will only be used when this is a member schema. - public let target: Schema? + public var target: Schema? { + _target() + } + + /// Target schema is passed as an autoclosure so that schemas with self-referencing targets will not cause + /// an infinite loop when accessed. + private let _target: @Sendable () -> Schema? /// The index of this schema, if it represents a Smithy member. /// @@ -47,26 +53,45 @@ public final class Schema: Sendable { public init( id: ShapeID, type: ShapeType, - traits: [ShapeID: Node] = [:], + traits: TraitCollection = TraitCollection(), members: [Schema] = [], - target: Schema? = nil, + target: @Sendable @escaping @autoclosure () -> Schema? = nil, index: Int = -1 ) { self.id = id self.type = type self.traits = traits self.members = members - self.target = target + self._target = target self.index = index } -} -public extension Schema { + public func hasTrait(_ type: T.Type) -> Bool { + return traits.hasTrait(T.self) + } + + public func getTrait(_ type: T.Type) throws -> T? { + try traits.getTrait(type) + } + + /// Returns the member for a List's element. + /// + /// Only access this property on a schema of type `.list`. + public var member: Schema { + members[0] // `member` will be the only member in a list schema + } + + /// Returns the key member for a Map's key. + /// + /// Only access this property on a schema of type `.map`. + public var key: Schema { + members[0] // `key` will be the first member in a map schema, before `value` + } - /// The member name for this schema, if any. + /// Returns the value member for a Map's value. /// - /// Member name is computed from the schema's ID. - var memberName: String? { - id.member + /// Only access this property on a schema of type `.map`. + public var value: Schema { + members[1] // `value` will be the second / last member in a map schema, after `key` } } diff --git a/Sources/Smithy/Schema/ShapeID.swift b/Sources/Smithy/ShapeID.swift similarity index 86% rename from Sources/Smithy/Schema/ShapeID.swift rename to Sources/Smithy/ShapeID.swift index a429b0fed..febb0f84e 100644 --- a/Sources/Smithy/Schema/ShapeID.swift +++ b/Sources/Smithy/ShapeID.swift @@ -53,32 +53,37 @@ public struct ShapeID: Hashable, Sendable { } } - public init(id: ShapeID, member: String) { + public init(id: ShapeID, member: String?) { self.namespace = id.namespace self.name = id.name self.member = member } - public var id: String { + public var absoluteID: String { + return "\(namespace)#\(relativeID)" + } + + public var relativeID: String { if let member { - return "\(namespace)#\(name)$\(member)" + return "\(name)$\(member)" } else { - return "\(namespace)#\(name)" + return "\(name)" } } } extension ShapeID: Comparable { + // This logic matches the sorting logic used by the Java-based codegen public static func < (lhs: ShapeID, rhs: ShapeID) -> Bool { - return lhs.id.lowercased() < rhs.id.lowercased() + lhs.absoluteID.lowercased() < rhs.absoluteID.lowercased() } } extension ShapeID: CustomStringConvertible { /// Returns the absolute Shape ID in a single, printable string. - public var description: String { id } + public var description: String { absoluteID } } public struct ShapeIDError: Error { diff --git a/Sources/Smithy/Trait/Trait.swift b/Sources/Smithy/Trait/Trait.swift new file mode 100644 index 000000000..42218a3ac --- /dev/null +++ b/Sources/Smithy/Trait/Trait.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol Trait { + static var id: ShapeID { get } + + var node: Node { get } + + init(node: Node) throws +} + +public extension Trait { + + var id: ShapeID { Self.id } +} diff --git a/Sources/Smithy/Trait/TraitCollection.swift b/Sources/Smithy/Trait/TraitCollection.swift new file mode 100644 index 000000000..5693af893 --- /dev/null +++ b/Sources/Smithy/Trait/TraitCollection.swift @@ -0,0 +1,65 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct TraitCollection: Sendable, Hashable { + public var traitDict: [ShapeID: Node] + + public init() { + self.traitDict = [:] + } + + public init(traits: [ShapeID: Node]) { + self.traitDict = traits + } + + public var isEmpty: Bool { + traitDict.isEmpty + } + + public var count: Int { + traitDict.count + } + + public func hasTrait(_ type: T.Type) -> Bool { + traitDict[T.id] != nil + } + + public func hasTrait(_ id: ShapeID) -> Bool { + traitDict[id] != nil + } + + public func getTrait(_ type: T.Type) throws -> T? { + guard let node = traitDict[T.id] else { return nil } + return try T(node: node) + } + + public mutating func add(_ trait: Trait) { + traitDict[trait.id] = trait.node + } + + public func adding(_ other: TraitCollection) -> TraitCollection { + let combined = self.traitDict.merging(other.traitDict) { _, new in new } + return TraitCollection(traits: combined) + } +} + +extension TraitCollection: ExpressibleByDictionaryLiteral { + public typealias Key = ShapeID + public typealias Value = Node + + public init(dictionaryLiteral elements: (Key, Value)...) { + self.traitDict = Dictionary(uniqueKeysWithValues: elements) + } +} + +extension TraitCollection: ExpressibleByArrayLiteral { + public typealias ArrayLiteralElement = any Trait + + public init(arrayLiteral elements: ArrayLiteralElement...) { + self.init(traits: Dictionary(uniqueKeysWithValues: elements.map { ($0.id, $0.node) })) + } +} diff --git a/Sources/Smithy/Trait/TraitError.swift b/Sources/Smithy/Trait/TraitError.swift new file mode 100644 index 000000000..93b6ce72c --- /dev/null +++ b/Sources/Smithy/Trait/TraitError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct TraitError: Error { + public let localizedDescription: String + + public init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift b/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift new file mode 100644 index 000000000..ca8b8381e --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct AWSQueryCompatibleTrait: Trait { + public static var id: ShapeID { .init("aws.protocols", "awsQueryCompatible") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift b/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift new file mode 100644 index 000000000..5d42b727e --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct AWSQueryErrorTrait: Trait { + public static var id: ShapeID { .init("aws.protocols", "awsQueryError") } + + public let node: Node + public let code: String + public let httpResponseCode: Int + + public init(node: Node) throws { + guard case .object(let object) = node else { + throw TraitError("AWSQueryError trait does not have root object") + } + guard case .string(let code) = object["code"] else { + throw TraitError("AWSQueryError trait does not have code") + } + guard case .number(let httpResponseCode) = object["httpResponseCode"] else { + throw TraitError("AWSQueryError trait does not have httpResponseCode") + } + self.node = node + self.code = code + self.httpResponseCode = Int(httpResponseCode) + } +} diff --git a/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift b/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift new file mode 100644 index 000000000..7fb7ad979 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct AddedDefaultTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "addedDefault") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift new file mode 100644 index 000000000..0989566db --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// The trait IDs that should be copied into schemas. Other traits are omitted for brevity. +/// +/// This list can be expanded as features are added to Smithy/SDK that use them. +public let allSupportedTraits = Set([ + + // Traits defined in Smithy + ServiceTrait.id, + AddedDefaultTrait.id, + AWSQueryCompatibleTrait.id, + AWSQueryErrorTrait.id, + SparseTrait.id, + ClientOptionalTrait.id, + InputTrait.id, + OutputTrait.id, + ErrorTrait.id, + DefaultTrait.id, + SensitiveTrait.id, + + // Synthetic traits + TargetsUnitTrait.id, +]) diff --git a/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift b/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift new file mode 100644 index 000000000..f1f3bfefd --- /dev/null +++ b/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ClientOptionalTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "clientOptional") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/Smithy/TraitLibrary/DefaultTrait.swift b/Sources/Smithy/TraitLibrary/DefaultTrait.swift new file mode 100644 index 000000000..581776b3b --- /dev/null +++ b/Sources/Smithy/TraitLibrary/DefaultTrait.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct DefaultTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "default") } + + public let node: Node + + public init(node: Node) throws { + self.node = node + } +} diff --git a/Sources/Smithy/TraitLibrary/ErrorTrait.swift b/Sources/Smithy/TraitLibrary/ErrorTrait.swift new file mode 100644 index 000000000..d55a5cc48 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/ErrorTrait.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ErrorTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "error") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/InputTrait.swift b/Sources/Smithy/TraitLibrary/InputTrait.swift new file mode 100644 index 000000000..6e1e178a2 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/InputTrait.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct InputTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "input") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/Smithy/TraitLibrary/OutputTrait.swift b/Sources/Smithy/TraitLibrary/OutputTrait.swift new file mode 100644 index 000000000..f1638db3c --- /dev/null +++ b/Sources/Smithy/TraitLibrary/OutputTrait.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct OutputTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "output") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/Smithy/TraitLibrary/SensitiveTrait.swift b/Sources/Smithy/TraitLibrary/SensitiveTrait.swift new file mode 100644 index 000000000..433c901f8 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/SensitiveTrait.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SensitiveTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "sensitive") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/ServiceTrait.swift b/Sources/Smithy/TraitLibrary/ServiceTrait.swift new file mode 100644 index 000000000..6971f99f7 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/ServiceTrait.swift @@ -0,0 +1,24 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ServiceTrait: Trait { + public static var id: ShapeID { .init("aws.api", "service") } + + public let node: Node + public let sdkId: String + + public init(node: Node) throws { + guard case .object(let object) = node else { + throw TraitError("ServiceTrait does not have root object") + } + guard let sdkId = object["sdkId"]?.string else { + throw TraitError("ServiceTrait does not have sdkId") + } + self.node = node + self.sdkId = sdkId + } +} diff --git a/Sources/Smithy/TraitLibrary/SparseTrait.swift b/Sources/Smithy/TraitLibrary/SparseTrait.swift new file mode 100644 index 000000000..8ebf819d4 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/SparseTrait.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SparseTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "sparse") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift b/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift new file mode 100644 index 000000000..023c94fa6 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct TargetsUnitTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "targetsUnit") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init () {} +} diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index bf0b608c8..73c688caf 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -12,34 +12,44 @@ import struct SmithyCodegenCore.CodeGenerator @main struct SmithyCodegenCLI: AsyncParsableCommand { - @Argument(help: "The full or relative path to the JSON model file.") + @Argument(help: "The shape ID of the service to be code-generated. Must exist in the model file.") + var service: String + + @Argument(help: "The full or relative path to read the JSON AST model input file.") var modelPath: String - @Option(help: "The full or relative path to write the schemas output file.") + @Option(help: "The full or relative path to write the Schemas output file.") var schemasPath: String? - @Option(help: "The full or relative path to write the struct consumers output file.") - var structConsumersPath: String? - func run() async throws { + + let start = Date() + let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() - print("Current working directory: \(currentWorkingDirectoryFileURL.path)") // Create the model file URL let modelFileURL = URL(fileURLWithPath: modelPath, relativeTo: currentWorkingDirectoryFileURL) guard FileManager.default.fileExists(atPath: modelFileURL.path) else { throw SmithyCodegenCLIError(localizedDescription: "no file at model path \(modelFileURL.path)") } - print("Model file path: \(modelFileURL.path)") // If --schemas-path was supplied, create the schema file URL - let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) + let schemasFileURL = resolve(path: schemasPath) // Use resolved file URLs to run code generator try CodeGenerator( + service: service, modelFileURL: modelFileURL, schemasFileURL: schemasFileURL ).run() + + let duration = Date().timeIntervalSince(start) + let secondsDuration = String( + format: "%0.2f", + locale: Locale(identifier: "en_US_POSIX"), + arguments: [duration] + ) + print("Completed generating model \(modelFileURL.lastPathComponent) in \(secondsDuration) sec") } private func currentWorkingDirectoryFileURL() -> URL { @@ -51,15 +61,9 @@ struct SmithyCodegenCLI: AsyncParsableCommand { return URL(fileURLWithPath: currentWorkingDirectoryPath) } - private func resolve(paramName: String, path: String?) -> URL? { - if let path { - let fileURL = URL(fileURLWithPath: path, relativeTo: currentWorkingDirectoryFileURL()) - print("Resolved \(paramName): \(fileURL.path)") - return fileURL - } else { - print("\(paramName) not provided, skipping generation") - return nil - } + private func resolve(path: String?) -> URL? { + guard let path else { return nil } + return URL(fileURLWithPath: path, relativeTo: currentWorkingDirectoryFileURL()) } } diff --git a/Sources/SmithyCodegenCore/AST/ASTShape.swift b/Sources/SmithyCodegenCore/AST/ASTShape.swift index a2ffd146b..93d5ea0da 100644 --- a/Sources/SmithyCodegenCore/AST/ASTShape.swift +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -9,6 +9,8 @@ import enum Smithy.Node // See https://smithy.io/2.0/spec/json-ast.html#json-ast // This Swift type captures fields for all AST shape types +// Mixins are omitted because they are applied by Smithy before +// the AST is generated. struct ASTShape: Decodable { let type: ASTType let traits: [String: Node]? diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 51d3eba50..7ba570fe8 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -6,18 +6,21 @@ // import struct Foundation.Data -import class Foundation.FileManager import class Foundation.JSONDecoder import struct Foundation.URL +import struct Smithy.ShapeID public struct CodeGenerator { + let service: String let modelFileURL: URL let schemasFileURL: URL? public init( + service: String, modelFileURL: URL, schemasFileURL: URL? - ) { + ) throws { + self.service = service self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL } @@ -27,21 +30,19 @@ 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 - _ = try GenerationContext(model: model) - - // Generation context will be used here in the future - // to generate needed files. + let ctx = try GenerationContext(serviceID: serviceID, model: model) - // This code simply writes an empty schemas file, since it is expected to exist after the - // code generator plugin runs. - // - // Actual code generation will be implemented here later. + // If a schemas file URL was provided, generate it if let schemasFileURL { - FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) + let schemasContents = try SchemasCodegen().generate(ctx: ctx) + try Data(schemasContents.utf8).write(to: schemasFileURL) } } } diff --git a/Sources/SmithyCodegenCore/CodegenError.swift b/Sources/SmithyCodegenCore/CodegenError.swift new file mode 100644 index 000000000..42000614d --- /dev/null +++ b/Sources/SmithyCodegenCore/CodegenError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct CodegenError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift index 4e3d7269e..2141831b1 100644 --- a/Sources/SmithyCodegenCore/GenerationContext.swift +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -5,6 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Smithy.ShapeID + public struct GenerationContext { public let service: ServiceShape public let model: Model @@ -12,16 +14,23 @@ public struct GenerationContext { /// Creates a ``GenerationContext`` from a model. /// - /// The model must contain exactly one service. + /// The model must contain a service with the passed service ID. + /// - 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(model: Model) throws { - let services = model.shapes.values.filter { $0.type == .service } - guard services.count == 1, let service = services.first as? ServiceShape else { - throw ModelError("Model contains \(services.count) services") - } - self.service = service - self.model = model - self.symbolProvider = SymbolProvider(service: service, model: model) + init(serviceID: ShapeID, model: Model) throws { + + // Perform model transformations here + let finalModel = try model + .withSynthesizedInputsOutputs() + .withDeprecatedShapesRemoved() + .withUnionsTargetingUnitAdded() + .optionalizeStructMembers(serviceID: serviceID) + .prune(serviceID: serviceID) + + // Initialize using the final, processed model + self.service = try finalModel.expectServiceShape(id: serviceID) + self.model = finalModel + self.symbolProvider = SymbolProvider(service: service, model: finalModel) } } diff --git a/Sources/SmithyCodegenCore/HasShapeID.swift b/Sources/SmithyCodegenCore/HasShapeID.swift new file mode 100644 index 000000000..d83bcf600 --- /dev/null +++ b/Sources/SmithyCodegenCore/HasShapeID.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +protocol HasShapeID { + var id: ShapeID { get } +} + +extension Array where Element: HasShapeID { + + /// Sorts alphabetically by shape ID to match the ordering used by Kotlin-based codegen. + /// + /// The comparator on `ShapeID` implements the actual comparison logic. + /// - Returns: An array of elements in sorted order + func smithySorted() -> Self { + sorted { $0.id < $1.id } + } +} + +extension ShapeID: HasShapeID { + + var id: ShapeID { + self + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift index 74264c1df..c81461a01 100644 --- a/Sources/SmithyCodegenCore/Model/Model+AST.swift +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -6,7 +6,9 @@ // import enum Smithy.Node +import enum Smithy.Prelude import struct Smithy.ShapeID +import struct Smithy.TraitCollection extension Model { @@ -15,7 +17,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 be created. + /// - 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 @@ -31,17 +33,10 @@ extension Model { // Initialize the properties of self self.init(version: astModel.smithy, metadata: astModel.metadata, shapes: shapes) - - // self is now initialized, set all of the Shapes with references back to this model - self.shapes.values.forEach { $0.model = self } - - // Verify that there is exactly one Service - let services = self.shapes.values.filter { $0.type == .service } - guard services.count == 1 else { throw ModelError("Model has \(services.count) services") } } private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, MemberShape)] { - var baseMembers = (astShape.members ?? [:]) + var baseMembers = astShape.members ?? [:] // If this AST shape is an array, add a member for its element if let member = astShape.member { @@ -56,6 +51,21 @@ extension Model { baseMembers["value"] = value } + // If this shape is a string with the enum trait, add members for its trait members + if astShape.type == .string, let enumTraitNode = astShape.traits?[EnumTrait.id.absoluteID] { + let enumTrait = try EnumTrait(node: enumTraitNode) + let unitID = Smithy.Prelude.unitSchema.id.absoluteID + enumTrait.members.forEach { enumMember in + let name = enumMember.name ?? enumMember.value + let traits: [String: Node] = if enumMember.name != nil { + ["smithy.api#enumValue": .string(enumMember.value)] + } else { + [:] + } + baseMembers[name] = ASTMember(target: unitID, traits: traits) + } + } + // Map the AST members to ShapeID-to-MemberShape pairs & return the list of pairs return try baseMembers.map { astMember in // Create a ShapeID for this member @@ -63,7 +73,8 @@ extension Model { // Create traits for this member let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value) } - let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? []) + let traitDict = Dictionary(uniqueKeysWithValues: traitPairs ?? []) + let traits = TraitCollection(traits: traitDict) // Create a Shape ID for this member's target let targetID = try ShapeID(astMember.value.target) @@ -81,7 +92,8 @@ extension Model { // Create model traits from the AST traits. let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value) } ?? [] - let traits = Dictionary(uniqueKeysWithValues: idToTraitPairs) + let traitDict = Dictionary(uniqueKeysWithValues: idToTraitPairs) + let traits = TraitCollection(traits: traitDict) // Based on the AST shape type, create the appropriate Shape type. switch astShape.type { @@ -89,15 +101,31 @@ extension Model { let shape = ServiceShape( id: shapeID, 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 } ?? [] ) return (shapeID, shape) + case .resource: + let shape = ResourceShape( + id: shapeID, + traits: traits, + operationIDs: try astShape.operations?.map { try $0.id } ?? [], + createID: try astShape.create?.id, + putID: try astShape.put?.id, + readID: try astShape.read?.id, + updateID: try astShape.update?.id, + deleteID: try astShape.delete?.id, + listID: try astShape.list?.id + ) + return (shapeID, shape) case .operation: let shape = OperationShape( id: shapeID, traits: traits, - input: try astShape.input?.id, - output: try astShape.output?.id + inputID: try astShape.input?.id, + outputID: try astShape.output?.id, + errorIDs: try astShape.errors?.map { try $0.id } ?? [] ) return (shapeID, shape) case .structure: @@ -142,6 +170,17 @@ extension Model { memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) ) return (shapeID, shape) + case .string: + if traits.hasTrait(EnumTrait.self) { + let shape = EnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + } else { + fallthrough + } default: let shape = Shape( id: shapeID, diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift index 2ee603c47..8fe328908 100644 --- a/Sources/SmithyCodegenCore/Model/Model.swift +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -12,11 +12,16 @@ public class Model { public let version: String public let metadata: Node? public let shapes: [ShapeID: Shape] + public let allShapesSorted: [Shape] init(version: String, metadata: Node?, shapes: [ShapeID: Shape]) { self.version = version self.metadata = metadata self.shapes = shapes + self.allShapesSorted = Array(shapes.values).smithySorted() + // self is now fully initialized. + // Set the model on each shape to self. + shapes.values.forEach { $0.model = self } } func expectShape(id: ShapeID) throws -> Shape { @@ -25,4 +30,53 @@ public class Model { } return shape } + + func expectServiceShape(id: ShapeID) throws -> ServiceShape { + guard let shape = try expectShape(id: id) as? ServiceShape else { + throw ModelError("ShapeID \(id) is not a ServiceShape") + } + return shape + } + + func expectResourceShape(id: ShapeID) throws -> ResourceShape { + guard let shape = try expectShape(id: id) as? ResourceShape else { + throw ModelError("ShapeID \(id) is not a ResourceShape") + } + return shape + } + + func expectOperationShape(id: ShapeID) throws -> OperationShape { + guard let shape = try expectShape(id: id) as? OperationShape else { + throw ModelError("ShapeID \(id) is not a OperationShape") + } + return shape + } + + func expectStructureShape(id: ShapeID) throws -> StructureShape { + guard let shape = try expectShape(id: id) as? StructureShape else { + throw ModelError("ShapeID \(id) is not a StructureShape") + } + return shape + } + + func expectListShape(id: ShapeID) throws -> ListShape { + guard let shape = try expectShape(id: id) as? ListShape else { + throw ModelError("ShapeID \(id) is not a ListShape") + } + return shape + } + + func expectMapShape(id: ShapeID) throws -> MapShape { + guard let shape = try expectShape(id: id) as? MapShape else { + throw ModelError("ShapeID \(id) is not a MapShape") + } + return shape + } + + func expectMemberShape(id: ShapeID) throws -> MemberShape { + guard let shape = try expectShape(id: id) as? MemberShape else { + throw ModelError("ShapeID \(id) is not a MemberShape") + } + return shape + } } diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift new file mode 100644 index 000000000..233beebd5 --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift @@ -0,0 +1,51 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ClientOptionalTrait +import struct Smithy.ShapeID + +extension Model { + + func optionalizeStructMembers(serviceID: ShapeID) throws -> Model { + guard affectedServices.contains(serviceID.absoluteID) else { return self } + + let newShapes = try self.shapes.mapValues { shape in + guard let member = shape as? MemberShape else { return shape } + guard try member.container.type == .structure else { return shape } + return MemberShape( + id: member.id, + traits: member.traits.adding([ClientOptionalTrait()]), + targetID: member.targetID + ) + } + return Model(version: self.version, metadata: self.metadata, shapes: newShapes) + } +} + +private var affectedServices = [ + "com.amazonaws.ec2#AmazonEC2", + "com.amazonaws.nimble#nimble", + "com.amazonaws.amplifybackend#AmplifyBackend", + "com.amazonaws.apigatewaymanagementapi#ApiGatewayManagementApi", + "com.amazonaws.apigatewayv2#ApiGatewayV2", + "com.amazonaws.dataexchange#DataExchange", + "com.amazonaws.greengrass#Greengrass", + "com.amazonaws.iot1clickprojects#AWSIoT1ClickProjects", + "com.amazonaws.kafka#Kafka", + "com.amazonaws.macie2#Macie2", + "com.amazonaws.mediaconnect#MediaConnect", + "com.amazonaws.mediaconvert#MediaConvert", + "com.amazonaws.medialive#MediaLive", + "com.amazonaws.mediapackage#MediaPackage", + "com.amazonaws.mediapackagevod#MediaPackageVod", + "com.amazonaws.mediatailor#MediaTailor", + "com.amazonaws.pinpoint#Pinpoint", + "com.amazonaws.pinpointsmsvoice#PinpointSMSVoice", + "com.amazonaws.serverlessapplicationrepository#ServerlessApplicationRepository", + "com.amazonaws.mq#mq", + "com.amazonaws.schemas#schemas", +] diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift new file mode 100644 index 000000000..dd28459ef --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift @@ -0,0 +1,116 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension Model { + + func withDeprecatedShapesRemoved() throws -> Model { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd" + + let nonDeprecatedShapes = try shapes.filter { (_, shape) in + guard let since = try shape.getTrait(DeprecatedTrait.self)?.since else { return true } + + guard let sinceDate = formatter.date(from: since) else { return true } + + let cutoff = formatter.date(from: "2024-09-17")! + + return sinceDate > cutoff + } + + var trimmedShapes = nonDeprecatedShapes + var trimmedShapesCount = 0 + + repeat { + trimmedShapesCount = 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 != trimmedShapesCount + + 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 + } + } + + return Model(version: self.version, metadata: self.metadata, shapes: finalShapes) + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift new file mode 100644 index 000000000..190ec5786 --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -0,0 +1,95 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import enum Smithy.Prelude +import struct Smithy.ShapeID +import struct Smithy.TargetsUnitTrait +import struct Smithy.TraitCollection + +extension Model { + + func withSynthesizedInputsOutputs() throws -> Model { + + // Get the operations in the model + let operations = shapes.values + .filter { $0.type == .operation } + .compactMap { $0 as? OperationShape } + + // Make a copy of this model's shapes to modify + var newShapes = shapes + + for operation in operations { + // Make new "synthetic" ShapeIDs for this operation's input & output + let newInputShapeID = ShapeID("swift.synthetic", "\(operation.id.name)Input") + let newOutputShapeID = ShapeID("swift.synthetic", "\(operation.id.name)Output") + + // Get the input & output structures for this operation. Substitute an empty + // structure if ID is omitted or targets Unit + let inputShape = if operation.inputID == Prelude.unitSchema.id { + StructureShape(id: Prelude.unitSchema.id, traits: [TargetsUnitTrait()], memberIDs: []) + } else { + try expectStructureShape(id: operation.inputID) + } + let outputShape = if operation.outputID == Prelude.unitSchema.id { + StructureShape(id: Prelude.unitSchema.id, traits: [TargetsUnitTrait()], memberIDs: []) + } else { + try expectStructureShape(id: operation.outputID) + } + + // Make new input and output shapes, plus their members, with the new ID + // Add UsedAsInput and UsedAsOutput traits to the input/output structures + // These traits allow us to identify inputs/outputs by trait, but allow us to + // leave the Smithy input & output traits as set on the original model. + let newInput = newStruct(newID: newInputShapeID, newTraits: [UsedAsInputTrait()], original: inputShape) + let newInputShapeMembers = try renamedMembers(newID: newInputShapeID, original: inputShape) + let newOutput = newStruct(newID: newOutputShapeID, newTraits: [UsedAsOutputTrait()], original: outputShape) + let newOutputShapeMembers = try renamedMembers(newID: newOutputShapeID, original: outputShape) + + // Add the new input & output and their members to the new shape dictionary. + // The originals will remain and will be pruned later if they are unreferenced. + newShapes[newInput.id] = newInput + newInputShapeMembers.forEach { newShapes[$0.id] = $0 } + newShapes[newOutput.id] = newOutput + newOutputShapeMembers.forEach { newShapes[$0.id] = $0 } + + // Make a new operation with the new input & output IDs + let newOperation = OperationShape( + id: operation.id, + traits: operation.traits, + inputID: newInputShapeID, + outputID: newOutputShapeID, + errorIDs: operation.errorIDs + ) + + // Add the new operation to the new shapes. It will replace the original + // since the new operation has the same ID. + newShapes[newOperation.id] = newOperation + } + // Return the new model with the updated shapes. + return Model(version: version, metadata: metadata, shapes: newShapes) + } + + private func newStruct(newID: ShapeID, newTraits: TraitCollection, original: StructureShape) -> StructureShape { + StructureShape( + id: newID, + traits: original.traits.adding(newTraits), + memberIDs: original.memberIDs.map { .init(id: newID, member: $0.member) } + ) + } + + private func renamedMembers(newID: ShapeID, original: StructureShape) throws -> [MemberShape] { + let originalMembers = try original.memberIDs.map { try expectMemberShape(id: $0) } + return originalMembers.map { member in + MemberShape( + id: .init(id: newID, member: member.id.member), + traits: member.traits, + targetID: member.targetID + ) + } + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift new file mode 100644 index 000000000..c9b8cae2e --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Model { + + // Filters out all shapes except for the identified service and its descendants + // Returns the pruned model, plus the service shape that it is pruned to + func prune(serviceID: ShapeID) throws -> Model { + + // Get the service + let service = try expectServiceShape(id: serviceID) + + // Create a set with the service and its descendants + let shapesForService = try Set([service]).union(service.descendants) + + // Create a dictionary from the set, keyed by ShapeID + let shapeDict = Dictionary(uniqueKeysWithValues: shapesForService.map { ($0.id, $0) }) + + // Create and return the model & service in a tuple + return Model(version: self.version, metadata: self.metadata, shapes: shapeDict) + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift new file mode 100644 index 000000000..e092d0aeb --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift @@ -0,0 +1,44 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Prelude +import struct Smithy.ShapeID + +extension Model { + + // smithy-swift mistakenly creates a structure for an enum case with an associated value for a union + // member that targets smithy.api#Unit. To ensure that these structures get SerializableStruct / + // DeserializableStruct conformance, we replace smithy.api#Unit with a structure named Unit in the + // union's namespace. + func withUnionsTargetingUnitAdded() throws -> Model { + var unitSubstitute: Shape? + + var newShapes = try shapes.mapValues { shape in + guard let member = shape as? MemberShape else { return shape } + guard try member.container.type == .union else { return member } + guard member.targetID == Smithy.Prelude.unitSchema.id else { return member } + let unitSubstituteID = ShapeID(member.id.namespace, "Unit") + unitSubstitute = unitSubstitute ?? StructureShape( + id: unitSubstituteID, + traits: [:], + memberIDs: [] + ) + let newMember = MemberShape( + id: member.id, + traits: member.traits, + targetID: unitSubstituteID + ) + return newMember + } + + if let unitSubstitute, newShapes[unitSubstitute.id] == nil { + newShapes[unitSubstitute.id] = unitSubstitute + } + + return Model(version: self.version, metadata: self.metadata, shapes: newShapes) + } +} diff --git a/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift b/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift new file mode 100644 index 000000000..0812ddda3 --- /dev/null +++ b/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift @@ -0,0 +1,76 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.AddedDefaultTrait +import struct Smithy.ClientOptionalTrait +import struct Smithy.DefaultTrait +import struct Smithy.InputTrait +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.SparseTrait + +struct NullableIndex { + + /// Determines whether a structure member should be rendered as non-optional. + /// - Parameter memberShape: The member for which optionality is being determined + /// - Returns: `true` if the member should be non-optional, `false` otherwise + func isNonOptional(_ memberShape: MemberShape) throws -> Bool { + let container = try memberShape.container + let target = try memberShape.target + + // Note that these are the current rules in use by smithy-swift. They are not Smithy 2.0 "correct". + + // If the container is a list/set, member is nonoptional unless sparse trait is applied + if [ShapeType.list, .set].contains(container.type), memberShape.id.member == "member" { + return !container.hasTrait(SparseTrait.self) + } + + // If the container is a map, value is nonoptional unless sparse trait is applied + if container.type == .map { + if memberShape.id.member == "value" { + return !container.hasTrait(SparseTrait.self) + } else { + // key is always non-optional + return true + } + } + + // If the containing shape has the input trait, it's definitely optional + if container.hasTrait(InputTrait.self) { + return false + } + + // If the member has the clientOptional trait, it's definitely optional + if memberShape.hasTrait(ClientOptionalTrait.self) { + return false + } + + // If the member has the addedDefault trait, it's definitely optional + if memberShape.hasTrait(AddedDefaultTrait.self) { + return false + } + + // Only number & Boolean types are allowed to be non-optional + let allowedTypes = + [ShapeType.boolean, .bigDecimal, .bigInteger, .byte, .double, .float, .intEnum, .integer, .long, .short] + guard allowedTypes.contains(target.type) else { return false } + + // Check if there is a default trait with a zero/false value. If so, member is non-optional. + let memberDefaultTrait = try memberShape.getTrait(DefaultTrait.self) + let targetDefaultTrait = try target.getTrait(DefaultTrait.self) + guard let defaultNode = (memberDefaultTrait ?? targetDefaultTrait)?.node else { + return false + } + if target.type == .boolean, let bool = defaultNode.boolean, !bool { + return true + } else if let number = defaultNode.number, number == 0.0 { + return true + } else { + return false + } + } +} diff --git a/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt b/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt new file mode 100644 index 000000000..8f50fc772 --- /dev/null +++ b/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by SmithyCodegenCLI. DO NOT EDIT! diff --git a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift new file mode 100644 index 000000000..b24460f9c --- /dev/null +++ b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift @@ -0,0 +1,108 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import let Smithy.allSupportedTraits +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +package struct SchemasCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import struct Smithy.Schema") + writer.write("import enum Smithy.Prelude") + writer.write("") + + // Get all operations, sorted + let sortedOperationShapes = ctx.model.allShapesSorted + .filter { $0.type == .operation } + + // Get the rest of the shapes, sorted + let sortedModelShapes: [Shape] = ctx.model.allShapesSorted + .filter { ![ShapeType.service, .resource, .operation, .member].contains($0.type) } + .filter { $0.id.namespace != "smithy.api" } + + // Combine shapes in order: service, operations sorted, models sorted + let allShapes = [ctx.service] + sortedOperationShapes + sortedModelShapes + + for shape in allShapes { + try writer.openBlock("var \(shape.schemaVarName): Smithy.Schema {", "}") { writer in + try writeSchema(writer: writer, shape: shape) + writer.unwrite(",") + } + writer.write("") + + // If a schema has a member that targets the schema itself, we avoid a compile warning for + // self-reference by generating a duplicate schema var that references this schema, and we + // target the duplicate instead. + // + // This happens ~20 times in AWS models so it is not so frequent that the extra var will bloat + // service clients. + if let hm = shape as? HasMembers, try hm.members.contains(where: { $0.targetID == shape.id }) { + try writer.openBlock("var dup_of_\(shape.schemaVarName): Smithy.Schema {", "}") { writer in + try writer.write(shape.schemaVarName) + } + writer.write("") + } + } + writer.unwrite("\n") + return writer.contents + } + + private func writeSchema(writer: SwiftWriter, shape: Shape, index: Int? = nil) throws { + try writer.openBlock(".init(", "),") { writer in + writer.write("id: \(shape.id.rendered),") + writer.write("type: .\(shape.type),") + let relevantTraitIDs = shape.traits.traitDict.keys.filter { allSupportedTraits.contains($0) } + let traitIDs = Array(relevantTraitIDs).smithySorted() + if !traitIDs.isEmpty { + writer.openBlock("traits: [", "],") { writer in + for traitID in traitIDs { + let trait = shape.traits.traitDict[traitID]! + writer.write("\(traitID.rendered): \(trait.rendered),") + } + } + } + let members = try (shape as? HasMembers)?.members ?? [] + if !members.isEmpty { + try writer.openBlock("members: [", "],") { writer in + for (index, member) in members.enumerated() { + try writeSchema(writer: writer, shape: member, index: index) + } + } + } + if let member = shape as? MemberShape { + let target = try member.target + + // If this schema's target is the same as itself, target the duplicate + // (see above) to avoid a self-reference compile warning. + let prefix = target.id == member.containerID ? "dup_of_" : "" + writer.write(try "target: \(prefix)\(target.schemaVarName),") + } + if let index { + writer.write("index: \(index),") + } + writer.unwrite(",") + } + } +} + +extension ShapeID { + + var rendered: String { + let namespaceLiteral = namespace.literal + let nameLiteral = name.literal + if let member { + let memberLiteral = member.literal + return ".init(\(namespaceLiteral), \(nameLiteral), \(memberLiteral))" + } else { + return ".init(\(namespaceLiteral), \(nameLiteral))" + } + } +} diff --git a/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift new file mode 100644 index 000000000..9fcfeb628 --- /dev/null +++ b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift @@ -0,0 +1,61 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Shape { + + var schemaVarName: String { + get throws { + if id.namespace == "smithy.api" { + try id.preludeSchemaVarName + } else { + try id.schemaVarName + } + } + } +} + +private extension ShapeID { + + var preludeSchemaVarName: String { + get throws { + let propertyName = switch name { + case "Unit": "unitSchema" + case "String": "stringSchema" + case "Blob": "blobSchema" + case "Integer": "integerSchema" + case "Timestamp": "timestampSchema" + case "Boolean": "booleanSchema" + case "Float": "floatSchema" + case "Double": "doubleSchema" + case "Long": "longSchema" + case "Short": "shortSchema" + case "Byte": "byteSchema" + case "PrimitiveInteger": "primitiveIntegerSchema" + case "PrimitiveBoolean": "primitiveBooleanSchema" + case "PrimitiveFloat": "primitiveFloatSchema" + case "PrimitiveDouble": "primitiveDoubleSchema" + case "PrimitiveLong": "primitiveLongSchema" + case "PrimitiveShort": "primitiveShortSchema" + case "PrimitiveByte": "primitiveByteSchema" + case "Document": "documentSchema" + default: throw ModelError("Unhandled prelude type converted to schemaVar: \"\(name)\"") + } + return "Smithy.Prelude.\(propertyName)" + } + } + + var schemaVarName: String { + get throws { + guard member == nil else { throw ModelError("Assigning member schema to a var") } + let namespacePortion = namespace.replacingOccurrences(of: ".", with: "_") + let namePortion = name + return "schema__\(namespacePortion)__\(namePortion)" + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift index b21948702..5a6ad3d4a 100644 --- a/Sources/SmithyCodegenCore/Shape/EnumShape.swift +++ b/Sources/SmithyCodegenCore/Shape/EnumShape.swift @@ -8,21 +8,29 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy enums. public class EnumShape: Shape, HasMembers { + let memberIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs super.init(id: id, type: .enum, traits: traits) } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) } - override func candidates(for shape: Shape) -> [Shape] { - members.map { $0.target } + func trimmingMembers(onlyTarget: Set) -> Shape { + return self } } diff --git a/Sources/SmithyCodegenCore/Shape/HasMembers.swift b/Sources/SmithyCodegenCore/Shape/HasMembers.swift index dd2a32e3d..96f90272a 100644 --- a/Sources/SmithyCodegenCore/Shape/HasMembers.swift +++ b/Sources/SmithyCodegenCore/Shape/HasMembers.swift @@ -7,5 +7,5 @@ /// Protocol provided as a convenience to get members from Shapes that have them. protocol HasMembers { - var members: [MemberShape] { get } + var members: [MemberShape] { get throws } } diff --git a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift index 43703dc00..5862fda78 100644 --- a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift +++ b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift @@ -8,21 +8,24 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy intEnums. public class IntEnumShape: Shape, HasMembers { let memberIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs super.init(id: id, type: .intEnum, traits: traits) } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } } - override func candidates(for shape: Shape) -> [Shape] { - members.map { $0.target } + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) } } diff --git a/Sources/SmithyCodegenCore/Shape/ListShape.swift b/Sources/SmithyCodegenCore/Shape/ListShape.swift index 4dd331cf5..1ff52d53d 100644 --- a/Sources/SmithyCodegenCore/Shape/ListShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ListShape.swift @@ -8,26 +8,32 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy lists. public class ListShape: Shape, HasMembers { let memberIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs super.init(id: id, type: .list, traits: traits) } + public var memberID: ShapeID { .init(id: self.id, member: "member") } + public var member: MemberShape { - // swiftlint:disable:next force_cast - model.shapes[.init(id: id, member: "member")]! as! MemberShape + get throws { + try model.expectMemberShape(id: memberID) + } } public var members: [MemberShape] { - [member] + get throws { + try [member] + } } - override func candidates(for shape: Shape) -> [Shape] { - members.map { $0.target } + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) } } diff --git a/Sources/SmithyCodegenCore/Shape/MapShape.swift b/Sources/SmithyCodegenCore/Shape/MapShape.swift index fa6d9bc36..7feb35631 100644 --- a/Sources/SmithyCodegenCore/Shape/MapShape.swift +++ b/Sources/SmithyCodegenCore/Shape/MapShape.swift @@ -8,29 +8,40 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy maps. public class MapShape: Shape, HasMembers { let memberIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs - super.init(id: id, type: .list, traits: traits) + super.init(id: id, type: .map, traits: traits) } + public var keyID: ShapeID { .init(id: self.id, member: "key") } + public var key: MemberShape { - model.shapes[.init(id: id, member: "key")]! as! MemberShape // swiftlint:disable:this force_cast + get throws { + try model.expectMemberShape(id: keyID) + } } + public var valueID: ShapeID { .init(id: self.id, member: "value") } + public var value: MemberShape { - model.shapes[.init(id: id, member: "value")]! as! MemberShape // swiftlint:disable:this force_cast + get throws { + try model.expectMemberShape(id: valueID) + } } public var members: [MemberShape] { - return [key, value] + get throws { + try [key, value] + } } - override func candidates(for shape: Shape) -> [Shape] { - members.map { $0.target } + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) } } diff --git a/Sources/SmithyCodegenCore/Shape/MemberShape.swift b/Sources/SmithyCodegenCore/Shape/MemberShape.swift index 29abb70ca..ab715f70b 100644 --- a/Sources/SmithyCodegenCore/Shape/MemberShape.swift +++ b/Sources/SmithyCodegenCore/Shape/MemberShape.swift @@ -7,17 +7,36 @@ import enum Smithy.Node import struct Smithy.ShapeID +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy members. public class MemberShape: Shape { let targetID: ShapeID - init(id: ShapeID, traits: [ShapeID: Node], targetID: ShapeID) { + init(id: ShapeID, traits: TraitCollection, targetID: ShapeID) { self.targetID = targetID super.init(id: id, type: .member, traits: traits) } + public var container: Shape { + get throws { + return try model.expectShape(id: containerID) + } + } + + public var containerID: ShapeID { .init(id: id, member: nil) } + public var target: Shape { - return model.shapes[targetID] ?? Shape.prelude[targetID]! + get throws { + guard let target = model.shapes[targetID] ?? Shape.prelude[targetID] else { + throw ModelError("Member \(id): target \(targetID) does not exist") + } + return target + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + guard targetID.namespace != "smithy.api" else { return [] } + return [try target] } } diff --git a/Sources/SmithyCodegenCore/Shape/OperationShape.swift b/Sources/SmithyCodegenCore/Shape/OperationShape.swift index 53a582b2f..1531c0623 100644 --- a/Sources/SmithyCodegenCore/Shape/OperationShape.swift +++ b/Sources/SmithyCodegenCore/Shape/OperationShape.swift @@ -6,41 +6,46 @@ // import enum Smithy.Node +import enum Smithy.Prelude import struct Smithy.ShapeID import enum Smithy.ShapeType +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy operations. -class OperationShape: Shape { - let inputShapeID: ShapeID? - let outputShapeID: ShapeID? +public class OperationShape: Shape { + let inputID: ShapeID + let outputID: ShapeID + let errorIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], input: ShapeID?, output: ShapeID?) { - self.inputShapeID = input - self.outputShapeID = output + public init(id: ShapeID, traits: TraitCollection, inputID: ShapeID?, outputID: ShapeID?, errorIDs: [ShapeID]) { + self.inputID = inputID ?? Prelude.unitSchema.id + self.outputID = outputID ?? Prelude.unitSchema.id + self.errorIDs = errorIDs super.init(id: id, type: .operation, traits: traits) } - public var input: Shape { - if let inputShapeID { - return model.shapes[inputShapeID]!.adding(traits: [.init("smithy.api", "input"): [:]]) - } else { - let traits: [ShapeID: Node] = [ - .init("smithy.api", "input"): [:], - .init("swift.synthetic", "operationName"): .string(id.id), - ] - return Shape.unit.adding(traits: traits) + public var input: StructureShape { + get throws { + try model.expectStructureShape(id: inputID) } } - public var output: Shape { - if let outputShapeID { - return model.shapes[outputShapeID]!.adding(traits: [.init("smithy.api", "output"): [:]]) - } else { - let traits: [ShapeID: Node] = [ - .init("smithy.api", "input"): [:], - .init("swift.synthetic", "operationName"): .string(id.id), - ] - return Shape.unit.adding(traits: traits) + public var output: StructureShape { + get throws { + try model.expectStructureShape(id: outputID) } } + + public var errors: [StructureShape] { + get throws { + try errorIDs.map { try model.expectStructureShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + let inputOrNone = try includeInput ? [input] : [] + let outputOrNone = try includeOutput ? [output] : [] + let errorsOrNone = try includeOutput ? errorIDs.map { try model.expectShape(id: $0) } : [] + return Set(inputOrNone + outputOrNone + errorsOrNone) + } } diff --git a/Sources/SmithyCodegenCore/Shape/ResourceShape.swift b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift new file mode 100644 index 000000000..ec15f85b8 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift @@ -0,0 +1,46 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import struct Smithy.TraitCollection + +public class ResourceShape: Shape { + let operationIDs: [ShapeID] + let createID: ShapeID? + let putID: ShapeID? + let readID: ShapeID? + let updateID: ShapeID? + let deleteID: ShapeID? + let listID: ShapeID? + + init( + id: ShapeID, + traits: TraitCollection, + operationIDs: [ShapeID], + createID: ShapeID?, + putID: ShapeID?, + readID: ShapeID?, + updateID: ShapeID?, + deleteID: ShapeID?, + listID: ShapeID? + ) { + self.operationIDs = operationIDs + self.createID = createID + self.putID = putID + self.readID = readID + self.updateID = updateID + self.deleteID = deleteID + self.listID = listID + super.init(id: id, type: .resource, traits: traits) + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + let allOps = [createID, putID, readID, updateID, deleteID, listID].compactMap { $0 } + operationIDs + return try Set(allOps.map { try model.expectShape(id: $0) }) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift index 101257cfd..01fc02b96 100644 --- a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift @@ -6,18 +6,76 @@ // import enum Smithy.Node +import struct Smithy.ServiceTrait import struct Smithy.ShapeID +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy services. public class ServiceShape: Shape { + let operationIDs: [ShapeID] + let resourceIDs: [ShapeID] let errorIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], errorIDs: [ShapeID]) { + public init( + id: ShapeID, + traits: TraitCollection, + operationIDs: [ShapeID], + resourceIDs: [ShapeID], + errorIDs: [ShapeID] + ) { + self.operationIDs = operationIDs + self.resourceIDs = resourceIDs self.errorIDs = errorIDs super.init(id: id, type: .service, traits: traits) } - public var errors: [Shape] { - errorIDs.compactMap { model.shapes[$0] } + public var operations: [OperationShape] { + get throws { + try operationIDs.compactMap { try model.expectOperationShape(id: $0) } + } + } + + public var resources: [ResourceShape] { + get throws { + try resourceIDs.compactMap { try model.expectResourceShape(id: $0) } + } + } + + public var errors: [StructureShape] { + get throws { + try errorIDs.compactMap { try model.expectStructureShape(id: $0) } + } + } + + public var sdkId: String { + get throws { + try getTrait(ServiceTrait.self)?.sdkId ?? id.name + } + } + + public var sdkIdStrippingService: String { + get throws { + var sdkIdStrippingService = try sdkId + let unwantedSuffix = " Service" + if sdkIdStrippingService.hasSuffix(unwantedSuffix) { + sdkIdStrippingService.removeLast(unwantedSuffix.count) + } + return sdkIdStrippingService + } + } + + public var clientBaseName: String { + get throws { + try sdkIdStrippingService.toUpperCamelCase() + } + + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + if includeOutput { + try Set(errors + operations + resources) + } else { + try Set(operations + resources) + } } } diff --git a/Sources/SmithyCodegenCore/Shape/Shape.swift b/Sources/SmithyCodegenCore/Shape/Shape.swift index d955faf05..49586d8a3 100644 --- a/Sources/SmithyCodegenCore/Shape/Shape.swift +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -8,51 +8,81 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType +import protocol Smithy.Trait +import struct Smithy.TraitCollection -public class Shape { +public class Shape: HasShapeID { public let id: ShapeID public let type: ShapeType - public let traits: [ShapeID: Node] + public let traits: TraitCollection weak var model: Model! - public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node]) { + public init(id: ShapeID, type: ShapeType, traits: TraitCollection) { self.id = id self.type = type self.traits = traits } - public func hasTrait(_ traitID: ShapeID) -> Bool { - traits[traitID] != nil + public func hasTrait(_ type: T.Type) -> Bool { + traits.hasTrait(type) } - public func getTrait(_ traitID: ShapeID) -> Node? { - traits[traitID] + public func getTrait(_ type: T.Type) throws -> T? { + try traits.getTrait(type) } - public func adding(traits newTraits: [ShapeID: Node]) -> Shape { - let combinedTraits = traits.merging(newTraits) { _, new in new } + public func adding(traits newTraits: TraitCollection) -> Shape { + let combinedTraits = traits.adding(newTraits) let new = Shape(id: id, type: type, traits: combinedTraits) new.model = model return new } public var descendants: Set { - var c = Set() - descendants(&c) - return c + get throws { + var c = Set() + try descendants(descendants: &c, includeInput: true, includeOutput: true) + return c + } + } + + public var inputDescendants: Set { + get throws { + var c = Set() + try descendants(descendants: &c, includeInput: true, includeOutput: false) + return c + } + } + + public var outputDescendants: Set { + get throws { + var c = Set() + try descendants(descendants: &c, includeInput: false, includeOutput: true) + return c + } } - private func descendants(_ descendants: inout Set) { - let shapes = candidates(for: self) - for shape in shapes { + private func descendants(descendants: inout Set, includeInput: Bool, includeOutput: Bool) throws { + for shape in try immediateDescendants(includeInput: includeInput, includeOutput: includeOutput) { if descendants.contains(shape) { continue } descendants.insert(shape) - shape.descendants(&descendants) + try shape.descendants( + descendants: &descendants, + includeInput: includeInput, + includeOutput: includeOutput + ) } } - func candidates(for shape: Shape) -> [Shape] { - [] // default. May be overridden by Shape subclasses. + /// Returns shapes that this shape refers to. + /// + /// Used to build a set of shapes for code generation purposes. + /// - Parameters: + /// - includeInput: Whether to include shapes that are associated with input + /// - includeOutput: Whether to include shapes that are associated with input + /// - Returns: A set of shapes that this shape refers to. + func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + [] // none by default. Must be overridden by Shape subclasses that refer to descendant shape types. } } diff --git a/Sources/SmithyCodegenCore/Shape/StructureShape.swift b/Sources/SmithyCodegenCore/Shape/StructureShape.swift index 122b795d8..6c5c5a705 100644 --- a/Sources/SmithyCodegenCore/Shape/StructureShape.swift +++ b/Sources/SmithyCodegenCore/Shape/StructureShape.swift @@ -8,21 +8,24 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy structures. public class StructureShape: Shape, HasMembers { let memberIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs super.init(id: id, type: .structure, traits: traits) } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } } - override func candidates(for shape: Shape) -> [Shape] { - members.map { $0.target } + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) } } diff --git a/Sources/SmithyCodegenCore/Shape/UnionShape.swift b/Sources/SmithyCodegenCore/Shape/UnionShape.swift index f6b7ba3e5..1d141b3d3 100644 --- a/Sources/SmithyCodegenCore/Shape/UnionShape.swift +++ b/Sources/SmithyCodegenCore/Shape/UnionShape.swift @@ -8,21 +8,24 @@ import enum Smithy.Node import struct Smithy.ShapeID import enum Smithy.ShapeType +import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy unions. public class UnionShape: Shape, HasMembers { let memberIDs: [ShapeID] - public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs super.init(id: id, type: .union, traits: traits) } public var members: [MemberShape] { - return memberIDs.map { model.shapes[$0]! as! MemberShape } // swiftlint:disable:this force_cast + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } } - override func candidates(for shape: Shape) -> [Shape] { - members.map { $0.target } + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) } } diff --git a/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift b/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift new file mode 100644 index 000000000..56c153ee4 --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node + +extension Node { + + /// Returns the node, rendered into a Swift literal for use in generated code. + /// + /// The node is rendered with some interstitial whitespace but single-line. + var rendered: String { + switch self { + case .object(let object): + guard !object.isEmpty else { return "[:]" } + return "[" + object.map { "\($0.key.literal): \($0.value.rendered)" }.joined(separator: ",") + "]" + case .list(let list): + return "[" + list.map { $0.rendered }.joined(separator: ", ") + "]" + case .string(let string): + return string.literal + case .number(let number): + return "\(number)" + case .boolean(let bool): + return "\(bool)" + case .null: + return "nil" + } + } +} diff --git a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift new file mode 100644 index 000000000..3d3c21e87 --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension String { + + /// Escapes special characters in the string, then surrounds it in double quotes + /// to form a Swift string literal. + var literal: String { + let escaped = description + .replacingOccurrences(of: "\0", with: "\\0") + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\t", with: "\\t") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\'", with: "\\'") + return "\"\(escaped)\"" + } + + var inBackticks: String { + "`\(self)`" + } + + var capitalized: String { + let firstChar = self.first?.uppercased() ?? "" + return "\(firstChar)\(self.dropFirst())" + } +} diff --git a/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift new file mode 100644 index 000000000..79a468e1a --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift @@ -0,0 +1,74 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class Foundation.Bundle +import struct Foundation.Data +import struct Foundation.URL + +class SwiftWriter { + + private var lines: [String] + + var indentLevel = 0 + + init(includeHeader: Bool = true) { + if includeHeader { + let defaultHeaderFileURL = Bundle.module.url(forResource: "DefaultSwiftHeader", withExtension: "txt")! + // swiftlint:disable:next force_try + let defaultHeader = try! String(data: Data(contentsOf: defaultHeaderFileURL), encoding: .utf8)! + self.lines = defaultHeader.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) } + } else { + self.lines = [] + } + } + + func indent() { + indentLevel += 4 + } + + func dedent() { + indentLevel -= 4 + } + + func write(_ line: String) { + if line.isEmpty { + // Don't write whitespace to an empty line + lines.append("") + } else { + // Write whitespace for the indent level, then the line content + lines.append(String(repeating: " ", count: indentLevel) + line) + } + } + + /// Removes previously written text. + /// + /// If the unwritten text matches the end of the last written line, then that text will be removed from that line. + /// + /// If the unwritten text is `\n`, then the entire previous line will be removed only if it is an empty line. + /// Otherwise, unwriting `\n` has no effect. + /// - Parameter text: The text to be removed from the last written text. + func unwrite(_ text: String) { + guard let lastIndex = lines.indices.last else { return } + if text == "\n" && lines[lastIndex] == "" { + _ = lines.removeLast() + } else if lines[lastIndex].hasSuffix(text) { + lines[lastIndex].removeLast(text.count) + } + } + + func openBlock(_ openWith: String, _ closeWith: String, contents: (SwiftWriter) throws -> Void) rethrows { + write(openWith) + indent() + try contents(self) + dedent() + write(closeWith) + } + + var contents: String { + return lines.joined(separator: "\n").appending("\n") + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift b/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift new file mode 100644 index 000000000..b4a25a5a9 --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift @@ -0,0 +1,125 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension String { + + /// Modifies Swift reserved words that are used in Smithy models so that they can be safely used as identifiers in rendered Swift. + /// + /// Taken from `ReservedWords.kt`: + /// https://github.com/smithy-lang/smithy-swift/blob/main/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/lang/ReservedWords.kt + var escapingReservedWords: String { + if self == "Protocol" || self == "Type" { + // Swift metatypes + "Model\(self)" + } else if reservedWords.contains(self) { + // Surround reserved words in backticks to force compiler + // to treat them as identifiers + self.inBackticks + } else { + self + } + } +} + +private let reservedWords = [ + "Any", + "#available", + "associatedtype", + "associativity", + "as", + "break", + "case", + "catch", + "class", + "#colorLiteral", + "#column", + "continue", + "convenience", + "deinit", + "default", + "defer", + "didSet", + "do", + "dynamic", + "enum", + "extension", + "else", + "#else", + "#elseif", + "#endif", + "#error", + "fallthrough", + "false", + "#file", + "#fileLiteral", + "fileprivate", + "final", + "for", + "func", + "#function", + "get", + "guard", + "indirect", + "infix", + "if", + "#if", + "#imageLiteral", + "in", + "is", + "import", + "init", + "inout", + "internal", + "lazy", + "left", + "let", + "#line", + "mutating", + "none", + "nonmutating", + "nil", + "open", + "operator", + "optional", + "override", + "package", + "postfix", + "prefix", + "private", + "protocol", + "Protocol", + "public", + "repeat", + "rethrows", + "return", + "required", + "right", + "#selector", + "self", + "Self", + "set", + "#sourceLocation", + "super", + "static", + "struct", + "subscript", + "switch", + "this", + "throw", + "throws", + "true", + "try", + "Type", + "typealias", + "unowned", + "var", + "#warning", + "weak", + "willSet", + "where", + "while", +] diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift index 9ce726e89..0bff45cbc 100644 --- a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -5,8 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Foundation.Locale import struct Foundation.NSRange import class Foundation.NSRegularExpression +import struct Smithy.ErrorTrait +import struct Smithy.ServiceTrait import struct Smithy.ShapeID public struct SymbolProvider { @@ -20,62 +23,102 @@ public struct SymbolProvider { var serviceName: String { get throws { - guard service.type == .service else { - throw SymbolProviderError("Called serviceName on non-service shape") + 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: + let base = shape.id.name + if shape.isTopLevel { + return base.capitalized.escapingReservedWords + } 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 + } else { + return try "\(modelNamespace).\(base.capitalized.escapingReservedWords)" + } + case .list, .set: + guard let listShape = shape as? ListShape else { + throw SymbolProviderError("Shape has type .list but is not a ListShape") } - guard case .object(let serviceInfo) = service.getTrait(.init("aws.api", "service")) else { - throw SymbolProviderError("No service trait on service") + let elementType = try swiftType(shape: listShape.member.target) + let opt = try NullableIndex().isNonOptional(listShape.member) ? "" : "?" + return "[\(elementType)\(opt)]" + case .map: + guard let mapShape = shape as? MapShape else { + throw SymbolProviderError("Shape has type .map but is not a MapShape") } - guard case .string(let sdkID) = serviceInfo["sdkId"] else { - throw SymbolProviderError("No sdkId on service trait") + let valueType = try swiftType(shape: mapShape.value.target) + let opt = try NullableIndex().isNonOptional(mapShape.value) ? "" : "?" + return "[Swift.String: \(valueType)\(opt)]" + case .string: + return "Swift.String" + case .boolean: + return "Swift.Bool" + case .byte: + return "Swift.Int8" + case .short: + return "Swift.Int16" + case .integer, .long: + return "Swift.Int" + case .bigInteger: + return "Swift.Int64" + case .float: + return "Swift.Float" + case .double, .bigDecimal: + return "Swift.Double" + case .blob: + return "Foundation.Data" + case .timestamp: + return "Foundation.Date" + 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 sdkID.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "Service", with: "") + return try "\(serviceShape.clientBaseName)Client" + case .member, .operation, .resource: + throw SymbolProviderError("Cannot provide Swift symbol for shape type \(shape.type)") } } - private var inputTraitID = ShapeID("smithy.api", "input") - private var outputTraitID = ShapeID("smithy.api", "output") - private var errorTraitID = ShapeID("smithy.api", "error") - private var operationNameTraitID = ShapeID("swift.synthetic", "operationName") + static let locale = Locale(identifier: "en_US_POSIX") - public func swiftType(shape: Shape) throws -> String { - if case .string(let name) = shape.getTrait(operationNameTraitID), shape.hasTrait(inputTraitID) { - return "\(name)Input" - } else if shape.hasTrait(inputTraitID) { - guard let operation = model.shapes.values - .filter({ $0.type == .operation }) - .map({ $0 as! OperationShape }) // swiftlint:disable:this force_cast - .first(where: { $0.inputShapeID == shape.id }) - else { throw SymbolProviderError("Operation for input \(shape.id) not found") } - return "\(operation.id.name)Input" - } else if - case .string(let name) = shape.getTrait(operationNameTraitID), shape.hasTrait(outputTraitID) { - return "\(name)Output" - } else if shape.hasTrait(outputTraitID) { - guard let operation = model.shapes.values - .filter({ $0.type == .operation }) - .map({ $0 as! OperationShape }) // swiftlint:disable:this force_cast - .first(where: { $0.outputShapeID == shape.id }) - else { throw SymbolProviderError("Operation for output \(shape.id) not found") } - return "\(operation.id.name)Output" - } else if shape.hasTrait(errorTraitID) { - return shape.id.name - } else { - return try "\(serviceName)ClientTypes.\(shape.id.name)" - } + public func operationMethodName(operation: OperationShape) throws -> String { + return operation.id.name.toLowerCamelCase().escapingReservedWords } public func propertyName(shapeID: ShapeID) throws -> String { guard let member = shapeID.member else { throw SymbolProviderError("Shape ID has no member name") } - return member.toLowerCamelCase() + return member.toLowerCamelCase().escapingReservedWords } public func enumCaseName(shapeID: ShapeID) throws -> String { - try propertyName(shapeID: shapeID).toLowerCamelCase().lowercased() + try propertyName(shapeID: shapeID).lowercased().escapingReservedWords + } + + private var modelNamespace: String { + get throws { + try swiftType(shape: service).appending("Types") + } } } -private extension String { +private extension Shape { + + var isTopLevel: Bool { + hasTrait(UsedAsInputTrait.self) || hasTrait(UsedAsOutputTrait.self) || hasTrait(ErrorTrait.self) + } +} + +extension String { func toLowerCamelCase() -> String { let words = splitOnWordBoundaries() // Split into words @@ -83,6 +126,12 @@ private extension String { return firstWord + words.dropFirst().joined() // join lowercased first word to remainder } + func toUpperCamelCase() -> String { + let words = splitOnWordBoundaries() // Split into words + let firstLetter = words.first!.first!.uppercased() // make first letter uppercase + return firstLetter + words.joined().dropFirst() // join uppercased first letter to remainder + } + func splitOnWordBoundaries() -> [String] { // TODO: when nonsupporting platforms are dropped, convert this to Swift-native regex // adapted from Java v2 SDK CodegenNamingUtils.splitOnWordBoundaries @@ -125,7 +174,7 @@ private extension String { private let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") private let underscoreRegex = try! NSRegularExpression(pattern: "_") private let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") -private let largeVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})V([0-9]+)") +private let largeVRegex = try! NSRegularExpression(pattern: "([^A-Z]{2,})V([0-9]+)") private let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") private let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") private let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") diff --git a/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift new file mode 100644 index 000000000..2fa2367e6 --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift @@ -0,0 +1,26 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait +import struct Smithy.TraitError + +public struct DeprecatedTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "deprecated") } + + public let node: Node + public let since: String? + + public init(node: Node) throws { + guard case .object(let object) = node else { + throw TraitError("DeprecatedTrait does not have root object") + } + self.since = object["since"]?.string + self.node = node + } +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift new file mode 100644 index 000000000..7617259e1 --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift @@ -0,0 +1,43 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait +import struct Smithy.TraitError + +public struct EnumTrait: Trait { + + public struct EnumMember { + public let value: String + public let name: String? + } + public static var id: ShapeID { .init("smithy.api", "enum") } + + public let node: Node + public let members: [EnumMember] + + public init(node: Node) throws { + guard let list = node.list else { + throw TraitError("EnumTrait root node is not .list") + } + let members = try list.map { element in + guard let member = element.object else { + throw TraitError("EnumTrait member is not .object") + } + guard let valueNode = member["value"] else { + throw TraitError("EnumTrait member does not have value field") + } + guard let value = valueNode.string else { + throw TraitError("EnumTrait member does not have string for value") + } + return EnumMember(value: value, name: member["name"]?.string) + } + self.node = node + self.members = members + } +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift new file mode 100644 index 000000000..1bcb6abf5 --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +public struct StreamingTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "streaming") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift new file mode 100644 index 000000000..5e1f2d2dc --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +public struct UsedAsInputTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "usedAsInput") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift new file mode 100644 index 000000000..e9772600a --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +public struct UsedAsOutputTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "usedAsOutput") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Tests/SmithyCodegenCoreTests/Resources/smithy-rpcv2-cbor.json b/Tests/SmithyCodegenCoreTests/Resources/smithy-rpcv2-cbor.json new file mode 100644 index 000000000..6737c16ea --- /dev/null +++ b/Tests/SmithyCodegenCoreTests/Resources/smithy-rpcv2-cbor.json @@ -0,0 +1,3622 @@ +{ + "smithy": "2.0", + "metadata": { + "suppressions": [ + { + "id": "HttpMethodSemantics", + "namespace": "com.amazonaws.glacier" + }, + { + "id": "HttpMethodSemantics", + "namespace": "com.amazonaws.s3" + }, + { + "id": "EnumTrait", + "namespace": "com.amazonaws.s3" + }, + { + "id": "UnreferencedShape", + "namespace": "aws.protocoltests.config", + "reason": "These shapes are intended to be used to validate vendorParams in\nprotocol tests, so they naturally will not be connected to a service.\n" + }, + { + "id": "DeprecatedTrait", + "namespace": "*", + "reason": "Some of the AWS protocols make use of deprecated traits, and some are\nthemselves deprecated traits. As this package is intended to test those\nprotocols, the warnings should be suppressed." + } + ], + "validators": [ + { + "configuration": { + "selector": "operation :not(< service)" + }, + "id": "UnboundTestOperation", + "message": "This operation in the AWS protocol tests is not bound to a service.", + "name": "EmitEachSelector", + "namespaces": [ + "aws.protocoltests.json10", + "aws.protocoltests.json", + "aws.protocoltests.query", + "aws.protocoltests.ec2", + "aws.protocoltests.restjson.validation", + "aws.protocoltests.restjson", + "aws.protocoltests.restxml", + "aws.protocoltests.restxml.xmlns", + "com.amazonaws.apigateway", + "com.amazonaws.glacier", + "com.amazonaws.machinelearning", + "com.amazonaws.s3" + ], + "severity": "WARNING" + }, + { + "configuration": { + "selector": "operation :not(< service)" + }, + "id": "UnboundTestOperation", + "message": "This operation in the Smithy protocol tests is not bound to a service.", + "name": "EmitEachSelector", + "namespaces": [ + "smithy.protocoltests.rpcv2Cbor" + ], + "severity": "WARNING" + } + ] + }, + "shapes": { + "smithy.framework#ValidationException": { + "type": "structure", + "members": { + "message": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A summary of the validation failure.", + "smithy.api#required": {} + } + }, + "fieldList": { + "target": "smithy.framework#ValidationExceptionFieldList", + "traits": { + "smithy.api#documentation": "A list of specific failures encountered while validating the input.\nA member can appear in this list more than once if it failed to satisfy multiple constraints." + } + } + }, + "traits": { + "smithy.api#documentation": "A standard error for input validation failures.\nThis should be thrown by services when a member of the input structure\nfalls outside of the modeled or documented constraints.", + "smithy.api#error": "client" + } + }, + "smithy.framework#ValidationExceptionField": { + "type": "structure", + "members": { + "path": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A JSONPointer expression to the structure member whose value failed to satisfy the modeled constraints.", + "smithy.api#required": {} + } + }, + "message": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A detailed description of the validation failure.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "Describes one specific validation failure for an input member." + } + }, + "smithy.framework#ValidationExceptionFieldList": { + "type": "list", + "member": { + "target": "smithy.framework#ValidationExceptionField" + } + }, + "smithy.protocols#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#documentation": "A list of String shapes.", + "smithy.api#private": {} + } + }, + "smithy.protocols#rpcv2Cbor": { + "type": "structure", + "members": { + "http": { + "target": "smithy.protocols#StringList", + "traits": { + "smithy.api#documentation": "Priority ordered list of supported HTTP protocol versions." + } + }, + "eventStreamHttp": { + "target": "smithy.protocols#StringList", + "traits": { + "smithy.api#documentation": "Priority ordered list of supported HTTP protocol versions\nthat are required when using event streams." + } + } + }, + "traits": { + "smithy.api#documentation": "An RPC-based protocol that serializes CBOR payloads.", + "smithy.api#protocolDefinition": { + "traits": [ + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel", + "smithy.api#httpError" + ] + }, + "smithy.api#trait": { + "selector": "service" + }, + "smithy.api#traitValidators": { + "rpcv2Cbor.NoDocuments": { + "selector": "service ~> member :test(> document)", + "message": "This protocol does not support document types in most possible scenarios.", + "severity": "DANGER" + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#ClientOptionalDefaults": { + "type": "structure", + "members": { + "member": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#clientOptional": {}, + "smithy.api#default": 0 + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#ComplexError": { + "type": "structure", + "members": { + "TopLevel": { + "target": "smithy.api#String" + }, + "Nested": { + "target": "smithy.protocoltests.rpcv2Cbor#ComplexNestedErrorData" + } + }, + "traits": { + "smithy.api#documentation": "This error is thrown when a request is invalid.", + "smithy.api#error": "client", + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborComplexError", + "documentation": "Parses a complex error with no message member", + "protocol": "smithy.protocols#rpcv2Cbor", + "params": { + "TopLevel": "Top level", + "Nested": { + "Foo": "bar" + } + }, + "code": 400, + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v2ZfX3R5cGV4K3NtaXRoeS5wcm90b2NvbHRlc3RzLnJwY3YyQ2JvciNDb21wbGV4RXJyb3JoVG9wTGV2ZWxpVG9wIGxldmVsZk5lc3RlZL9jRm9vY2Jhcv//", + "bodyMediaType": "application/cbor" + }, + { + "id": "RpcV2CborEmptyComplexError", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 400, + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v2ZfX3R5cGV4K3NtaXRoeS5wcm90b2NvbHRlc3RzLnJwY3YyQ2JvciNDb21wbGV4RXJyb3L/", + "bodyMediaType": "application/cbor" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#ComplexNestedErrorData": { + "type": "structure", + "members": { + "Foo": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#Defaults": { + "type": "structure", + "members": { + "defaultString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "hi" + } + }, + "defaultBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": true + } + }, + "defaultList": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringList", + "traits": { + "smithy.api#default": [] + } + }, + "defaultTimestamp": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#default": 0 + } + }, + "defaultBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "YWJj" + } + }, + "defaultByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 10 + } + }, + "defaultLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 100 + } + }, + "defaultFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultMap": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringMap", + "traits": { + "smithy.api#default": {} + } + }, + "defaultEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestEnum", + "traits": { + "smithy.api#default": "FOO" + } + }, + "defaultIntEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestIntEnum", + "traits": { + "smithy.api#default": 1 + } + }, + "emptyString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "" + } + }, + "falseBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": false + } + }, + "emptyBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "" + } + }, + "zeroByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 0.0 + } + }, + "zeroDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 0.0 + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseBooleanMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Boolean" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseNumberMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Integer" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseSetMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#StringSet" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseStringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseStructMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#GreetingStruct" + } + }, + "smithy.protocoltests.rpcv2Cbor#EmptyInputOutput": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#EmptyStructure" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#EmptyStructure" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "empty_input", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When Input structure is empty we write CBOR equivalent of {}", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "forbidHeaders": [ + "X-Amz-Target" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/EmptyInputOutput", + "bodyMediaType": "application/cbor", + "body": "v/8=" + }, + { + "id": "empty_input_no_body", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When Input structure is empty the server should accept an empty body", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/EmptyInputOutput", + "bodyMediaType": "application/cbor", + "body": "", + "appliesTo": "server" + }, + { + "id": "empty_input_no_body_has_accept", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When input structure, is empty the server should accept an empty body\neven if the Accept header is set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/EmptyInputOutput", + "bodyMediaType": "application/cbor", + "body": "", + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "empty_output", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When output structure is empty we write CBOR equivalent of {}", + "body": "v/8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200 + }, + { + "id": "empty_output_no_body", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When output structure is empty the client should accept an empty body", + "body": "", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#EmptyStructure": { + "type": "structure", + "members": {} + }, + "smithy.protocoltests.rpcv2Cbor#Float16": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#Float16Output" + }, + "traits": { + "smithy.api#tags": [ + "client-only" + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborFloat16Inf", + "documentation": "Ensures that clients can correctly parse float16 +Inf.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfl8AA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "Infinity" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16NegInf", + "documentation": "Ensures that clients can correctly parse float16 -Inf.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfn8AA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "-Infinity" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16LSBNaN", + "documentation": "Ensures that clients can correctly parse float16 NaN with high LSB.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfl8AQ==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "NaN" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16MSBNaN", + "documentation": "Ensures that clients can correctly parse float16 NaN with high MSB.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfl+AA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "NaN" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16Subnormal", + "documentation": "Ensures that clients can correctly parse a subnormal float16.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1ZfkAUA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": 4.76837158203125E-6 + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#Float16Output": { + "type": "structure", + "members": { + "value": { + "target": "smithy.api#Double" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#FractionalSeconds": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#FractionalSecondsOutput" + }, + "traits": { + "smithy.api#tags": [ + "client-only" + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborDateTimeWithFractionalSeconds", + "documentation": "Ensures that clients can correctly parse timestamps with fractional seconds", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2hkYXRldGltZcH7Qcw32zgPvnf/", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "datetime": 9.46845296123E8 + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#FractionalSecondsOutput": { + "type": "structure", + "members": { + "datetime": { + "target": "smithy.protocoltests.shared#DateTime" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#GreetingWithErrors": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#GreetingWithErrorsOutput" + }, + "errors": [ + { + "target": "smithy.protocoltests.rpcv2Cbor#ComplexError" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#InvalidGreeting" + } + ], + "traits": { + "smithy.api#documentation": "This operation has three possible return values:\n\n1. A successful response in the form of GreetingWithErrorsOutput\n2. An InvalidGreeting error.\n3. A ComplexError error.\n\nImplementations must be able to successfully take a response and\nproperly deserialize successful and error responses.", + "smithy.api#idempotent": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#GreetingWithErrorsOutput": { + "type": "structure", + "members": { + "greeting": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#InvalidGreeting": { + "type": "structure", + "members": { + "Message": { + "target": "smithy.api#String" + } + }, + "traits": { + "smithy.api#documentation": "This error is thrown when an invalid greeting value is provided.", + "smithy.api#error": "client", + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborInvalidGreetingError", + "documentation": "Parses simple RpcV2 Cbor errors", + "protocol": "smithy.protocols#rpcv2Cbor", + "params": { + "Message": "Hi" + }, + "code": 400, + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v2ZfX3R5cGV4LnNtaXRoeS5wcm90b2NvbHRlc3RzLnJwY3YyQ2JvciNJbnZhbGlkR3JlZXRpbmdnTWVzc2FnZWJIaf8=", + "bodyMediaType": "application/cbor" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#NoInputOutput": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.api#Unit" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "no_input", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Body is empty and no Content-Type header if no input", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor" + }, + "forbidHeaders": [ + "Content-Type", + "X-Amz-Target" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/NoInputOutput", + "body": "" + }, + { + "id": "NoInputServerAllowsEmptyCbor", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Servers should accept CBOR empty struct if no input.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/NoInputOutput", + "body": "v/8=", + "appliesTo": "server" + }, + { + "id": "NoInputServerAllowsEmptyBody", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Servers should accept an empty body if there is no input. Additionally,\nthey should not raise an error if the `Accept` header is set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/NoInputOutput", + "body": "", + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "no_output", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "A `Content-Type` header should not be set if the response body is empty.", + "body": "", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor" + }, + "forbidHeaders": [ + "Content-Type" + ], + "code": 200 + }, + { + "id": "NoOutputClientAllowsEmptyCbor", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Clients should accept a CBOR empty struct if there is no output.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v/8=", + "appliesTo": "client" + }, + { + "id": "NoOutputClientAllowsEmptyBody", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Clients should accept an empty body if there is no output and\nshould not raise an error if the `Content-Type` header is set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "", + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#OperationWithDefaults": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsInput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborClientPopulatesDefaultValuesInInput", + "documentation": "Client populates default values in input.", + "protocol": "smithy.protocols#rpcv2Cbor", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "bodyMediaType": "application/cbor", + "body": "v2hkZWZhdWx0c79tZGVmYXVsdFN0cmluZ2JoaW5kZWZhdWx0Qm9vbGVhbvVrZGVmYXVsdExpc3Sf/3BkZWZhdWx0VGltZXN0YW1wwQBrZGVmYXVsdEJsb2JDYWJja2RlZmF1bHRCeXRlAWxkZWZhdWx0U2hvcnQBbmRlZmF1bHRJbnRlZ2VyCmtkZWZhdWx0TG9uZxhkbGRlZmF1bHRGbG9hdPo/gAAAbWRlZmF1bHREb3VibGX6P4AAAGpkZWZhdWx0TWFwv/9rZGVmYXVsdEVudW1jRk9PbmRlZmF1bHRJbnRFbnVtAWtlbXB0eVN0cmluZ2BsZmFsc2VCb29sZWFu9GllbXB0eUJsb2JAaHplcm9CeXRlAGl6ZXJvU2hvcnQAa3plcm9JbnRlZ2VyAGh6ZXJvTG9uZwBpemVyb0Zsb2F0+gAAAABqemVyb0RvdWJsZfoAAAAA//8=", + "params": { + "defaults": {} + } + }, + { + "id": "RpcV2CborClientSkipsTopLevelDefaultValuesInInput", + "documentation": "Client skips top level default values in input.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v/8=", + "params": {} + }, + { + "id": "RpcV2CborClientUsesExplicitlyProvidedMemberValuesOverDefaults", + "documentation": "Client uses explicitly provided member values over defaults", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v2hkZWZhdWx0c7dtZGVmYXVsdFN0cmluZ2NieWVuZGVmYXVsdEJvb2xlYW71a2RlZmF1bHRMaXN0gWFhcGRlZmF1bHRUaW1lc3RhbXDB+z/wAAAAAAAAa2RlZmF1bHRCbG9iQmhpa2RlZmF1bHRCeXRlAmxkZWZhdWx0U2hvcnQCbmRlZmF1bHRJbnRlZ2VyFGtkZWZhdWx0TG9uZxjIbGRlZmF1bHRGbG9hdPpAAAAAbWRlZmF1bHREb3VibGX7QAAAAAAAAABqZGVmYXVsdE1hcKFkbmFtZWRKYWNra2RlZmF1bHRFbnVtY0JBUm5kZWZhdWx0SW50RW51bQJrZW1wdHlTdHJpbmdjZm9vbGZhbHNlQm9vbGVhbvVpZW1wdHlCbG9iQmhpaHplcm9CeXRlAWl6ZXJvU2hvcnQBa3plcm9JbnRlZ2VyAWh6ZXJvTG9uZwFpemVyb0Zsb2F0+j+AAABqemVyb0RvdWJsZfs/8AAAAAAAAP8=", + "params": { + "defaults": { + "defaultString": "bye", + "defaultBoolean": true, + "defaultList": [ + "a" + ], + "defaultTimestamp": 1, + "defaultBlob": "hi", + "defaultByte": 2, + "defaultShort": 2, + "defaultInteger": 20, + "defaultLong": 200, + "defaultFloat": 2.0, + "defaultDouble": 2.0, + "defaultMap": { + "name": "Jack" + }, + "defaultEnum": "BAR", + "defaultIntEnum": 2, + "emptyString": "foo", + "falseBoolean": true, + "emptyBlob": "hi", + "zeroByte": 1, + "zeroShort": 1, + "zeroInteger": 1, + "zeroLong": 1, + "zeroFloat": 1.0, + "zeroDouble": 1.0 + } + } + }, + { + "id": "RpcV2CborServerPopulatesDefaultsWhenMissingInRequestBody", + "documentation": "Server populates default values when missing in request body.", + "appliesTo": "server", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v2hkZWZhdWx0c6D/", + "params": { + "defaults": { + "defaultString": "hi", + "defaultBoolean": true, + "defaultList": [], + "defaultTimestamp": 0, + "defaultBlob": "abc", + "defaultByte": 1, + "defaultShort": 1, + "defaultInteger": 10, + "defaultLong": 100, + "defaultFloat": 1.0, + "defaultDouble": 1.0, + "defaultMap": {}, + "defaultEnum": "FOO", + "defaultIntEnum": 1, + "emptyString": "", + "falseBoolean": false, + "emptyBlob": "", + "zeroByte": 0, + "zeroShort": 0, + "zeroInteger": 0, + "zeroLong": 0, + "zeroFloat": 0.0, + "zeroDouble": 0.0 + }, + "topLevelDefault": "hi", + "otherTopLevelDefault": 0 + } + }, + { + "id": "RpcV2CborClientUsesExplicitlyProvidedValuesInTopLevel", + "documentation": "Any time a value is provided for a member in the top level of input, it is used, regardless of if its the default.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v290b3BMZXZlbERlZmF1bHRiaGl0b3RoZXJUb3BMZXZlbERlZmF1bHQA/w==", + "params": { + "topLevelDefault": "hi", + "otherTopLevelDefault": 0 + } + }, + { + "id": "RpcV2CborClientIgnoresNonTopLevelDefaultsOnMembersWithClientOptional", + "documentation": "Typically, non top-level members would have defaults filled in, but if they have the clientOptional trait, the defaults should be ignored.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v3ZjbGllbnRPcHRpb25hbERlZmF1bHRzoP8=", + "params": { + "clientOptionalDefaults": {} + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", + "documentation": "Client populates default values when missing in response.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v/8=", + "params": { + "defaultString": "hi", + "defaultBoolean": true, + "defaultList": [], + "defaultTimestamp": 0, + "defaultBlob": "abc", + "defaultByte": 1, + "defaultShort": 1, + "defaultInteger": 10, + "defaultLong": 100, + "defaultFloat": 1.0, + "defaultDouble": 1.0, + "defaultMap": {}, + "defaultEnum": "FOO", + "defaultIntEnum": 1, + "emptyString": "", + "falseBoolean": false, + "emptyBlob": "", + "zeroByte": 0, + "zeroShort": 0, + "zeroInteger": 0, + "zeroLong": 0, + "zeroFloat": 0.0, + "zeroDouble": 0.0 + } + }, + { + "id": "RpcV2CborClientIgnoresDefaultValuesIfMemberValuesArePresentInResponse", + "documentation": "Client ignores default values if member values are present in the response.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v21kZWZhdWx0U3RyaW5nY2J5ZW5kZWZhdWx0Qm9vbGVhbvRrZGVmYXVsdExpc3SBYWFwZGVmYXVsdFRpbWVzdGFtcMH7QAAAAAAAAABrZGVmYXVsdEJsb2JCaGlrZGVmYXVsdEJ5dGUCbGRlZmF1bHRTaG9ydAJuZGVmYXVsdEludGVnZXIUa2RlZmF1bHRMb25nGMhsZGVmYXVsdEZsb2F0+kAAAABtZGVmYXVsdERvdWJsZftAAAAAAAAAAGpkZWZhdWx0TWFwoWRuYW1lZEphY2trZGVmYXVsdEVudW1jQkFSbmRlZmF1bHRJbnRFbnVtAmtlbXB0eVN0cmluZ2Nmb29sZmFsc2VCb29sZWFu9WllbXB0eUJsb2JCaGloemVyb0J5dGUBaXplcm9TaG9ydAFremVyb0ludGVnZXIBaHplcm9Mb25nAWl6ZXJvRmxvYXT6P4AAAGp6ZXJvRG91Ymxl+z/wAAAAAAAA/w==", + "params": { + "defaultString": "bye", + "defaultBoolean": false, + "defaultList": [ + "a" + ], + "defaultTimestamp": 2, + "defaultBlob": "hi", + "defaultByte": 2, + "defaultShort": 2, + "defaultInteger": 20, + "defaultLong": 200, + "defaultFloat": 2.0, + "defaultDouble": 2.0, + "defaultMap": { + "name": "Jack" + }, + "defaultEnum": "BAR", + "defaultIntEnum": 2, + "emptyString": "foo", + "falseBoolean": true, + "emptyBlob": "hi", + "zeroByte": 1, + "zeroShort": 1, + "zeroInteger": 1, + "zeroLong": 1, + "zeroFloat": 1.0, + "zeroDouble": 1.0 + } + }, + { + "id": "RpcV2CborServerPopulatesDefaultsInResponseWhenMissingInParams", + "documentation": "Server populates default values in response when missing in params.", + "appliesTo": "server", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v21kZWZhdWx0U3RyaW5nYmhpbmRlZmF1bHRCb29sZWFu9WtkZWZhdWx0TGlzdIBwZGVmYXVsdFRpbWVzdGFtcMH7AAAAAAAAAABrZGVmYXVsdEJsb2JDYWJja2RlZmF1bHRCeXRlAWxkZWZhdWx0U2hvcnQBbmRlZmF1bHRJbnRlZ2VyCmtkZWZhdWx0TG9uZxhkbGRlZmF1bHRGbG9hdPo/gAAAbWRlZmF1bHREb3VibGX7P/AAAAAAAABqZGVmYXVsdE1hcKBrZGVmYXVsdEVudW1jRk9PbmRlZmF1bHRJbnRFbnVtAWtlbXB0eVN0cmluZ2BsZmFsc2VCb29sZWFu9GllbXB0eUJsb2JAaHplcm9CeXRlAGl6ZXJvU2hvcnQAa3plcm9JbnRlZ2VyAGh6ZXJvTG9uZwBpemVyb0Zsb2F0+gAAAABqemVyb0RvdWJsZfsAAAAAAAAAAP8=", + "params": {} + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsInput": { + "type": "structure", + "members": { + "defaults": { + "target": "smithy.protocoltests.rpcv2Cbor#Defaults" + }, + "clientOptionalDefaults": { + "target": "smithy.protocoltests.rpcv2Cbor#ClientOptionalDefaults" + }, + "topLevelDefault": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "hi" + } + }, + "otherTopLevelDefault": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 0 + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsOutput": { + "type": "structure", + "members": { + "defaultString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "hi" + } + }, + "defaultBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": true + } + }, + "defaultList": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringList", + "traits": { + "smithy.api#default": [] + } + }, + "defaultTimestamp": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#default": 0 + } + }, + "defaultBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "YWJj" + } + }, + "defaultByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 10 + } + }, + "defaultLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 100 + } + }, + "defaultFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultMap": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringMap", + "traits": { + "smithy.api#default": {} + } + }, + "defaultEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestEnum", + "traits": { + "smithy.api#default": "FOO" + } + }, + "defaultIntEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestIntEnum", + "traits": { + "smithy.api#default": 1 + } + }, + "emptyString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "" + } + }, + "falseBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": false + } + }, + "emptyBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "" + } + }, + "zeroByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 0.0 + } + }, + "zeroDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 0.0 + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#OptionalInputOutput": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleStructure" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleStructure" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "optional_input", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When input is empty we write CBOR equivalent of {}", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "forbidHeaders": [ + "X-Amz-Target" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/OptionalInputOutput", + "bodyMediaType": "application/cbor", + "body": "v/8=" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "optional_output", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When output is empty we write CBOR equivalent of {}", + "body": "v/8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200 + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapes": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutput" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborRecursiveShapes", + "documentation": "Serializes recursive structures", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RecursiveShapes", + "body": "v2ZuZXN0ZWS/Y2Zvb2RGb28xZm5lc3RlZL9jYmFyZEJhcjFvcmVjdXJzaXZlTWVtYmVyv2Nmb29kRm9vMmZuZXN0ZWS/Y2JhcmRCYXIy//////8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "nested": { + "foo": "Foo1", + "nested": { + "bar": "Bar1", + "recursiveMember": { + "foo": "Foo2", + "nested": { + "bar": "Bar2" + } + } + } + } + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborRecursiveShapes", + "documentation": "Serializes recursive structures", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2ZuZXN0ZWS/Y2Zvb2RGb28xZm5lc3RlZL9jYmFyZEJhcjFvcmVjdXJzaXZlTWVtYmVyv2Nmb29kRm9vMmZuZXN0ZWS/Y2JhcmRCYXIy//////8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "nested": { + "foo": "Foo1", + "nested": { + "bar": "Bar1", + "recursiveMember": { + "foo": "Foo2", + "nested": { + "bar": "Bar2" + } + } + } + } + } + }, + { + "id": "RpcV2CborRecursiveShapesUsingDefiniteLength", + "documentation": "Deserializes recursive structures encoded using a map with definite length", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWZuZXN0ZWSiY2Zvb2RGb28xZm5lc3RlZKJjYmFyZEJhcjFvcmVjdXJzaXZlTWVtYmVyomNmb29kRm9vMmZuZXN0ZWShY2JhcmRCYXIy", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "nested": { + "foo": "Foo1", + "nested": { + "bar": "Bar1", + "recursiveMember": { + "foo": "Foo2", + "nested": { + "bar": "Bar2" + } + } + } + } + }, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutput": { + "type": "structure", + "members": { + "nested": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested1" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested1": { + "type": "structure", + "members": { + "foo": { + "target": "smithy.api#String" + }, + "nested": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested2" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested2": { + "type": "structure", + "members": { + "bar": { + "target": "smithy.api#String" + }, + "recursiveMember": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested1" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMaps": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMapsInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMapsInputOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "The example tests basic map serialization.", + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborMaps", + "documentation": "Serializes maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborDenseMaps", + "body": "oW5kZW5zZVN0cnVjdE1hcKJjZm9voWJoaWV0aGVyZWNiYXqhYmhpY2J5ZQ==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "denseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborSerializesZeroValuesInMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborDenseMaps", + "body": "om5kZW5zZU51bWJlck1hcKFheABvZGVuc2VCb29sZWFuTWFwoWF49A==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "denseNumberMap": { + "x": 0 + }, + "denseBooleanMap": { + "x": false + } + } + }, + { + "id": "RpcV2CborSerializesDenseSetMap", + "documentation": "A request that contains a dense map of sets.", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborDenseMaps", + "body": "oWtkZW5zZVNldE1hcKJheIBheYJhYWFi", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "denseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborMaps", + "documentation": "Deserializes maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oW5kZW5zZVN0cnVjdE1hcKJjZm9voWJoaWV0aGVyZWNiYXqhYmhpY2J5ZQ==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborDeserializesZeroValuesInMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "om5kZW5zZU51bWJlck1hcKFheABvZGVuc2VCb29sZWFuTWFwoWF49A==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseNumberMap": { + "x": 0 + }, + "denseBooleanMap": { + "x": false + } + } + }, + { + "id": "RpcV2CborDeserializesDenseSetMap", + "documentation": "A response that contains a dense map of sets", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWtkZW5zZVNldE1hcKJheIBheYJhYWFi", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + }, + { + "id": "RpcV2CborDeserializesDenseSetMapAndSkipsNull", + "documentation": "Clients SHOULD tolerate seeing a null value in a dense map, and they SHOULD\ndrop the null key-value pair.", + "protocol": "smithy.protocols#rpcv2Cbor", + "appliesTo": "client", + "code": 200, + "body": "oWtkZW5zZVNldE1hcKNheIBheYJhYWFiYXr2", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMapsInputOutput": { + "type": "structure", + "members": { + "denseStructMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseStructMap" + }, + "denseNumberMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseNumberMap" + }, + "denseBooleanMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseBooleanMap" + }, + "denseStringMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseStringMap" + }, + "denseSetMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseSetMap" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborListInputOutput": { + "type": "structure", + "members": { + "stringList": { + "target": "smithy.protocoltests.shared#StringList" + }, + "stringSet": { + "target": "smithy.protocoltests.shared#StringSet" + }, + "integerList": { + "target": "smithy.protocoltests.shared#IntegerList" + }, + "booleanList": { + "target": "smithy.protocoltests.shared#BooleanList" + }, + "timestampList": { + "target": "smithy.protocoltests.shared#TimestampList" + }, + "enumList": { + "target": "smithy.protocoltests.shared#FooEnumList" + }, + "intEnumList": { + "target": "smithy.protocoltests.shared#IntegerEnumList" + }, + "nestedStringList": { + "target": "smithy.protocoltests.shared#NestedStringList" + }, + "structureList": { + "target": "smithy.protocoltests.rpcv2Cbor#StructureList" + }, + "blobList": { + "target": "smithy.protocoltests.shared#BlobList" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborLists": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborListInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborListInputOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "This test case serializes JSON lists for the following cases for both\ninput and output:\n\n1. Normal lists.\n2. Normal sets.\n3. Lists of lists.\n4. Lists of structures.", + "smithy.api#idempotent": {}, + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborLists", + "documentation": "Serializes RpcV2 Cbor lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "v2pzdHJpbmdMaXN0gmNmb29jYmFyaXN0cmluZ1NldIJjZm9vY2JhcmtpbnRlZ2VyTGlzdIIBAmtib29sZWFuTGlzdIL19G10aW1lc3RhbXBMaXN0gsH7QdTX+/OAAADB+0HU1/vzgAAAaGVudW1MaXN0gmNGb29hMGtpbnRFbnVtTGlzdIIBAnBuZXN0ZWRTdHJpbmdMaXN0goJjZm9vY2JhcoJjYmF6Y3F1eG1zdHJ1Y3R1cmVMaXN0gqJhYWExYWJhMqJhYWEzYWJhNGhibG9iTGlzdIJDZm9vQ2Jhcv8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [ + "foo", + "bar" + ], + "stringSet": [ + "foo", + "bar" + ], + "integerList": [ + 1, + 2 + ], + "booleanList": [ + true, + false + ], + "timestampList": [ + 1398796238, + 1398796238 + ], + "enumList": [ + "Foo", + "0" + ], + "intEnumList": [ + 1, + 2 + ], + "nestedStringList": [ + [ + "foo", + "bar" + ], + [ + "baz", + "qux" + ] + ], + "structureList": [ + { + "a": "1", + "b": "2" + }, + { + "a": "3", + "b": "4" + } + ], + "blobList": [ + "foo", + "bar" + ] + } + }, + { + "id": "RpcV2CborListsEmpty", + "documentation": "Serializes empty JSON lists", + "tags": [ + "client-indefinite" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "v2pzdHJpbmdMaXN0n///", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [] + } + }, + { + "id": "RpcV2CborListsEmptyUsingDefiniteLength", + "documentation": "Serializes empty JSON definite length lists", + "tags": [ + "client-definite" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "oWpzdHJpbmdMaXN0gA==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [] + } + }, + { + "id": "RpcV2CborIndefiniteStringInsideIndefiniteList", + "documentation": "Can deserialize indefinite length text strings inside an indefinite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "v2pzdHJpbmdMaXN0n394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborIndefiniteStringInsideDefiniteList", + "documentation": "Can deserialize indefinite length text strings inside a definite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "oWpzdHJpbmdMaXN0g394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborLists", + "documentation": "Serializes RpcV2 Cbor lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2pzdHJpbmdMaXN0n2Nmb29jYmFy/2lzdHJpbmdTZXSfY2Zvb2NiYXL/a2ludGVnZXJMaXN0nwEC/2tib29sZWFuTGlzdJ/19P9tdGltZXN0YW1wTGlzdJ/B+0HU1/vzgAAAwftB1Nf784AAAP9oZW51bUxpc3SfY0Zvb2Ew/2tpbnRFbnVtTGlzdJ8BAv9wbmVzdGVkU3RyaW5nTGlzdJ+fY2Zvb2NiYXL/n2NiYXpjcXV4//9tc3RydWN0dXJlTGlzdJ+/YWFhMWFiYTL/v2FhYTNhYmE0//9oYmxvYkxpc3SfQ2Zvb0NiYXL//w==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [ + "foo", + "bar" + ], + "stringSet": [ + "foo", + "bar" + ], + "integerList": [ + 1, + 2 + ], + "booleanList": [ + true, + false + ], + "timestampList": [ + 1398796238, + 1398796238 + ], + "enumList": [ + "Foo", + "0" + ], + "intEnumList": [ + 1, + 2 + ], + "nestedStringList": [ + [ + "foo", + "bar" + ], + [ + "baz", + "qux" + ] + ], + "structureList": [ + { + "a": "1", + "b": "2" + }, + { + "a": "3", + "b": "4" + } + ], + "blobList": [ + "foo", + "bar" + ] + } + }, + { + "id": "RpcV2CborListsEmpty", + "documentation": "Serializes empty RpcV2 Cbor lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2pzdHJpbmdMaXN0n///", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [] + } + }, + { + "id": "RpcV2CborIndefiniteStringInsideIndefiniteListCanDeserialize", + "documentation": "Can deserialize indefinite length text strings inside an indefinite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2pzdHJpbmdMaXN0n394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborIndefiniteStringInsideDefiniteListCanDeserialize", + "documentation": "Can deserialize indefinite length text strings inside a definite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWpzdHJpbmdMaXN0g394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMaps": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMapsInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMapsInputOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborSparseMaps", + "documentation": "Serializes sparse maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v29zcGFyc2VTdHJ1Y3RNYXC/Y2Zvb79iaGlldGhlcmX/Y2Jher9iaGljYnll////", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborSerializesNullMapValues", + "documentation": "Serializes null map values in sparse maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v3BzcGFyc2VCb29sZWFuTWFwv2F49v9vc3BhcnNlTnVtYmVyTWFwv2F49v9vc3BhcnNlU3RyaW5nTWFwv2F49v9vc3BhcnNlU3RydWN0TWFwv2F49v//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseBooleanMap": { + "x": null + }, + "sparseNumberMap": { + "x": null + }, + "sparseStringMap": { + "x": null + }, + "sparseStructMap": { + "x": null + } + } + }, + { + "id": "RpcV2CborSerializesSparseSetMap", + "documentation": "A request that contains a sparse map of sets", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v2xzcGFyc2VTZXRNYXC/YXif/2F5n2FhYWL///8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + }, + { + "id": "RpcV2CborSerializesSparseSetMapAndRetainsNull", + "documentation": "A request that contains a sparse map of sets.", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v2xzcGFyc2VTZXRNYXC/YXif/2F5n2FhYWL/YXr2//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ], + "z": null + } + } + }, + { + "id": "RpcV2CborSerializesZeroValuesInSparseMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v29zcGFyc2VOdW1iZXJNYXC/YXgA/3BzcGFyc2VCb29sZWFuTWFwv2F49P//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseNumberMap": { + "x": 0 + }, + "sparseBooleanMap": { + "x": false + } + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborSparseJsonMaps", + "documentation": "Deserializes sparse maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v29zcGFyc2VTdHJ1Y3RNYXC/Y2Zvb79iaGlldGhlcmX/Y2Jher9iaGljYnll////", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborDeserializesNullMapValues", + "documentation": "Deserializes null map values", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v3BzcGFyc2VCb29sZWFuTWFwv2F49v9vc3BhcnNlTnVtYmVyTWFwv2F49v9vc3BhcnNlU3RyaW5nTWFwv2F49v9vc3BhcnNlU3RydWN0TWFwv2F49v//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseBooleanMap": { + "x": null + }, + "sparseNumberMap": { + "x": null + }, + "sparseStringMap": { + "x": null + }, + "sparseStructMap": { + "x": null + } + } + }, + { + "id": "RpcV2CborDeserializesSparseSetMap", + "documentation": "A response that contains a sparse map of sets", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2xzcGFyc2VTZXRNYXC/YXmfYWFhYv9heJ////8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + }, + { + "id": "RpcV2CborDeserializesSparseSetMapAndRetainsNull", + "documentation": "A response that contains a sparse map of sets with a null", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2xzcGFyc2VTZXRNYXC/YXif/2F5n2FhYWL/YXr2//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ], + "z": null + } + } + }, + { + "id": "RpcV2CborDeserializesZeroValuesInSparseMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v29zcGFyc2VOdW1iZXJNYXC/YXgA/3BzcGFyc2VCb29sZWFuTWFwv2F49P//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseNumberMap": { + "x": 0 + }, + "sparseBooleanMap": { + "x": false + } + } + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMapsInputOutput": { + "type": "structure", + "members": { + "sparseStructMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseStructMap" + }, + "sparseNumberMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNumberMap" + }, + "sparseBooleanMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseBooleanMap" + }, + "sparseStringMap": { + "target": "smithy.protocoltests.shared#SparseStringMap" + }, + "sparseSetMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseSetMap" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol": { + "type": "service", + "version": "2020-07-14", + "operations": [ + { + "target": "smithy.protocoltests.rpcv2Cbor#EmptyInputOutput" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#Float16" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#FractionalSeconds" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#GreetingWithErrors" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#NoInputOutput" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#OperationWithDefaults" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#OptionalInputOutput" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapes" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMaps" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborLists" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMaps" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleScalarProperties" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNullsOperation" + } + ], + "traits": { + "smithy.api#title": "RpcV2 Protocol Service", + "smithy.protocols#rpcv2Cbor": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SimpleScalarProperties": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleScalarStructure" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleScalarStructure" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborSimpleScalarProperties", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Serializes simple scalar properties", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + } + }, + { + "id": "RpcV2CborSimpleScalarPropertiesUsingIndefiniteLength", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should be capable of deserializing simple scalar properties\nencoded using a map with a definite length. The server should also be able to parse\na key encoded using an indefinite length string.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "qmlieXRlVmFsdWUFf2Zkb3VibGVlVmFsdWX/+z/+OVgQYk3Tf2VmYWxzZWdCb29sZWFuZVZhbHVl//RqZmxvYXRWYWx1ZfpA9AAAbGludGVnZXJWYWx1ZRkBAGlsb25nVmFsdWUZJpFqc2hvcnRWYWx1ZRkmqn9mc3RyaW5nZVZhbHVl/2ZzaW1wbGVwdHJ1ZUJvb2xlYW5WYWx1ZfVpYmxvYlZhbHVlQ2Zvbw==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborClientDoesntSerializeNullStructureValues", + "documentation": "RpcV2 Cbor should not serialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v/8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringValue": null + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborServerDoesntDeSerializeNullStructureValues", + "documentation": "RpcV2 Cbor should not deserialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tzdHJpbmdWYWx1Zfb/", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": {}, + "appliesTo": "server" + }, + { + "id": "RpcV2CborSupportsNaNFloatInputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling NaN float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zft/+AAAAAAAAGpmbG9hdFZhbHVl+n/AAAD/", + "params": { + "doubleValue": "NaN", + "floatValue": "NaN" + } + }, + { + "id": "RpcV2CborSupportsInfinityFloatInputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zft/8AAAAAAAAGpmbG9hdFZhbHVl+n+AAAD/", + "params": { + "doubleValue": "Infinity", + "floatValue": "Infinity" + } + }, + { + "id": "RpcV2CborSupportsNegativeInfinityFloatInputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zfv/8AAAAAAAAGpmbG9hdFZhbHVl+v+AAAD/", + "params": { + "doubleValue": "-Infinity", + "floatValue": "-Infinity" + } + }, + { + "id": "RpcV2CborIndefiniteLengthStringsCanBeDeserialized", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should be capable of deserializing indefinite length text strings.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "oWtzdHJpbmdWYWx1ZX94HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcscSBjaHVua2VkIG9uIGNvbW1h/w==", + "params": { + "stringValue": "An example indefinite string, chunked on comma" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborIndefiniteLengthByteStringsCanBeDeserialized", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should be capable of deserializing indefinite length byte strings.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "oWlibG9iVmFsdWVfWCJBbiBleGFtcGxlIGluZGVmaW5pdGUtYnl0ZSBzdHJpbmcsUSBjaHVua2VkIG9uIGNvbW1h/w==", + "params": { + "blobValue": "An example indefinite-byte string, chunked on comma" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborSupportsUpcastingData", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports upcasting from a smaller byte representation of the same data type.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zfk+AGpmbG9hdFZhbHVl+UegbGludGVnZXJWYWx1ZRg4aWxvbmdWYWx1ZRkBAGpzaG9ydFZhbHVlCv8=", + "params": { + "doubleValue": 1.5, + "floatValue": 7.625, + "integerValue": 56, + "longValue": 256, + "shortValue": 10 + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborExtraFieldsInTheBodyShouldBeSkippedByServers", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should skip over additional fields that are not part of the structure. This allows a\nclient generated against a newer Smithy model to be able to communicate with a server that is\ngenerated against an older Smithy model.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABrZXh0cmFPYmplY3S/c2luZGVmaW5pdGVMZW5ndGhNYXC/a3dpdGhBbkFycmF5nwECA///cWRlZmluaXRlTGVuZ3RoTWFwo3J3aXRoQURlZmluaXRlQXJyYXmDAQIDeB1hbmRTb21lSW5kZWZpbml0ZUxlbmd0aFN0cmluZ3gfdGhhdCBoYXMsIGJlZW4gY2h1bmtlZCBvbiBjb21tYWxub3JtYWxTdHJpbmdjZm9vanNob3J0VmFsdWUZJw9uc29tZU90aGVyRmllbGR2dGhpcyBzaG91bGQgYmUgc2tpcHBlZP9saW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborServersShouldHandleNoAcceptHeader", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Servers should tolerate requests without an Accept header set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborSimpleScalarProperties", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Serializes simple scalar properties", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "bodyMediaType": "application/cbor", + "body": "v3B0cnVlQm9vbGVhblZhbHVl9XFmYWxzZUJvb2xlYW5WYWx1ZfRpYnl0ZVZhbHVlBWtkb3VibGVWYWx1Zfs//jlYEGJN02pmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAanNob3J0VmFsdWUZJqprc3RyaW5nVmFsdWVmc2ltcGxlaWJsb2JWYWx1ZUNmb2//", + "code": 200, + "params": { + "trueBooleanValue": true, + "falseBooleanValue": false, + "byteValue": 5, + "doubleValue": 1.889, + "floatValue": 7.625, + "integerValue": 256, + "shortValue": 9898, + "stringValue": "simple", + "blobValue": "foo" + } + }, + { + "id": "RpcV2CborSimpleScalarPropertiesUsingDefiniteLength", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Deserializes simple scalar properties encoded using a map with definite length", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "bodyMediaType": "application/cbor", + "body": "qXB0cnVlQm9vbGVhblZhbHVl9XFmYWxzZUJvb2xlYW5WYWx1ZfRpYnl0ZVZhbHVlBWtkb3VibGVWYWx1Zfs//jlYEGJN02pmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAanNob3J0VmFsdWUZJqprc3RyaW5nVmFsdWVmc2ltcGxlaWJsb2JWYWx1ZUNmb28=", + "code": 200, + "params": { + "trueBooleanValue": true, + "falseBooleanValue": false, + "byteValue": 5, + "doubleValue": 1.889, + "floatValue": 7.625, + "integerValue": 256, + "shortValue": 9898, + "stringValue": "simple", + "blobValue": "foo" + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborClientDoesntDeserializeNullStructureValues", + "documentation": "RpcV2 Cbor should not deserialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "body": "v2tzdHJpbmdWYWx1Zfb/", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": {}, + "appliesTo": "client" + }, + { + "id": "RpcV2CborServerDoesntSerializeNullStructureValues", + "documentation": "RpcV2 Cbor should not serialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "body": "v/8=", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringValue": null + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborSupportsNaNFloatOutputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling NaN float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zft/+AAAAAAAAGpmbG9hdFZhbHVl+n/AAAD/", + "params": { + "doubleValue": "NaN", + "floatValue": "NaN" + } + }, + { + "id": "RpcV2CborSupportsInfinityFloatOutputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zft/8AAAAAAAAGpmbG9hdFZhbHVl+n+AAAD/", + "params": { + "doubleValue": "Infinity", + "floatValue": "Infinity" + } + }, + { + "id": "RpcV2CborSupportsNegativeInfinityFloatOutputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Negative Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zfv/8AAAAAAAAGpmbG9hdFZhbHVl+v+AAAD/", + "params": { + "doubleValue": "-Infinity", + "floatValue": "-Infinity" + } + }, + { + "id": "RpcV2CborSupportsUpcastingDataOnDeserialize", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports upcasting from a smaller byte representation of the same data type.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zfk+AGpmbG9hdFZhbHVl+UegbGludGVnZXJWYWx1ZRg4aWxvbmdWYWx1ZRkBAGpzaG9ydFZhbHVlCv8=", + "params": { + "doubleValue": 1.5, + "floatValue": 7.625, + "integerValue": 56, + "longValue": 256, + "shortValue": 10 + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborExtraFieldsInTheBodyShouldBeSkippedByClients", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The client should skip over additional fields that are not part of the structure. This allows a\nclient generated against an older Smithy model to be able to communicate with a server that is\ngenerated against a newer Smithy model.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABrZXh0cmFPYmplY3S/c2luZGVmaW5pdGVMZW5ndGhNYXC/a3dpdGhBbkFycmF5nwECA///cWRlZmluaXRlTGVuZ3RoTWFwo3J3aXRoQURlZmluaXRlQXJyYXmDAQIDeB1hbmRTb21lSW5kZWZpbml0ZUxlbmd0aFN0cmluZ3gfdGhhdCBoYXMsIGJlZW4gY2h1bmtlZCBvbiBjb21tYWxub3JtYWxTdHJpbmdjZm9vanNob3J0VmFsdWUZJw9uc29tZU90aGVyRmllbGR2dGhpcyBzaG91bGQgYmUgc2tpcHBlZP9saW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#SimpleScalarStructure": { + "type": "structure", + "members": { + "trueBooleanValue": { + "target": "smithy.api#Boolean" + }, + "falseBooleanValue": { + "target": "smithy.api#Boolean" + }, + "byteValue": { + "target": "smithy.api#Byte" + }, + "doubleValue": { + "target": "smithy.api#Double" + }, + "floatValue": { + "target": "smithy.api#Float" + }, + "integerValue": { + "target": "smithy.api#Integer" + }, + "longValue": { + "target": "smithy.api#Long" + }, + "shortValue": { + "target": "smithy.api#Short" + }, + "stringValue": { + "target": "smithy.api#String" + }, + "blobValue": { + "target": "smithy.api#Blob" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#SimpleStructure": { + "type": "structure", + "members": { + "value": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseBooleanMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Boolean" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseNullsOperation": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNullsOperationInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNullsOperationInputOutput" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborSparseMapsSerializeNullValues", + "documentation": "Serializes null values in maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v29zcGFyc2VTdHJpbmdNYXC/Y2Zvb/b//w==", + "params": { + "sparseStringMap": { + "foo": null + } + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SparseNullsOperation" + }, + { + "id": "RpcV2CborSparseListsSerializeNull", + "documentation": "Serializes null values in lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v3BzcGFyc2VTdHJpbmdMaXN0n/b//w==", + "params": { + "sparseStringList": [ + null + ] + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SparseNullsOperation" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborSparseMapsDeserializeNullValues", + "documentation": "Deserializes null values in maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v29zcGFyc2VTdHJpbmdNYXC/Y2Zvb/b//w==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseStringMap": { + "foo": null + } + } + }, + { + "id": "RpcV2CborSparseListsDeserializeNull", + "documentation": "Deserializes null values in lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v3BzcGFyc2VTdHJpbmdMaXN0n/b//w==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseStringList": [ + null + ] + } + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseNullsOperationInputOutput": { + "type": "structure", + "members": { + "sparseStringList": { + "target": "smithy.protocoltests.shared#SparseStringList" + }, + "sparseStringMap": { + "target": "smithy.protocoltests.shared#SparseStringMap" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseNumberMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Integer" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseSetMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#StringSet" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseStructMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#GreetingStruct" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#StructureList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.rpcv2Cbor#StructureListMember" + } + }, + "smithy.protocoltests.rpcv2Cbor#StructureListMember": { + "type": "structure", + "members": { + "a": { + "target": "smithy.api#String" + }, + "b": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#TestEnum": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FOO" + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAR" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAZ" + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#TestIntEnum": { + "type": "intEnum", + "members": { + "ONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 1 + } + }, + "TWO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 2 + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#TestStringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.rpcv2Cbor#TestStringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.shared#BlobList": { + "type": "list", + "member": { + "target": "smithy.api#Blob" + } + }, + "smithy.protocoltests.shared#BooleanList": { + "type": "list", + "member": { + "target": "smithy.api#Boolean" + } + }, + "smithy.protocoltests.shared#DateTime": { + "type": "timestamp", + "traits": { + "smithy.api#timestampFormat": "date-time" + } + }, + "smithy.protocoltests.shared#FooEnum": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Foo" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Baz" + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Bar" + } + }, + "ONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "1" + } + }, + "ZERO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "0" + } + } + } + }, + "smithy.protocoltests.shared#FooEnumList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.shared#FooEnum" + } + }, + "smithy.protocoltests.shared#GreetingStruct": { + "type": "structure", + "members": { + "hi": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.shared#IntegerEnum": { + "type": "intEnum", + "members": { + "A": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 1 + } + }, + "B": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 2 + } + }, + "C": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 3 + } + } + } + }, + "smithy.protocoltests.shared#IntegerEnumList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.shared#IntegerEnum" + } + }, + "smithy.protocoltests.shared#IntegerList": { + "type": "list", + "member": { + "target": "smithy.api#Integer" + } + }, + "smithy.protocoltests.shared#NestedStringList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.shared#StringList" + }, + "traits": { + "smithy.api#documentation": "A list of lists of strings." + } + }, + "smithy.protocoltests.shared#SparseStringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.shared#SparseStringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.shared#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.shared#StringSet": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#uniqueItems": {} + } + }, + "smithy.protocoltests.shared#TimestampList": { + "type": "list", + "member": { + "target": "smithy.api#Timestamp" + } + }, + "smithy.test#AppliesTo": { + "type": "enum", + "members": { + "CLIENT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "The test only applies to client implementations.", + "smithy.api#enumValue": "client" + } + }, + "SERVER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "The test only applies to server implementations.", + "smithy.api#enumValue": "server" + } + } + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#HttpRequestTestCase": { + "type": "structure", + "members": { + "id": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The identifier of the test case. This identifier can be used by\nprotocol test implementations to filter out unsupported test\ncases by ID, to generate test case names, etc. The provided `id`\nMUST match Smithy's `identifier` ABNF. No two `httpRequestTests`\ntest cases can share the same ID.", + "smithy.api#pattern": "^[A-Za-z_][A-Za-z0-9_]+$", + "smithy.api#required": {} + } + }, + "protocol": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The name of the protocol to test.", + "smithy.api#idRef": { + "selector": "[trait|protocolDefinition]", + "failWhenMissing": true + }, + "smithy.api#required": {} + } + }, + "method": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The expected serialized HTTP request method.", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#required": {} + } + }, + "uri": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The request-target of the HTTP request, not including\nthe query string (for example, \"/foo/bar\").", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#required": {} + } + }, + "host": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The host / endpoint provided to the client, not including the path\nor scheme (for example, \"example.com\")." + } + }, + "resolvedHost": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The host / endpoint that the client should send to, not including\nthe path or scheme (for example, \"prefix.example.com\").\n\nThis can differ from the host provided to the client if the `hostPrefix`\nmember of the `endpoint` trait is set, for instance." + } + }, + "authScheme": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The optional authentication scheme shape ID to assume. It's\npossible that specific authentication schemes might influence\nthe serialization logic of an HTTP request.", + "smithy.api#idRef": { + "selector": "[trait|authDefinition]", + "failWhenMissing": true + } + } + }, + "queryParams": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of the expected serialized query string parameters.\n\nEach element in the list is a query string key value pair\nthat starts with the query string parameter name optionally\nfollowed by \"=\", optionally followed by the query string\nparameter value. For example, \"foo=bar\", \"foo=\", and \"foo\"\nare all valid values. The query string parameter name and\nthe value MUST appear in the format in which it is expected\nto be sent over the wire; if a key or value needs to be\npercent-encoded, then it MUST appear percent-encoded in this list.\n\nA serialized HTTP request is not in compliance with the protocol\nif any query string parameter defined in `queryParams` is not\ndefined in the request or if the value of a query string parameter\nin the request differs from the expected value.\n\n`queryParams` applies no constraints on additional query parameters." + } + }, + "forbidQueryParams": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of query string parameter names that must not appear in the\nserialized HTTP request.\n\nEach value MUST appear in the format in which it is sent over the\nwire; if a key needs to be percent-encoded, then it MUST appear\npercent-encoded in this list." + } + }, + "requireQueryParams": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of query string parameter names that MUST appear in the\nserialized request URI, but no assertion is made on the value.\n\nEach value MUST appear in the format in which it is sent over the\nwire; if a key needs to be percent-encoded, then it MUST appear\npercent-encoded in this list." + } + }, + "headers": { + "target": "smithy.test#StringMap", + "traits": { + "smithy.api#documentation": "Defines a map of expected HTTP headers.\n\nHeaders that are not listed in this map are ignored unless they are\nexplicitly forbidden through `forbidHeaders`." + } + }, + "forbidHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must not appear in the serialized\nHTTP request." + } + }, + "requireHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must appear in the serialized\nHTTP message, but no assertion is made on the value.\n\nHeaders listed in `headers` do not need to appear in this list." + } + }, + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The expected HTTP message body.\n\nIf no request body is defined, then no assertions are made about\nthe body of the message." + } + }, + "bodyMediaType": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The media type of the `body`.\n\nThis is used to help test runners to parse and validate the expected\ndata against generated data." + } + }, + "params": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines the input parameters used to generated the HTTP request.\n\nThese parameters MUST be compatible with the input of the operation." + } + }, + "vendorParams": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines vendor-specific parameters that are used to influence the\nrequest. For example, some vendors might utilize environment\nvariables, configuration files on disk, or other means to influence\nthe serialization formats used by clients or servers.\n\nIf a `vendorParamsShape` is set, these parameters MUST be compatible\nwith that shape's definition." + } + }, + "vendorParamsShape": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A shape to be used to validate the `vendorParams` member contents.\n\nIf set, the parameters in `vendorParams` MUST be compatible with this\nshape's definition.", + "smithy.api#idRef": { + "failWhenMissing": true + } + } + }, + "documentation": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A description of the test and what is being asserted." + } + }, + "tags": { + "target": "smithy.test#NonEmptyStringList", + "traits": { + "smithy.api#documentation": "Applies a list of tags to the test." + } + }, + "appliesTo": { + "target": "smithy.test#AppliesTo", + "traits": { + "smithy.api#documentation": "Indicates that the test case is only to be implemented by \"client\" or\n\"server\" implementations. This property is useful for identifying and\ntesting edge cases of clients and servers that are impossible or\nundesirable to test in *both* client and server implementations." + } + } + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#HttpResponseTestCase": { + "type": "structure", + "members": { + "id": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The identifier of the test case. This identifier can be used by\nprotocol test implementations to filter out unsupported test\ncases by ID, to generate test case names, etc. The provided `id`\nMUST match Smithy's `identifier` ABNF. No two `httpResponseTests`\ntest cases can share the same ID.", + "smithy.api#pattern": "^[A-Za-z_][A-Za-z0-9_]+$", + "smithy.api#required": {} + } + }, + "protocol": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The shape ID of the protocol to test.", + "smithy.api#idRef": { + "selector": "[trait|protocolDefinition]", + "failWhenMissing": true + }, + "smithy.api#required": {} + } + }, + "code": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "Defines the HTTP response code.", + "smithy.api#range": { + "min": 100, + "max": 599 + }, + "smithy.api#required": {} + } + }, + "authScheme": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The optional authentication scheme shape ID to assume. It's possible\nthat specific authentication schemes might influence the serialization\nlogic of an HTTP response.", + "smithy.api#idRef": { + "selector": "[trait|authDefinition]", + "failWhenMissing": true + } + } + }, + "headers": { + "target": "smithy.test#StringMap", + "traits": { + "smithy.api#documentation": "A map of expected HTTP headers. Each key represents a header field\nname and each value represents the expected header value. An HTTP\nresponse is not in compliance with the protocol if any listed header\nis missing from the serialized response or if the expected header\nvalue differs from the serialized response value.\n\n`headers` applies no constraints on additional headers." + } + }, + "forbidHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must not appear." + } + }, + "requireHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must appear in the serialized\nHTTP message, but no assertion is made on the value.\n\nHeaders listed in `headers` map do not need to appear in this list." + } + }, + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Defines the HTTP message body.\n\nIf no response body is defined, then no assertions are made about\nthe body of the message." + } + }, + "bodyMediaType": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The media type of the `body`.\n\nThis is used to help test runners to parse and validate the expected\ndata against generated data. Binary media type formats require that\nthe contents of `body` are base64 encoded." + } + }, + "params": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines the output parameters deserialized from the HTTP response.\n\nThese parameters MUST be compatible with the output of the operation." + } + }, + "vendorParams": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines vendor-specific parameters that are used to influence the\nresponse. For example, some vendors might utilize environment\nvariables, configuration files on disk, or other means to influence\nthe serialization formats used by clients or servers.\n\nIf a `vendorParamsShape` is set, these parameters MUST be compatible\nwith that shape's definition." + } + }, + "vendorParamsShape": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A shape to be used to validate the `vendorParams` member contents.\n\nIf set, the parameters in `vendorParams` MUST be compatible with this\nshape's definition.", + "smithy.api#idRef": { + "failWhenMissing": true + } + } + }, + "documentation": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A description of the test and what is being asserted." + } + }, + "tags": { + "target": "smithy.test#NonEmptyStringList", + "traits": { + "smithy.api#documentation": "Applies a list of tags to the test." + } + }, + "appliesTo": { + "target": "smithy.test#AppliesTo", + "traits": { + "smithy.api#documentation": "Indicates that the test case is only to be implemented by \"client\" or\n\"server\" implementations. This property is useful for identifying and\ntesting edge cases of clients and servers that are impossible or\nundesirable to test in *both* client and server implementations." + } + } + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#NonEmptyString": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1 + }, + "smithy.api#private": {} + } + }, + "smithy.test#NonEmptyStringList": { + "type": "list", + "member": { + "target": "smithy.test#NonEmptyString" + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#StringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#httpRequestTests": { + "type": "list", + "member": { + "target": "smithy.test#HttpRequestTestCase" + }, + "traits": { + "smithy.api#documentation": "Define how an HTTP request is serialized given a specific protocol,\nauthentication scheme, and set of input parameters.", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#trait": { + "selector": "operation" + } + } + }, + "smithy.test#httpResponseTests": { + "type": "list", + "member": { + "target": "smithy.test#HttpResponseTestCase" + }, + "traits": { + "smithy.api#documentation": "Define how an HTTP response is serialized given a specific protocol,\nauthentication scheme, and set of output or error parameters.", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#trait": { + "selector": ":test(operation, structure[trait|error])" + } + } + } + } +} diff --git a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift new file mode 100644 index 000000000..0c26ed377 --- /dev/null +++ b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift @@ -0,0 +1,36 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import Smithy +@testable import SmithyCodegenCore + +class SmithyCodegenCoreTests: XCTestCase { + + // This test runs against the real model for smithy-rpcv2-cbor protocol tests, + // which is embedded in this test bundle as a resource. + // + // When the code generator runs as part of the build system, + // the debugger does not attach and the only indication you have of what went wrong + // is the error message that gets written to the build logs. + // + // Running the code generator from here allows the debugger to attach so that you + // 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( + service: "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", + 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"), + typeRegistryFileURL: tempDirURL.appendingPathComponent("TypeRegistry.swift"), + operationsFileURL: tempDirURL.appendingPathComponent("Operations.swift") + ) + try generator.run() + } +} 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 3ad36bb44..25156c711 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 @@ -80,7 +80,6 @@ class DirectedSwiftCodegen( generateMessageMarshallable(ctx) generateMessageUnmarshallable(ctx) generateCodableConformanceForNestedTypes(ctx) - generateSchemas(ctx) initializeMiddleware(ctx) @@ -97,6 +96,9 @@ 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 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 index 0662d8d7d..6daae0e29 100644 --- 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 @@ -1,24 +1,19 @@ package software.amazon.smithy.swift.codegen -import software.amazon.smithy.aws.traits.ServiceTrait import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.model.getTrait class SmithyModelFileInfoGenerator( val ctx: ProtocolGenerator.GenerationContext, ) { fun writeSmithyModelFileInfo() { - ctx.service.getTrait()?.let { serviceTrait -> - val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" - val modelFileName = - serviceTrait - .sdkId - .lowercase() - .replace(",", "") - .replace(" ", "-") - val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" - ctx.delegator.useFileWriter(filename) { writer -> - writer.write("{\"path\":\"$contents\"}") + 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 7ee6846ce..3d3c09412 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 @@ -20,8 +20,10 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.protocol.traits.Rpcv2CborTrait +import software.amazon.smithy.swift.codegen.utils.clientName +import software.amazon.smithy.swift.codegen.utils.sdkId import java.util.logging.Logger -import kotlin.streams.toList +import kotlin.jvm.optionals.getOrNull private const val SERVICE = "service" private const val MODULE_NAME = "module" @@ -38,6 +40,7 @@ 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 MODEL_PATH = "modelPath" // Prioritized list of protocols supported for code generation private val DEFAULT_PROTOCOL_RESOLUTION_PRIORITY = @@ -65,6 +68,7 @@ class SwiftSettings( val copyrightNotice: String, val visibility: String, val internalClient: Boolean, + val modelPath: String, ) { companion object { private val LOGGER: Logger = Logger.getLogger(SwiftSettings::class.java.name) @@ -96,6 +100,7 @@ class SwiftSettings( COPYRIGHT_NOTICE, VISIBILITY, INTERNAL_CLIENT, + MODEL_PATH, ), ) @@ -104,6 +109,7 @@ class SwiftSettings( .getStringMember(SERVICE) .map(StringNode::expectShapeId) .orElseGet { inferService(model) } + val service = model.getShape(serviceId).getOrNull() as? ServiceShape val moduleName = config.expectStringMember(MODULE_NAME).value val version = config.expectStringMember(MODULE_VERSION).value @@ -112,7 +118,7 @@ class SwiftSettings( val author = config.expectStringMember(AUTHOR).value val gitRepo = config.expectStringMember(GIT_REPO).value val swiftVersion = config.expectStringMember(SWIFT_VERSION).value - val sdkId = sanitizeSdkId(config.getStringMemberOrDefault(SDK_ID, serviceId.name)) + val sdkId = config.getStringMemberOrDefault(SDK_ID, service?.sdkId ?: serviceId.name) val mergeModels = config.getBooleanMemberOrDefault(MERGE_MODELS) val copyrightNotice = config.getStringMemberOrDefault( @@ -121,6 +127,7 @@ class SwiftSettings( ) val visibility = config.getStringMemberOrDefault(VISIBILITY, "public") val internalClient = config.getBooleanMemberOrDefault(INTERNAL_CLIENT, false) + val modelPath = config.getStringMemberOrDefault(MODEL_PATH, "Sources/$moduleName/model.json") return SwiftSettings( serviceId, @@ -136,11 +143,10 @@ class SwiftSettings( copyrightNotice, visibility, internalClient, + modelPath, ) } - private fun sanitizeSdkId(sdkId: String): String = sdkId.removeSuffix(" Service") - // infer the service to generate from a model private fun inferService(model: Model): ShapeId { val services = @@ -219,13 +225,20 @@ class SwiftSettings( "The following protocol generators were found on the class path: $supportedProtocolTraits", ) } + + val testModuleName: String + get() = "${moduleName}Tests" + + val sdkIdStrippingService: String + get() = sdkId.removeSuffix(" Service") + + val clientBaseNamePreservingService: String + get() = sdkId.clientName() + + val clientBaseName: String + get() = sdkIdStrippingService.clientName() } class UnresolvableProtocolException( message: String, ) : CodegenException(message) - -val SwiftSettings.testModuleName: String - get() { - return "${this.moduleName}Tests" - } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt index cf51fd5c3..cbd1249bb 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt @@ -64,7 +64,6 @@ import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTimestampsTypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes import software.amazon.smithy.swift.codegen.utils.ModelFileUtils -import software.amazon.smithy.swift.codegen.utils.clientName import software.amazon.smithy.swift.codegen.utils.toLowerCamelCase import software.amazon.smithy.utils.StringUtils.lowerCase import java.util.logging.Logger @@ -79,7 +78,6 @@ class SwiftSymbolProvider( val swiftSettings: SwiftSettings, ) : SymbolProvider, ShapeVisitor { - private val sdkId = swiftSettings.sdkId private val service: ServiceShape? = try { swiftSettings.getService(model) @@ -253,7 +251,7 @@ class SwiftSymbolProvider( .build() override fun serviceShape(shape: ServiceShape): Symbol { - val name = sdkId.clientName() + val name = swiftSettings.clientBaseName return createSymbolBuilder(shape, "${name}Client", SwiftDeclaration.CLASS) .definitionFile(formatModuleName(name)) .build() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt index fed20bd5b..c70774321 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt @@ -13,7 +13,6 @@ import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.middleware.MiddlewareExecutionGenerator import software.amazon.smithy.swift.codegen.middleware.OperationMiddleware import software.amazon.smithy.swift.codegen.model.toUpperCamelCase -import software.amazon.smithy.swift.codegen.utils.toUpperCamelCase /** * Renders an implementation of a service interface for HTTP protocol @@ -46,10 +45,10 @@ open class HttpProtocolClientGenerator( val operationsIndex = OperationIndex.of(model) writer.openBlock("extension \$L {", "}", serviceSymbol.name) { - val serviceName = ctx.settings.sdkId.toUpperCamelCase() + val clientName = ctx.settings.clientBaseName operations.forEach { operation -> ServiceGenerator.renderOperationDefinition( - serviceName, + clientName, model, serviceShape, symbolProvider, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt index 99ba1847e..2702fbfdc 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt @@ -20,7 +20,7 @@ open class HttpProtocolServiceClient( private val writer: SwiftWriter, private val serviceConfig: ServiceConfig, ) { - private val serviceName: String = ctx.settings.sdkId + private val serviceName: String = ctx.settings.sdkIdStrippingService open val clientProtocolSymbol: Symbol = ClientRuntimeTypes.Core.Client diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt index 0ff2e5ad1..00869532d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt @@ -14,7 +14,6 @@ import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait import software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.model.toUpperCamelCase -import software.amazon.smithy.swift.codegen.testModuleName import java.util.TreeSet import java.util.logging.Logger diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt index ea7ca26da..feb94f0ba 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt @@ -21,7 +21,6 @@ import software.amazon.smithy.swift.codegen.integration.SmithyHttpProtocolClient import software.amazon.smithy.swift.codegen.integration.middlewares.OperationEndpointResolverMiddleware import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable import software.amazon.smithy.swift.codegen.model.getTrait -import software.amazon.smithy.swift.codegen.testModuleName abstract class SmithyHTTPBindingProtocolGenerator( customizations: HTTPProtocolCustomizable, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/ServiceShapeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/ServiceShapeUtils.kt new file mode 100644 index 000000000..74c1743b8 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/ServiceShapeUtils.kt @@ -0,0 +1,9 @@ +package software.amazon.smithy.swift.codegen.utils + +import software.amazon.smithy.aws.traits.ServiceTrait +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.swift.codegen.model.getTrait + +// Utility function for returning sdkId from service +val ServiceShape.sdkId: String? + get() = getTrait()?.sdkId diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt index 981407bb7..c81a2cc0b 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt @@ -194,6 +194,7 @@ class SwiftSettingsTest { copyrightNotice = "// Test copyright", visibility = "public", internalClient = false, + modelPath = "/path/to/model.json", ) private fun createServiceWithProtocols(protocols: Set): ServiceShape { From 52adbb5b3c32d8c216ab32c53c578c884fbb7fd7 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 19 Jan 2026 20:09:06 -0600 Subject: [PATCH 18/29] Fix tests, remove unneeded code --- .../SmithyCodegenCore/GenerationContext.swift | 4 +- .../NullableIndex/NullableIndex.swift | 76 -------- .../SmithyCodegenCore/Shape/EnumShape.swift | 4 - .../SwiftRendering/String+Utils.swift | 64 ++++++ .../SymbolProvider/String+ReservedWords.swift | 125 ------------ .../SymbolProvider/SymbolProvider.swift | 182 ------------------ .../SymbolProvider/SymbolProviderError.swift | 14 -- .../SmithyCodegenCoreTests.swift | 6 +- 8 files changed, 67 insertions(+), 408 deletions(-) delete mode 100644 Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift delete mode 100644 Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift delete mode 100644 Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift delete mode 100644 Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift index 2141831b1..838321db6 100644 --- a/Sources/SmithyCodegenCore/GenerationContext.swift +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -10,7 +10,7 @@ import struct Smithy.ShapeID public struct GenerationContext { public let service: ServiceShape public let model: Model - public let symbolProvider: SymbolProvider +// public let symbolProvider: SymbolProvider /// Creates a ``GenerationContext`` from a model. /// @@ -31,6 +31,6 @@ public struct GenerationContext { // Initialize using the final, processed model self.service = try finalModel.expectServiceShape(id: serviceID) self.model = finalModel - self.symbolProvider = SymbolProvider(service: service, model: finalModel) +// self.symbolProvider = SymbolProvider(service: service, model: finalModel) } } diff --git a/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift b/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift deleted file mode 100644 index 0812ddda3..000000000 --- a/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import struct Smithy.AddedDefaultTrait -import struct Smithy.ClientOptionalTrait -import struct Smithy.DefaultTrait -import struct Smithy.InputTrait -import struct Smithy.ShapeID -import enum Smithy.ShapeType -import struct Smithy.SparseTrait - -struct NullableIndex { - - /// Determines whether a structure member should be rendered as non-optional. - /// - Parameter memberShape: The member for which optionality is being determined - /// - Returns: `true` if the member should be non-optional, `false` otherwise - func isNonOptional(_ memberShape: MemberShape) throws -> Bool { - let container = try memberShape.container - let target = try memberShape.target - - // Note that these are the current rules in use by smithy-swift. They are not Smithy 2.0 "correct". - - // If the container is a list/set, member is nonoptional unless sparse trait is applied - if [ShapeType.list, .set].contains(container.type), memberShape.id.member == "member" { - return !container.hasTrait(SparseTrait.self) - } - - // If the container is a map, value is nonoptional unless sparse trait is applied - if container.type == .map { - if memberShape.id.member == "value" { - return !container.hasTrait(SparseTrait.self) - } else { - // key is always non-optional - return true - } - } - - // If the containing shape has the input trait, it's definitely optional - if container.hasTrait(InputTrait.self) { - return false - } - - // If the member has the clientOptional trait, it's definitely optional - if memberShape.hasTrait(ClientOptionalTrait.self) { - return false - } - - // If the member has the addedDefault trait, it's definitely optional - if memberShape.hasTrait(AddedDefaultTrait.self) { - return false - } - - // Only number & Boolean types are allowed to be non-optional - let allowedTypes = - [ShapeType.boolean, .bigDecimal, .bigInteger, .byte, .double, .float, .intEnum, .integer, .long, .short] - guard allowedTypes.contains(target.type) else { return false } - - // Check if there is a default trait with a zero/false value. If so, member is non-optional. - let memberDefaultTrait = try memberShape.getTrait(DefaultTrait.self) - let targetDefaultTrait = try target.getTrait(DefaultTrait.self) - guard let defaultNode = (memberDefaultTrait ?? targetDefaultTrait)?.node else { - return false - } - if target.type == .boolean, let bool = defaultNode.boolean, !bool { - return true - } else if let number = defaultNode.number, number == 0.0 { - return true - } else { - return false - } - } -} diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift index 5a6ad3d4a..f85d3dd92 100644 --- a/Sources/SmithyCodegenCore/Shape/EnumShape.swift +++ b/Sources/SmithyCodegenCore/Shape/EnumShape.swift @@ -29,8 +29,4 @@ public class EnumShape: Shape, HasMembers { override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { try Set(members) } - - func trimmingMembers(onlyTarget: Set) -> Shape { - return self - } } diff --git a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift index 3d3c21e87..ae2948d12 100644 --- a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift +++ b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift @@ -5,6 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Foundation.Locale +import struct Foundation.NSRange +import class Foundation.NSRegularExpression + extension String { /// Escapes special characters in the string, then surrounds it in double quotes @@ -29,4 +33,64 @@ extension String { let firstChar = self.first?.uppercased() ?? "" return "\(firstChar)\(self.dropFirst())" } + + func toLowerCamelCase() -> String { + let words = splitOnWordBoundaries() // Split into words + let firstWord = words.first!.lowercased() // make first word lowercase + return firstWord + words.dropFirst().joined() // join lowercased first word to remainder + } + + func toUpperCamelCase() -> String { + let words = splitOnWordBoundaries() // Split into words + let firstLetter = words.first!.first!.uppercased() // make first letter uppercase + return firstLetter + words.joined().dropFirst() // join uppercased first letter to remainder + } + + private func splitOnWordBoundaries() -> [String] { + // TODO: when nonsupporting platforms are dropped, convert this to Swift-native regex + // adapted from Java v2 SDK CodegenNamingUtils.splitOnWordBoundaries + var result = self + + // all non-alphanumeric characters: "acm-success"-> "acm success" + result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" + result = underscoreRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " _ ") + + // if a number has a standalone v or V in front of it, separate it out + result = smallVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 v$2") + result = largeVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 V$2") + + // add a space between camelCased words + result = camelCaseSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // add a space after acronyms + result = acronymSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // add space after a number in the middle of a word + result = spaceAfterNumberRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // remove extra spaces - multiple consecutive ones or those and the beginning/end of words + result = removeExtraSpaceRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + .trimmingCharacters(in: .whitespaces) + + return result.components(separatedBy: " ") + } + + private var range: NSRange { + NSRange(location: 0, length: count) + } } + +// Regexes used in splitOnWordBoundaries() above. +// force_try linter rule is disabled since these are just created from static strings. +// swiftlint:disable force_try +private let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") +private let underscoreRegex = try! NSRegularExpression(pattern: "_") +private let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") +private let largeVRegex = try! NSRegularExpression(pattern: "([^A-Z]{2,})V([0-9]+)") +private let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") +private let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") +private let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") +private let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") +// swiftlint:enable force_try diff --git a/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift b/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift deleted file mode 100644 index b4a25a5a9..000000000 --- a/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -extension String { - - /// Modifies Swift reserved words that are used in Smithy models so that they can be safely used as identifiers in rendered Swift. - /// - /// Taken from `ReservedWords.kt`: - /// https://github.com/smithy-lang/smithy-swift/blob/main/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/lang/ReservedWords.kt - var escapingReservedWords: String { - if self == "Protocol" || self == "Type" { - // Swift metatypes - "Model\(self)" - } else if reservedWords.contains(self) { - // Surround reserved words in backticks to force compiler - // to treat them as identifiers - self.inBackticks - } else { - self - } - } -} - -private let reservedWords = [ - "Any", - "#available", - "associatedtype", - "associativity", - "as", - "break", - "case", - "catch", - "class", - "#colorLiteral", - "#column", - "continue", - "convenience", - "deinit", - "default", - "defer", - "didSet", - "do", - "dynamic", - "enum", - "extension", - "else", - "#else", - "#elseif", - "#endif", - "#error", - "fallthrough", - "false", - "#file", - "#fileLiteral", - "fileprivate", - "final", - "for", - "func", - "#function", - "get", - "guard", - "indirect", - "infix", - "if", - "#if", - "#imageLiteral", - "in", - "is", - "import", - "init", - "inout", - "internal", - "lazy", - "left", - "let", - "#line", - "mutating", - "none", - "nonmutating", - "nil", - "open", - "operator", - "optional", - "override", - "package", - "postfix", - "prefix", - "private", - "protocol", - "Protocol", - "public", - "repeat", - "rethrows", - "return", - "required", - "right", - "#selector", - "self", - "Self", - "set", - "#sourceLocation", - "super", - "static", - "struct", - "subscript", - "switch", - "this", - "throw", - "throws", - "true", - "try", - "Type", - "typealias", - "unowned", - "var", - "#warning", - "weak", - "willSet", - "where", - "while", -] diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift deleted file mode 100644 index 0bff45cbc..000000000 --- a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import struct Foundation.Locale -import struct Foundation.NSRange -import class Foundation.NSRegularExpression -import struct Smithy.ErrorTrait -import struct Smithy.ServiceTrait -import struct Smithy.ShapeID - -public struct SymbolProvider { - let service: ServiceShape - let model: Model - - init(service: ServiceShape, model: Model) { - self.service = service - 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: - let base = shape.id.name - if shape.isTopLevel { - return base.capitalized.escapingReservedWords - } 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 - } else { - return try "\(modelNamespace).\(base.capitalized.escapingReservedWords)" - } - case .list, .set: - guard let listShape = shape as? ListShape else { - throw SymbolProviderError("Shape has type .list but is not a ListShape") - } - let elementType = try swiftType(shape: listShape.member.target) - let opt = try NullableIndex().isNonOptional(listShape.member) ? "" : "?" - return "[\(elementType)\(opt)]" - case .map: - guard let mapShape = shape as? MapShape else { - throw SymbolProviderError("Shape has type .map but is not a MapShape") - } - let valueType = try swiftType(shape: mapShape.value.target) - let opt = try NullableIndex().isNonOptional(mapShape.value) ? "" : "?" - return "[Swift.String: \(valueType)\(opt)]" - case .string: - return "Swift.String" - case .boolean: - return "Swift.Bool" - case .byte: - return "Swift.Int8" - case .short: - return "Swift.Int16" - case .integer, .long: - return "Swift.Int" - case .bigInteger: - return "Swift.Int64" - case .float: - return "Swift.Float" - case .double, .bigDecimal: - return "Swift.Double" - case .blob: - return "Foundation.Data" - case .timestamp: - return "Foundation.Date" - 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" - case .member, .operation, .resource: - throw SymbolProviderError("Cannot provide Swift symbol for shape type \(shape.type)") - } - } - - static let locale = Locale(identifier: "en_US_POSIX") - - public func operationMethodName(operation: OperationShape) throws -> String { - return operation.id.name.toLowerCamelCase().escapingReservedWords - } - - public func propertyName(shapeID: ShapeID) throws -> String { - guard let member = shapeID.member else { throw SymbolProviderError("Shape ID has no member name") } - return member.toLowerCamelCase().escapingReservedWords - } - - public func enumCaseName(shapeID: ShapeID) throws -> String { - try propertyName(shapeID: shapeID).lowercased().escapingReservedWords - } - - private var modelNamespace: String { - get throws { - try swiftType(shape: service).appending("Types") - } - } -} - -private extension Shape { - - var isTopLevel: Bool { - hasTrait(UsedAsInputTrait.self) || hasTrait(UsedAsOutputTrait.self) || hasTrait(ErrorTrait.self) - } -} - -extension String { - - func toLowerCamelCase() -> String { - let words = splitOnWordBoundaries() // Split into words - let firstWord = words.first!.lowercased() // make first word lowercase - return firstWord + words.dropFirst().joined() // join lowercased first word to remainder - } - - func toUpperCamelCase() -> String { - let words = splitOnWordBoundaries() // Split into words - let firstLetter = words.first!.first!.uppercased() // make first letter uppercase - return firstLetter + words.joined().dropFirst() // join uppercased first letter to remainder - } - - func splitOnWordBoundaries() -> [String] { - // TODO: when nonsupporting platforms are dropped, convert this to Swift-native regex - // adapted from Java v2 SDK CodegenNamingUtils.splitOnWordBoundaries - var result = self - - // all non-alphanumeric characters: "acm-success"-> "acm success" - result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") - - // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" - result = underscoreRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " _ ") - - // if a number has a standalone v or V in front of it, separate it out - result = smallVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 v$2") - result = largeVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 V$2") - - // add a space between camelCased words - result = camelCaseSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") - - // add a space after acronyms - result = acronymSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") - - // add space after a number in the middle of a word - result = spaceAfterNumberRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") - - // remove extra spaces - multiple consecutive ones or those and the beginning/end of words - result = removeExtraSpaceRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") - .trimmingCharacters(in: .whitespaces) - - return result.components(separatedBy: " ") - } - - var range: NSRange { - NSRange(location: 0, length: count) - } -} - -// Regexes used in splitOnWordBoundaries() above. -// force_try linter rule is disabled since these are just created from static strings. -// swiftlint:disable force_try -private let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") -private let underscoreRegex = try! NSRegularExpression(pattern: "_") -private let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") -private let largeVRegex = try! NSRegularExpression(pattern: "([^A-Z]{2,})V([0-9]+)") -private let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") -private let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") -private let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") -private let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") -// swiftlint:enable force_try diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift deleted file mode 100644 index 2d17de6bb..000000000 --- a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -public struct SymbolProviderError: Error { - let localizedDescription: String - - init(_ localizedDescription: String) { - self.localizedDescription = localizedDescription - } -} diff --git a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift index 0c26ed377..b4249f978 100644 --- a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift +++ b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift @@ -25,11 +25,7 @@ class SmithyCodegenCoreTests: XCTestCase { let generator = try CodeGenerator( service: "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", 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"), - typeRegistryFileURL: tempDirURL.appendingPathComponent("TypeRegistry.swift"), - operationsFileURL: tempDirURL.appendingPathComponent("Operations.swift") + schemasFileURL: tempDirURL.appendingPathComponent("Schemas.swift") ) try generator.run() } From 1483b46513ffa67436cce8d5c0c9d4fc14cc4836 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 19 Jan 2026 20:16:12 -0600 Subject: [PATCH 19/29] Fix Swift 6 build --- Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift index 233beebd5..ab9e4de18 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift @@ -26,7 +26,7 @@ extension Model { } } -private var affectedServices = [ +private let affectedServices = [ "com.amazonaws.ec2#AmazonEC2", "com.amazonaws.nimble#nimble", "com.amazonaws.amplifybackend#AmplifyBackend", From e796292ba494a8eb2780a44e07bbbb01cd403531 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 20 Jan 2026 08:02:58 -0600 Subject: [PATCH 20/29] Restore SmithyCBOR dependencies --- Package.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Package.swift b/Package.swift index 8fb28f78b..2b0d9f71c 100644 --- a/Package.swift +++ b/Package.swift @@ -286,6 +286,8 @@ let package = Package( .target( name: "SmithyCBOR", dependencies: [ + "SmithyReadWrite", + "SmithyTimestamps", "Smithy", "SmithySerialization", .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift") From 676d306ecac6b060c3417c1937b83bc3c22a8b34 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Fri, 23 Jan 2026 16:15:31 -0600 Subject: [PATCH 21/29] Comments and code cleanup --- Sources/Smithy/Schema/Prelude.swift | 16 +++---- Sources/Smithy/Schema/Schema.swift | 6 +++ Sources/Smithy/ShapeID.swift | 10 ++-- Sources/Smithy/Trait/Trait.swift | 4 ++ Sources/Smithy/Trait/TraitCollection.swift | 38 ++++++++++++--- Sources/Smithy/Trait/TraitError.swift | 1 + .../AWSQueryCompatibleTrait.swift | 1 + .../TraitLibrary/AWSQueryErrorTrait.swift | 1 + .../TraitLibrary/AddedDefaultTrait.swift | 1 + .../TraitLibrary/AllSupportedTraits.swift | 7 +-- .../TraitLibrary/ClientOptionalTrait.swift | 1 + .../Smithy/TraitLibrary/DefaultTrait.swift | 13 +++++ .../Smithy/TraitLibrary/EnumValueTrait.swift | 25 ++++++++++ Sources/Smithy/TraitLibrary/ErrorTrait.swift | 1 + Sources/Smithy/TraitLibrary/InputTrait.swift | 1 + Sources/Smithy/TraitLibrary/OutputTrait.swift | 1 + .../Smithy/TraitLibrary/SensitiveTrait.swift | 1 + .../Smithy/TraitLibrary/ServiceTrait.swift | 1 + Sources/Smithy/TraitLibrary/SparseTrait.swift | 1 + .../TraitLibrary/TargetsUnitTrait.swift | 4 ++ Sources/SmithyCodegenCore/AST/ASTError.swift | 1 + Sources/SmithyCodegenCore/AST/Node+AST.swift | 3 ++ Sources/SmithyCodegenCore/CodeGenerator.swift | 13 ++++- Sources/SmithyCodegenCore/CodegenError.swift | 1 + .../SmithyCodegenCore/GenerationContext.swift | 3 +- Sources/SmithyCodegenCore/HasShapeID.swift | 4 ++ .../SmithyCodegenCore/Model/Model+AST.swift | 20 +++++--- Sources/SmithyCodegenCore/Model/Model.swift | 48 ++++++++++++++++++- .../SmithyCodegenCore/Model/ModelError.swift | 1 + .../SmithyCodegenCore/Model/ShapeID+AST.swift | 5 +- .../Model/ShapeType+AST.swift | 3 +- .../ModelTransformer/Model+Box.swift | 8 +++- .../ModelTransformer/Model+Deprecated.swift | 29 ++++++++--- .../ModelTransformer/Model+InputOutput.swift | 18 +++---- .../Model+PruneToService.swift | 9 ++-- .../ModelTransformer/Model+Union.swift | 9 ++-- .../Schemas/SchemasCodegen.swift | 36 ++++++++++++-- .../SmithyCodegenCore/Shape/EnumShape.swift | 2 +- .../Shape/IntEnumShape.swift | 2 +- .../Shape/IntegerShape.swift | 19 ++++++++ .../SmithyCodegenCore/Shape/ListShape.swift | 13 +++-- .../SmithyCodegenCore/Shape/MapShape.swift | 20 +++++--- .../Shape/ResourceShape.swift | 1 + .../Shape/ServiceShape.swift | 6 +-- Sources/SmithyCodegenCore/Shape/Shape.swift | 7 ++- .../SmithyCodegenCore/Shape/StringShape.swift | 19 ++++++++ .../SwiftRendering/String+Utils.swift | 17 +++++-- .../SwiftRendering/SwiftWriter.swift | 30 ++++++++++-- .../TraitLibrary/DeprecatedTrait.swift | 1 + .../TraitLibrary/EnumTrait.swift | 4 ++ .../TraitLibrary/StreamingTrait.swift | 1 + .../TraitLibrary/UsedAsInputTrait.swift | 20 -------- .../TraitLibrary/UsedAsOutputTrait.swift | 20 -------- 53 files changed, 392 insertions(+), 135 deletions(-) create mode 100644 Sources/Smithy/TraitLibrary/EnumValueTrait.swift create mode 100644 Sources/SmithyCodegenCore/Shape/IntegerShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/StringShape.swift delete mode 100644 Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift delete mode 100644 Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift diff --git a/Sources/Smithy/Schema/Prelude.swift b/Sources/Smithy/Schema/Prelude.swift index f60f4476a..0383c5f1c 100644 --- a/Sources/Smithy/Schema/Prelude.swift +++ b/Sources/Smithy/Schema/Prelude.swift @@ -59,32 +59,30 @@ public enum Prelude { } public static var primitiveBooleanSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveBoolean"), type: .boolean, traits: [defaultTraitID: false]) + Schema(id: .init("smithy.api", "PrimitiveBoolean"), type: .boolean, traits: [DefaultTrait(false)]) } public static var primitiveIntegerSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveInteger"), type: .integer, traits: [defaultTraitID: 0]) + Schema(id: .init("smithy.api", "PrimitiveInteger"), type: .integer, traits: [DefaultTrait(0)]) } public static var primitiveByteSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveByte"), type: .byte, traits: [defaultTraitID: 0]) + Schema(id: .init("smithy.api", "PrimitiveByte"), type: .byte, traits: [DefaultTrait(0)]) } public static var primitiveShortSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveShort"), type: .short, traits: [defaultTraitID: 0]) + Schema(id: .init("smithy.api", "PrimitiveShort"), type: .short, traits: [DefaultTrait(0)]) } public static var primitiveLongSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveLong"), type: .long, traits: [defaultTraitID: 0]) + Schema(id: .init("smithy.api", "PrimitiveLong"), type: .long, traits: [DefaultTrait(0)]) } public static var primitiveFloatSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveFloat"), type: .float, traits: [defaultTraitID: 0]) + Schema(id: .init("smithy.api", "PrimitiveFloat"), type: .float, traits: [DefaultTrait(0.0)]) } public static var primitiveDoubleSchema: Schema { - Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [defaultTraitID: 0]) + Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [DefaultTrait(0.0)]) } } - -private let defaultTraitID = ShapeID("smithy.api", "default") diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift index 1471c4e10..cdcfcb4da 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -66,10 +66,16 @@ public struct Schema: Sendable { self.index = index } + /// Checks if a schema has a trait, by trait type. + /// - Parameter type: The trait type to be checked. + /// - Returns: `true` if the schema has a trait for the passed trait, `false` otherwise. public func hasTrait(_ type: T.Type) -> Bool { return traits.hasTrait(T.self) } + /// Gets a trait from the schema. + /// - Parameter type: The trait to be retrieved. + /// - Returns: The requested trait, or `nil` if the schema doesn't have that trait. public func getTrait(_ type: T.Type) throws -> T? { try traits.getTrait(type) } diff --git a/Sources/Smithy/ShapeID.swift b/Sources/Smithy/ShapeID.swift index febb0f84e..e5a3333bc 100644 --- a/Sources/Smithy/ShapeID.swift +++ b/Sources/Smithy/ShapeID.swift @@ -59,11 +59,11 @@ public struct ShapeID: Hashable, Sendable { self.member = member } - public var absoluteID: String { - return "\(namespace)#\(relativeID)" + public var absolute: String { + return "\(namespace)#\(relative)" } - public var relativeID: String { + public var relative: String { if let member { return "\(name)$\(member)" } else { @@ -76,14 +76,14 @@ extension ShapeID: Comparable { // This logic matches the sorting logic used by the Java-based codegen public static func < (lhs: ShapeID, rhs: ShapeID) -> Bool { - lhs.absoluteID.lowercased() < rhs.absoluteID.lowercased() + lhs.absolute.lowercased() < rhs.absolute.lowercased() } } extension ShapeID: CustomStringConvertible { /// Returns the absolute Shape ID in a single, printable string. - public var description: String { absoluteID } + public var description: String { absolute } } public struct ShapeIDError: Error { diff --git a/Sources/Smithy/Trait/Trait.swift b/Sources/Smithy/Trait/Trait.swift index 42218a3ac..92f75e34b 100644 --- a/Sources/Smithy/Trait/Trait.swift +++ b/Sources/Smithy/Trait/Trait.swift @@ -5,6 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +/// An interface for working with Smithy traits stored in a ``TraitCollection``. +/// +/// All traits have a ``Node`` associated with them, but will typically provide their properties +/// with convenient, type-safe accessors. public protocol Trait { static var id: ShapeID { get } diff --git a/Sources/Smithy/Trait/TraitCollection.swift b/Sources/Smithy/Trait/TraitCollection.swift index 5693af893..8e9de524a 100644 --- a/Sources/Smithy/Trait/TraitCollection.swift +++ b/Sources/Smithy/Trait/TraitCollection.swift @@ -5,7 +5,9 @@ // SPDX-License-Identifier: Apache-2.0 // +/// A container for traits that allows for type-safe access. public struct TraitCollection: Sendable, Hashable { + /// The "raw" traits in this collection, as a dictionary of `Node`s keyed by trait shape ID. public var traitDict: [ShapeID: Node] public init() { @@ -15,38 +17,61 @@ public struct TraitCollection: Sendable, Hashable { public init(traits: [ShapeID: Node]) { self.traitDict = traits } - + + /// Whether the trait collection is empty. public var isEmpty: Bool { traitDict.isEmpty } - + + /// The number of traits in the collection. public var count: Int { traitDict.count } + /// Checks if a trait collection has a trait, by trait type. + /// - Parameter type: The trait type to be checked. + /// - Returns: `true` if the collection has a trait for the passed trait, `false` otherwise. public func hasTrait(_ type: T.Type) -> Bool { traitDict[T.id] != nil } - + + /// Checks if a trait collection has a trait, by ID. + /// - Parameter id: The trait ID to be checked. + /// - Returns: `true` if the collection has a trait for the passed Shape ID, `false` otherwise. public func hasTrait(_ id: ShapeID) -> Bool { traitDict[id] != nil } - + + /// Gets a trait from the collection. + /// - Parameter type: The trait to be retrieved. + /// - Returns: The requested trait, or `nil` if the collection doesn't have that trait. public func getTrait(_ type: T.Type) throws -> T? { guard let node = traitDict[T.id] else { return nil } return try T(node: node) } - + + /// Adds a new trait to the collection, overwriting an existing, matching trait. + /// - Parameter trait: The trait to add to the collection. public mutating func add(_ trait: Trait) { traitDict[trait.id] = trait.node } - + + /// Combines two trait collections into a single collection. + /// - Parameter other: The trait collection to merge. Traits in this collection overwrite the other. + /// - Returns: The merged ``TraitCollection``. public func adding(_ other: TraitCollection) -> TraitCollection { let combined = self.traitDict.merging(other.traitDict) { _, new in new } return TraitCollection(traits: combined) } + + /// Returns a trait collection containing only this collection's traits that belong in a schema. + public var schemaTraits: TraitCollection { + let schemaTraitDict = traitDict.filter { (shapeID, _) in allSupportedTraits.contains(shapeID) } + return Self(traits: schemaTraitDict) + } } +/// Allows for the creation of a ``TraitCollection`` from a `[ShapeID: Node]` dictionary literal. extension TraitCollection: ExpressibleByDictionaryLiteral { public typealias Key = ShapeID public typealias Value = Node @@ -56,6 +81,7 @@ extension TraitCollection: ExpressibleByDictionaryLiteral { } } +/// Allows for the creation of a ``TraitCollection`` from a `[Trait]` array literal. extension TraitCollection: ExpressibleByArrayLiteral { public typealias ArrayLiteralElement = any Trait diff --git a/Sources/Smithy/Trait/TraitError.swift b/Sources/Smithy/Trait/TraitError.swift index 93b6ce72c..b615932f7 100644 --- a/Sources/Smithy/Trait/TraitError.swift +++ b/Sources/Smithy/Trait/TraitError.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// Used to throw errors relating to trait-related anomalies. public struct TraitError: Error { public let localizedDescription: String diff --git a/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift b/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift index ca8b8381e..d5bdce3ec 100644 --- a/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift +++ b/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsquerycompatible-trait public struct AWSQueryCompatibleTrait: Trait { public static var id: ShapeID { .init("aws.protocols", "awsQueryCompatible") } diff --git a/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift b/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift index 5d42b727e..a96ff5874 100644 --- a/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift +++ b/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsqueryerror-trait public struct AWSQueryErrorTrait: Trait { public static var id: ShapeID { .init("aws.protocols", "awsQueryError") } diff --git a/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift b/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift index 7fb7ad979..428a6fbfb 100644 --- a/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift +++ b/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/type-refinement-traits.html#addeddefault-trait public struct AddedDefaultTrait: Trait { public static var id: ShapeID { .init("smithy.api", "addedDefault") } diff --git a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift index 0989566db..d128909c9 100644 --- a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift +++ b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift @@ -15,13 +15,14 @@ public let allSupportedTraits = Set([ AddedDefaultTrait.id, AWSQueryCompatibleTrait.id, AWSQueryErrorTrait.id, - SparseTrait.id, ClientOptionalTrait.id, + DefaultTrait.id, + EnumValueTrait.id, + ErrorTrait.id, InputTrait.id, OutputTrait.id, - ErrorTrait.id, - DefaultTrait.id, SensitiveTrait.id, + SparseTrait.id, // Synthetic traits TargetsUnitTrait.id, diff --git a/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift b/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift index f1f3bfefd..35433a99b 100644 --- a/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift +++ b/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/type-refinement-traits.html#clientoptional-trait public struct ClientOptionalTrait: Trait { public static var id: ShapeID { .init("smithy.api", "clientOptional") } diff --git a/Sources/Smithy/TraitLibrary/DefaultTrait.swift b/Sources/Smithy/TraitLibrary/DefaultTrait.swift index 581776b3b..5d3699c41 100644 --- a/Sources/Smithy/TraitLibrary/DefaultTrait.swift +++ b/Sources/Smithy/TraitLibrary/DefaultTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/type-refinement-traits.html#default-trait public struct DefaultTrait: Trait { public static var id: ShapeID { .init("smithy.api", "default") } @@ -13,4 +14,16 @@ public struct DefaultTrait: Trait { public init(node: Node) throws { self.node = node } + + public init(_ bool: Bool) { + self.node = .boolean(bool) + } + + public init(_ integer: Int) { + self.node = .number(Double(integer)) + } + + public init(_ double: Double) { + self.node = .number(double) + } } diff --git a/Sources/Smithy/TraitLibrary/EnumValueTrait.swift b/Sources/Smithy/TraitLibrary/EnumValueTrait.swift new file mode 100644 index 000000000..df3ee666f --- /dev/null +++ b/Sources/Smithy/TraitLibrary/EnumValueTrait.swift @@ -0,0 +1,25 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#enumvalue-trait +public struct EnumValueTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "enumValue") } + + public var node: Node + + public init(node: Node) throws { + self.node = node + } + + public init(value: String) { + self.node = .string(value) + } + + public init(value: Int) { + self.node = .number(Double(value)) + } +} diff --git a/Sources/Smithy/TraitLibrary/ErrorTrait.swift b/Sources/Smithy/TraitLibrary/ErrorTrait.swift index d55a5cc48..61cb6fb62 100644 --- a/Sources/Smithy/TraitLibrary/ErrorTrait.swift +++ b/Sources/Smithy/TraitLibrary/ErrorTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/type-refinement-traits.html#error-trait public struct ErrorTrait: Trait { public static var id: ShapeID { .init("smithy.api", "error") } diff --git a/Sources/Smithy/TraitLibrary/InputTrait.swift b/Sources/Smithy/TraitLibrary/InputTrait.swift index 6e1e178a2..8763018d4 100644 --- a/Sources/Smithy/TraitLibrary/InputTrait.swift +++ b/Sources/Smithy/TraitLibrary/InputTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/type-refinement-traits.html#input-trait public struct InputTrait: Trait { public static var id: ShapeID { .init("smithy.api", "input") } diff --git a/Sources/Smithy/TraitLibrary/OutputTrait.swift b/Sources/Smithy/TraitLibrary/OutputTrait.swift index f1638db3c..ff6e91b1f 100644 --- a/Sources/Smithy/TraitLibrary/OutputTrait.swift +++ b/Sources/Smithy/TraitLibrary/OutputTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/type-refinement-traits.html#smithy-api-output-trait public struct OutputTrait: Trait { public static var id: ShapeID { .init("smithy.api", "output") } diff --git a/Sources/Smithy/TraitLibrary/SensitiveTrait.swift b/Sources/Smithy/TraitLibrary/SensitiveTrait.swift index 433c901f8..3b311dfce 100644 --- a/Sources/Smithy/TraitLibrary/SensitiveTrait.swift +++ b/Sources/Smithy/TraitLibrary/SensitiveTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/documentation-traits.html#smithy-api-sensitive-trait public struct SensitiveTrait: Trait { public static var id: ShapeID { .init("smithy.api", "sensitive") } diff --git a/Sources/Smithy/TraitLibrary/ServiceTrait.swift b/Sources/Smithy/TraitLibrary/ServiceTrait.swift index 6971f99f7..e6f40c6bd 100644 --- a/Sources/Smithy/TraitLibrary/ServiceTrait.swift +++ b/Sources/Smithy/TraitLibrary/ServiceTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/aws/aws-core.html#aws-api-service-trait public struct ServiceTrait: Trait { public static var id: ShapeID { .init("aws.api", "service") } diff --git a/Sources/Smithy/TraitLibrary/SparseTrait.swift b/Sources/Smithy/TraitLibrary/SparseTrait.swift index 8ebf819d4..ab8b73284 100644 --- a/Sources/Smithy/TraitLibrary/SparseTrait.swift +++ b/Sources/Smithy/TraitLibrary/SparseTrait.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// https://smithy.io/2.0/spec/type-refinement-traits.html#sparse-trait public struct SparseTrait: Trait { public static var id: ShapeID { .init("smithy.api", "sparse") } diff --git a/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift b/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift index 023c94fa6..30c0cc9c3 100644 --- a/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift +++ b/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift @@ -5,6 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +/// A synthetic trait that is used to identify operation inputs & outputs that were either left +/// undefined or that targeted `smithy.api#Unit`. +/// +/// This trait is applied using a model transform, prior to code generation. public struct TargetsUnitTrait: Trait { public static var id: ShapeID { .init("swift.synthetic", "targetsUnit") } diff --git a/Sources/SmithyCodegenCore/AST/ASTError.swift b/Sources/SmithyCodegenCore/AST/ASTError.swift index 3e32bc097..550d7db28 100644 --- a/Sources/SmithyCodegenCore/AST/ASTError.swift +++ b/Sources/SmithyCodegenCore/AST/ASTError.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// Used to raise errors while reading or processing the JSON AST file. public struct ASTError: Error { public let localizedDescription: String diff --git a/Sources/SmithyCodegenCore/AST/Node+AST.swift b/Sources/SmithyCodegenCore/AST/Node+AST.swift index 72f2e3239..77acfa221 100644 --- a/Sources/SmithyCodegenCore/AST/Node+AST.swift +++ b/Sources/SmithyCodegenCore/AST/Node+AST.swift @@ -7,6 +7,9 @@ import enum Smithy.Node +// The Node type itself is defined at ``Smithy.Node``. That type is extended here +// with `Decodable` conformance so it can be read directly into memory from JSON. +// See https://smithy.io/2.0/spec/model.html#trait-node-values in the Smithy docs. extension Node: Decodable { public init(from decoder: any Decoder) throws { diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 7ba570fe8..f94ade8b1 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -10,11 +10,17 @@ import class Foundation.JSONDecoder import struct Foundation.URL import struct Smithy.ShapeID +/// The wrapper for Swift-native code generation. public struct CodeGenerator { let service: String let modelFileURL: URL let schemasFileURL: URL? - + + /// Creates a code generator. + /// - Parameters: + /// - service: The absolute shape ID of the service to be generated. A service with this ID must exist in the model. + /// - 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, modelFileURL: URL, @@ -24,7 +30,10 @@ public struct CodeGenerator { self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL } - + + /// Executes the code generator. + /// + /// The model is loaded and processed, then Swift source files are generated and written to the specified URL(s). public func run() throws { // Load the AST from the model file let modelData = try Data(contentsOf: modelFileURL) diff --git a/Sources/SmithyCodegenCore/CodegenError.swift b/Sources/SmithyCodegenCore/CodegenError.swift index 42000614d..1b11bba70 100644 --- a/Sources/SmithyCodegenCore/CodegenError.swift +++ b/Sources/SmithyCodegenCore/CodegenError.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// An error that is thrown for general code generation problems. public struct CodegenError: Error { public let localizedDescription: String diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift index 838321db6..cdebf699e 100644 --- a/Sources/SmithyCodegenCore/GenerationContext.swift +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -7,10 +7,10 @@ import struct Smithy.ShapeID +/// A type that provides the resources needed to perform Swift code generation. public struct GenerationContext { public let service: ServiceShape public let model: Model -// public let symbolProvider: SymbolProvider /// Creates a ``GenerationContext`` from a model. /// @@ -31,6 +31,5 @@ public struct GenerationContext { // Initialize using the final, processed model self.service = try finalModel.expectServiceShape(id: serviceID) self.model = finalModel -// self.symbolProvider = SymbolProvider(service: service, model: finalModel) } } diff --git a/Sources/SmithyCodegenCore/HasShapeID.swift b/Sources/SmithyCodegenCore/HasShapeID.swift index d83bcf600..db766a4a9 100644 --- a/Sources/SmithyCodegenCore/HasShapeID.swift +++ b/Sources/SmithyCodegenCore/HasShapeID.swift @@ -7,6 +7,9 @@ import struct Smithy.ShapeID +/// A protocol for types that are identified by a Shape ID. +/// +/// Primary purpose is to facilitate sorting, filtering, etc. protocol HasShapeID { var id: ShapeID { get } } @@ -24,6 +27,7 @@ extension Array where Element: HasShapeID { extension ShapeID: HasShapeID { + /// ShapeID itself is made to conform to ``HasShapeID``. var id: ShapeID { self } diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift index c81461a01..77d496019 100644 --- a/Sources/SmithyCodegenCore/Model/Model+AST.swift +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Smithy.EnumValueTrait import enum Smithy.Node import enum Smithy.Prelude import struct Smithy.ShapeID @@ -17,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 load into the `Model` being created. + /// - Parameter astModel: The JSON AST model to create a `Model` from. convenience init(astModel: ASTModel) throws { // Get all of the members from the AST model, create pairs of ShapeID & MemberShape let idToMemberShapePairs = try astModel.shapes @@ -31,7 +32,7 @@ extension Model { // Combine all shapes (member & nonmember) into one large Dict for inclusion in the model let shapes = Dictionary(uniqueKeysWithValues: idToShapePairs + idToMemberShapePairs) - // Initialize the properties of self + // Initialize with the shape dictionary self.init(version: astModel.smithy, metadata: astModel.metadata, shapes: shapes) } @@ -52,13 +53,13 @@ extension Model { } // If this shape is a string with the enum trait, add members for its trait members - if astShape.type == .string, let enumTraitNode = astShape.traits?[EnumTrait.id.absoluteID] { + if astShape.type == .string, let enumTraitNode = astShape.traits?[EnumTrait.id.absolute] { let enumTrait = try EnumTrait(node: enumTraitNode) - let unitID = Smithy.Prelude.unitSchema.id.absoluteID + let unitID = Smithy.Prelude.unitSchema.id.absolute enumTrait.members.forEach { enumMember in let name = enumMember.name ?? enumMember.value let traits: [String: Node] = if enumMember.name != nil { - ["smithy.api#enumValue": .string(enumMember.value)] + [EnumValueTrait.id.absolute: .string(enumMember.value)] } else { [:] } @@ -172,6 +173,9 @@ extension Model { return (shapeID, shape) case .string: if traits.hasTrait(EnumTrait.self) { + // The enum trait is a holdover from Smithy 1.0 and is deprecated in favor of + // the enum shape. + // If this is a String with enum trait, convert it to a EnumShape. let shape = EnumShape( id: shapeID, traits: traits, @@ -179,8 +183,12 @@ extension Model { ) return (shapeID, shape) } else { - fallthrough + let shape = StringShape(id: shapeID, traits: traits) + return (shapeID, shape) } + case .integer: + let shape = IntegerShape(id: shapeID, traits: traits) + return (shapeID, shape) default: let shape = Shape( id: shapeID, diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift index 8fe328908..857f3c0b5 100644 --- a/Sources/SmithyCodegenCore/Model/Model.swift +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -8,22 +8,45 @@ import enum Smithy.Node import struct Smithy.ShapeID +/// An in-memory representation of a Smithy model, suitable for use in code generation. public class Model { + + /// The Smithy version that this model conforms to. This type supports `1.0` and `2.0`. public let version: String + + /// The model metadata. public let metadata: Node? + + /// All shapes in the model, keyed by their absolute ShapeID. public let shapes: [ShapeID: Shape] - public let allShapesSorted: [Shape] + /// An array of all shapes in the model, in Smithy-sorted order. + public let allShapesSorted: [Shape] + + /// Creates a new model. + /// + /// When a ``Model`` is created from another model, the shapes are updated to point to the new model + /// for reference resolution. The old model should no longer be used once a new model is created from it. + /// - Parameters: + /// - version: The Smithy version that the model conforms to. + /// - metadata: The model metadata. + /// - shapes: A dictionary of all shapes in the model, keyed by their absolute Shape IDs. init(version: String, metadata: Node?, shapes: [ShapeID: Shape]) { self.version = version self.metadata = metadata self.shapes = shapes + + // Sort the shapes in the dictionary & store them in an array. self.allShapesSorted = Array(shapes.values).smithySorted() + // self is now fully initialized. // Set the model on each shape to self. shapes.values.forEach { $0.model = self } } - + + /// Returns the shape for the passed ID. Throws if the shape is not present. + /// - Parameter id: The ShapeID for the shape to be retrieved. + /// - Returns: The retrieved shape. Throws if the shape is not found. func expectShape(id: ShapeID) throws -> Shape { guard let shape = shapes[id] else { throw ModelError("ShapeID \(id) was expected in model but not found") @@ -31,6 +54,9 @@ public class Model { return shape } + /// Returns the service for the passed ID. Throws if the shape is not present or not a ``ServiceShape``. + /// - Parameter id: The ShapeID for the service to be retrieved. + /// - Returns: The retrieved shape. Throws if the shape is not found or not a ``ServiceShape``. func expectServiceShape(id: ShapeID) throws -> ServiceShape { guard let shape = try expectShape(id: id) as? ServiceShape else { throw ModelError("ShapeID \(id) is not a ServiceShape") @@ -38,6 +64,9 @@ public class Model { return shape } + /// Returns the resource for the passed ID. Throws if the shape is not present or not a ``ResourceShape``. + /// - Parameter id: The ShapeID for the resource to be retrieved. + /// - Returns: The retrieved resource. Throws if the shape is not found or not a ``ResourceShape``. func expectResourceShape(id: ShapeID) throws -> ResourceShape { guard let shape = try expectShape(id: id) as? ResourceShape else { throw ModelError("ShapeID \(id) is not a ResourceShape") @@ -45,6 +74,9 @@ public class Model { return shape } + /// Returns the operation for the passed ID. Throws if the shape is not present or not a ``OperationShape``. + /// - Parameter id: The ShapeID for the operation to be retrieved. + /// - Returns: The retrieved operation. Throws if the shape is not found or not a ``OperationShape``. func expectOperationShape(id: ShapeID) throws -> OperationShape { guard let shape = try expectShape(id: id) as? OperationShape else { throw ModelError("ShapeID \(id) is not a OperationShape") @@ -52,6 +84,9 @@ public class Model { return shape } + /// Returns the structure for the passed ID. Throws if the shape is not present or not a ``StructureShape``. + /// - Parameter id: The ShapeID for the structure to be retrieved. + /// - Returns: The retrieved structure. Throws if the shape is not found or not a ``StructureShape``. func expectStructureShape(id: ShapeID) throws -> StructureShape { guard let shape = try expectShape(id: id) as? StructureShape else { throw ModelError("ShapeID \(id) is not a StructureShape") @@ -59,6 +94,9 @@ public class Model { return shape } + /// Returns the list for the passed ID. Throws if the shape is not present or not a ``ListShape``. + /// - Parameter id: The ShapeID for the list to be retrieved. + /// - Returns: The retrieved list. Throws if the shape is not found or not a ``ListShape``. func expectListShape(id: ShapeID) throws -> ListShape { guard let shape = try expectShape(id: id) as? ListShape else { throw ModelError("ShapeID \(id) is not a ListShape") @@ -66,6 +104,9 @@ public class Model { return shape } + /// Returns the map for the passed ID. Throws if the shape is not present or not a ``MapShape``. + /// - Parameter id: The ShapeID for the map to be retrieved. + /// - Returns: The retrieved map. Throws if the shape is not found or not a ``MapShape``. func expectMapShape(id: ShapeID) throws -> MapShape { guard let shape = try expectShape(id: id) as? MapShape else { throw ModelError("ShapeID \(id) is not a MapShape") @@ -73,6 +114,9 @@ public class Model { return shape } + /// Returns the member for the passed ID. Throws if the shape is not present or not a ``MemberShape``. + /// - Parameter id: The ShapeID for the member to be retrieved. + /// - Returns: The retrieved member. Throws if the shape is not found or not a ``MemberShape``. func expectMemberShape(id: ShapeID) throws -> MemberShape { guard let shape = try expectShape(id: id) as? MemberShape else { throw ModelError("ShapeID \(id) is not a MemberShape") diff --git a/Sources/SmithyCodegenCore/Model/ModelError.swift b/Sources/SmithyCodegenCore/Model/ModelError.swift index 97d45fdb5..b0b76c3ea 100644 --- a/Sources/SmithyCodegenCore/Model/ModelError.swift +++ b/Sources/SmithyCodegenCore/Model/ModelError.swift @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // +/// Used to throw errors related to a Smithy model. public struct ModelError: Error { public let localizedDescription: String diff --git a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift index e6d67a6a7..a57ea09aa 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -8,10 +8,11 @@ import struct Smithy.ShapeID extension ASTReference { - + + /// Convenience accessor to create a ``ShapeID`` from an ``ASTReference``. var id: ShapeID { get throws { - return try ShapeID(target) + try ShapeID(target) } } } diff --git a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift index 3f8332c5b..d3a2ae615 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -8,7 +8,8 @@ import enum Smithy.ShapeType extension ASTType { - + + /// Convenience method to convert ``ASTType`` to a ``Smithy.ShapeType``. var modelType: ShapeType { get throws { switch self { diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift index ab9e4de18..83121c828 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift @@ -9,9 +9,13 @@ import struct Smithy.ClientOptionalTrait import struct Smithy.ShapeID extension Model { - + + /// Adds the `clientOptional` trait to all structure members in selected services. + /// Reproduces the transform in [BoxServices.kt](https://github.com/awslabs/aws-sdk-swift/blob/main/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/customization/BoxServices.kt). + /// - Parameter serviceID: The ShapeID for the service that is being code generated. + /// - Returns: The transformed model. func optionalizeStructMembers(serviceID: ShapeID) throws -> Model { - guard affectedServices.contains(serviceID.absoluteID) else { return self } + guard affectedServices.contains(serviceID.absolute) else { return self } let newShapes = try self.shapes.mapValues { shape in guard let member = shape as? MemberShape else { return shape } diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift index dd28459ef..bd294d392 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift @@ -5,30 +5,43 @@ // SPDX-License-Identifier: Apache-2.0 // -import Foundation +import class Foundation.DateFormatter +import struct Foundation.Locale extension Model { - + + /// Removes all shapes with the `deprecated` trait and a `since` date after `2024-09-17`, the `aws-sdk-swift` + /// GA date. + /// - Returns: The transformed model. func withDeprecatedShapesRemoved() throws -> Model { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd" + // The "cutoff date". Shapes deprecated before this date will be removed from the model. + let cutoff = formatter.date(from: "2024-09-17")! + + // Filter the deprecated shapes from the model. let nonDeprecatedShapes = try shapes.filter { (_, shape) in + + // Keep this shape if it doesn't have a DeprecatedTrait with a `since` value. guard let since = try shape.getTrait(DeprecatedTrait.self)?.since else { return true } + // Keep this shape if the `since` value doesn't parse to a yyyy-MM-dd date. guard let sinceDate = formatter.date(from: since) else { return true } - let cutoff = formatter.date(from: "2024-09-17")! - + // Compare dates, keep the shape if it was deprecated before the cutoff. return sinceDate > cutoff } var trimmedShapes = nonDeprecatedShapes - var trimmedShapesCount = 0 + 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 { - trimmedShapesCount = trimmedShapes.count + previousTrimmedShapesCount = trimmedShapes.count let newTrimmedShapes = try trimmedShapes.filter { (_, shape) in switch shape { case let listShape as ListShape: @@ -51,8 +64,9 @@ extension Model { } } trimmedShapes = newTrimmedShapes - } while trimmedShapes.count != trimmedShapesCount + } 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: @@ -111,6 +125,7 @@ extension Model { } } + // 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/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift index 190ec5786..14391316e 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -12,7 +12,10 @@ import struct Smithy.TargetsUnitTrait import struct Smithy.TraitCollection extension Model { - + + /// Creates an empty structure in place of operation inputs & outputs that target `smithy.api#Unit` + /// or that have no target. Applies the `TargetsUnitTrait` to these synthesized structures as well. + /// - Returns: The transformed model. func withSynthesizedInputsOutputs() throws -> Model { // Get the operations in the model @@ -42,16 +45,13 @@ extension Model { } // Make new input and output shapes, plus their members, with the new ID - // Add UsedAsInput and UsedAsOutput traits to the input/output structures - // These traits allow us to identify inputs/outputs by trait, but allow us to - // leave the Smithy input & output traits as set on the original model. - let newInput = newStruct(newID: newInputShapeID, newTraits: [UsedAsInputTrait()], original: inputShape) + let newInput = newStruct(newID: newInputShapeID, original: inputShape) let newInputShapeMembers = try renamedMembers(newID: newInputShapeID, original: inputShape) - let newOutput = newStruct(newID: newOutputShapeID, newTraits: [UsedAsOutputTrait()], original: outputShape) + let newOutput = newStruct(newID: newOutputShapeID, original: outputShape) let newOutputShapeMembers = try renamedMembers(newID: newOutputShapeID, original: outputShape) // Add the new input & output and their members to the new shape dictionary. - // The originals will remain and will be pruned later if they are unreferenced. + // The originals will remain and will be pruned later if they are left unreferenced. newShapes[newInput.id] = newInput newInputShapeMembers.forEach { newShapes[$0.id] = $0 } newShapes[newOutput.id] = newOutput @@ -74,10 +74,10 @@ extension Model { return Model(version: version, metadata: metadata, shapes: newShapes) } - private func newStruct(newID: ShapeID, newTraits: TraitCollection, original: StructureShape) -> StructureShape { + private func newStruct(newID: ShapeID, original: StructureShape) -> StructureShape { StructureShape( id: newID, - traits: original.traits.adding(newTraits), + traits: original.traits, memberIDs: original.memberIDs.map { .init(id: newID, member: $0.member) } ) } diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift index c9b8cae2e..c14cd7b5b 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift @@ -9,8 +9,11 @@ import struct Smithy.ShapeID extension Model { - // Filters out all shapes except for the identified service and its descendants - // Returns the pruned model, plus the service shape that it is pruned to + /// Filters out all shapes except for the identified service and its direct & indirect descendants. + /// + /// This eliminates any shapes in the model that are not needed for the service being generated. + /// - Parameter serviceID: The ShapeID for the service that is being generated. + /// - Returns: The transformed model. func prune(serviceID: ShapeID) throws -> Model { // Get the service @@ -22,7 +25,7 @@ extension Model { // Create a dictionary from the set, keyed by ShapeID let shapeDict = Dictionary(uniqueKeysWithValues: shapesForService.map { ($0.id, $0) }) - // Create and return the model & service in a tuple + // Create and return the transformed model return Model(version: self.version, metadata: self.metadata, shapes: shapeDict) } } diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift index e092d0aeb..2dec0773f 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift @@ -10,10 +10,11 @@ import struct Smithy.ShapeID extension Model { - // smithy-swift mistakenly creates a structure for an enum case with an associated value for a union - // member that targets smithy.api#Unit. To ensure that these structures get SerializableStruct / - // DeserializableStruct conformance, we replace smithy.api#Unit with a structure named Unit in the - // union's namespace. + /// smithy-swift mistakenly creates a structure for an enum case with an associated value for a union + /// member that targets `smithy.api#Unit`. To ensure that these structures get SerializableStruct / + /// DeserializableStruct conformance, we replace `smithy.api#Unit` with a structure named `Unit` in the + /// union's namespace. + /// - Returns: The transformed model. func withUnionsTargetingUnitAdded() throws -> Model { var unitSubstitute: Shape? diff --git a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift index b24460f9c..61a9044a9 100644 --- a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift @@ -9,10 +9,14 @@ import let Smithy.allSupportedTraits import struct Smithy.ShapeID import enum Smithy.ShapeType +/// A generator for the `Schemas.swift` package struct SchemasCodegen { package init() {} - + + /// Generates the `Schemas.swift` file for a modeled service. + /// - Parameter ctx: The generation context to be used for codegen. + /// - Returns: The contents of the `Schemas.swift` source file. package func generate(ctx: GenerationContext) throws -> String { let writer = SwiftWriter() writer.write("import struct Smithy.Schema") @@ -32,6 +36,8 @@ package struct SchemasCodegen { let allShapes = [ctx.service] + sortedOperationShapes + sortedModelShapes for shape in allShapes { + + // Render an internal-scoped, computed var in the global namespace for this schema. try writer.openBlock("var \(shape.schemaVarName): Smithy.Schema {", "}") { writer in try writeSchema(writer: writer, shape: shape) writer.unwrite(",") @@ -40,7 +46,7 @@ package struct SchemasCodegen { // If a schema has a member that targets the schema itself, we avoid a compile warning for // self-reference by generating a duplicate schema var that references this schema, and we - // target the duplicate instead. + // will target the duplicate instead. // // This happens ~20 times in AWS models so it is not so frequent that the extra var will bloat // service clients. @@ -56,11 +62,18 @@ package struct SchemasCodegen { } private func writeSchema(writer: SwiftWriter, shape: Shape, index: Int? = nil) throws { + + // Open the initializer try writer.openBlock(".init(", "),") { writer in + + // Write the id: and type: params. All schemas will have this writer.write("id: \(shape.id.rendered),") writer.write("type: .\(shape.type),") - let relevantTraitIDs = shape.traits.traitDict.keys.filter { allSupportedTraits.contains($0) } - let traitIDs = Array(relevantTraitIDs).smithySorted() + + // Get the trait IDs for traits that are allow-listed for the schema & sort + let traitIDs = Array(shape.traits.schemaTraits.traitDict.keys).smithySorted() + + // If there are any traits, write the traits: param if !traitIDs.isEmpty { writer.openBlock("traits: [", "],") { writer in for traitID in traitIDs { @@ -69,14 +82,21 @@ package struct SchemasCodegen { } } } + + // Get the members for this shape let members = try (shape as? HasMembers)?.members ?? [] + + // If there are any members, write the members param if !members.isEmpty { try writer.openBlock("members: [", "],") { writer in for (index, member) in members.enumerated() { + // Make a recursive call to this method to render the member try writeSchema(writer: writer, shape: member, index: index) } } } + + // If this shape is a member, write the target: param if let member = shape as? MemberShape { let target = try member.target @@ -85,16 +105,22 @@ package struct SchemasCodegen { let prefix = target.id == member.containerID ? "dup_of_" : "" writer.write(try "target: \(prefix)\(target.schemaVarName),") } + + // Write the index: param if one was passed. Only members will have an index. if let index { writer.write("index: \(index),") } + + // Get rid of the trailing comma since Swift 5.x will fail to compile on a + // method param trailing comma. writer.unwrite(",") } } } extension ShapeID { - + + /// Change the Shape ID into a rendered ShapeID initializer call. var rendered: String { let namespaceLiteral = namespace.literal let nameLiteral = name.literal diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift index f85d3dd92..6e26906b5 100644 --- a/Sources/SmithyCodegenCore/Shape/EnumShape.swift +++ b/Sources/SmithyCodegenCore/Shape/EnumShape.swift @@ -13,7 +13,7 @@ import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy enums. public class EnumShape: Shape, HasMembers { - let memberIDs: [ShapeID] + public let memberIDs: [ShapeID] public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs diff --git a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift index 5862fda78..db330c18f 100644 --- a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift +++ b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift @@ -12,7 +12,7 @@ import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy intEnums. public class IntEnumShape: Shape, HasMembers { - let memberIDs: [ShapeID] + public let memberIDs: [ShapeID] public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs diff --git a/Sources/SmithyCodegenCore/Shape/IntegerShape.swift b/Sources/SmithyCodegenCore/Shape/IntegerShape.swift new file mode 100644 index 000000000..15c7031dc --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/IntegerShape.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy integers. +class IntegerShape: Shape { + + public init(id: ShapeID, traits: TraitCollection) { + super.init(id: id, type: .integer, traits: traits) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ListShape.swift b/Sources/SmithyCodegenCore/Shape/ListShape.swift index 1ff52d53d..680f7a7a7 100644 --- a/Sources/SmithyCodegenCore/Shape/ListShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ListShape.swift @@ -12,24 +12,27 @@ import struct Smithy.TraitCollection /// A ``Shape`` subclass specialized for Smithy lists. public class ListShape: Shape, HasMembers { - let memberIDs: [ShapeID] + public let memberIDs: [ShapeID] public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { self.memberIDs = memberIDs super.init(id: id, type: .list, traits: traits) } - public var memberID: ShapeID { .init(id: self.id, member: "member") } - public var member: MemberShape { get throws { - try model.expectMemberShape(id: memberID) + // A list will always have one member, and it will always be "member". + let memberID = memberIDs[0] + guard memberID.member == "member" else { + throw ModelError("ListShape does not have expected \"member\" member") + } + return try model.expectMemberShape(id: memberID) } } public var members: [MemberShape] { get throws { - try [member] + try memberIDs.map { try model.expectMemberShape(id: $0) } } } diff --git a/Sources/SmithyCodegenCore/Shape/MapShape.swift b/Sources/SmithyCodegenCore/Shape/MapShape.swift index 7feb35631..304c65d88 100644 --- a/Sources/SmithyCodegenCore/Shape/MapShape.swift +++ b/Sources/SmithyCodegenCore/Shape/MapShape.swift @@ -19,25 +19,31 @@ public class MapShape: Shape, HasMembers { super.init(id: id, type: .map, traits: traits) } - public var keyID: ShapeID { .init(id: self.id, member: "key") } - public var key: MemberShape { get throws { - try model.expectMemberShape(id: keyID) + // A map will always have two members, and the first will always be "key". + let keyID = memberIDs[0] + guard keyID.member == "key" else { + throw ModelError("MapShape does not have expected Key member") + } + return try model.expectMemberShape(id: keyID) } } - public var valueID: ShapeID { .init(id: self.id, member: "value") } - public var value: MemberShape { get throws { - try model.expectMemberShape(id: valueID) + // A map will always have two members, and the second will always be "value". + let valueID = memberIDs[1] + guard valueID.member == "value" else { + throw ModelError("MapShape does not have expected Value member") + } + return try model.expectMemberShape(id: valueID) } } public var members: [MemberShape] { get throws { - try [key, value] + try memberIDs.map { try model.expectMemberShape(id: $0) } } } diff --git a/Sources/SmithyCodegenCore/Shape/ResourceShape.swift b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift index ec15f85b8..8c805f170 100644 --- a/Sources/SmithyCodegenCore/Shape/ResourceShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift @@ -9,6 +9,7 @@ import enum Smithy.Node import struct Smithy.ShapeID import struct Smithy.TraitCollection +/// A ``Shape`` subclass specialized for Smithy resources. public class ResourceShape: Shape { let operationIDs: [ShapeID] let createID: ShapeID? diff --git a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift index 01fc02b96..7a6b4fdc5 100644 --- a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift +++ b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift @@ -31,19 +31,19 @@ public class ServiceShape: Shape { public var operations: [OperationShape] { get throws { - try operationIDs.compactMap { try model.expectOperationShape(id: $0) } + try operationIDs.map { try model.expectOperationShape(id: $0) } } } public var resources: [ResourceShape] { get throws { - try resourceIDs.compactMap { try model.expectResourceShape(id: $0) } + try resourceIDs.map { try model.expectResourceShape(id: $0) } } } public var errors: [StructureShape] { get throws { - try errorIDs.compactMap { try model.expectStructureShape(id: $0) } + try errorIDs.map { try model.expectStructureShape(id: $0) } } } diff --git a/Sources/SmithyCodegenCore/Shape/Shape.swift b/Sources/SmithyCodegenCore/Shape/Shape.swift index 49586d8a3..5f661ccf2 100644 --- a/Sources/SmithyCodegenCore/Shape/Shape.swift +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -74,9 +74,9 @@ public class Shape: HasShapeID { } } - /// Returns shapes that this shape refers to. + /// Returns shapes that this shape directly refers to. /// - /// Used to build a set of shapes for code generation purposes. + /// Used to build a tree of shapes for code generation purposes. /// - Parameters: /// - includeInput: Whether to include shapes that are associated with input /// - includeOutput: Whether to include shapes that are associated with input @@ -86,6 +86,9 @@ public class Shape: HasShapeID { } } +// Shapes should always be unique to a model by their Shape ID, so Hashable, Equatable, +// and Comparable conformances are based on the Shape ID alone. + extension Shape: Hashable { public func hash(into hasher: inout Hasher) { diff --git a/Sources/SmithyCodegenCore/Shape/StringShape.swift b/Sources/SmithyCodegenCore/Shape/StringShape.swift new file mode 100644 index 000000000..1d79dfd47 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/StringShape.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy strings. +class StringShape: Shape { + + public init(id: ShapeID, traits: TraitCollection) { + super.init(id: id, type: .string, traits: traits) + } +} diff --git a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift index ae2948d12..1c1550990 100644 --- a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift +++ b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift @@ -24,22 +24,33 @@ extension String { .replacingOccurrences(of: "\'", with: "\\'") return "\"\(escaped)\"" } - + + /// Surrounds the string in backticks. + /// Used with Swift keywords that are used as identifiers. var inBackticks: String { "`\(self)`" } - + + /// Capitalizes the first letter of a string. var capitalized: String { let firstChar = self.first?.uppercased() ?? "" return "\(firstChar)\(self.dropFirst())" } - + + /// Converts the string to lower camel case. + /// + /// Follows the exact logic used in the Kotlin code generator. + /// - Returns: The string, in lower camel case. func toLowerCamelCase() -> String { let words = splitOnWordBoundaries() // Split into words let firstWord = words.first!.lowercased() // make first word lowercase return firstWord + words.dropFirst().joined() // join lowercased first word to remainder } + /// Converts the string to upper camel case. + /// + /// Follows the exact logic used in the Kotlin code generator. + /// - Returns: The string, in upper camel case. func toUpperCamelCase() -> String { let words = splitOnWordBoundaries() // Split into words let firstLetter = words.first!.first!.uppercased() // make first letter uppercase diff --git a/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift index 79a468e1a..47933df29 100644 --- a/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift +++ b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift @@ -9,12 +9,21 @@ import class Foundation.Bundle import struct Foundation.Data import struct Foundation.URL +/// A type used to write structured source code text. +/// +/// Based heavily on the Kotlin-based code generator's `SwiftWriter` type. class SwiftWriter { private var lines: [String] var indentLevel = 0 + // One indent/dedent will move indentation by this number of spaces. + let indentStep = 4 + + /// Creates a new ``SwiftWriter``. + /// - Parameter includeHeader: Whether to include the standard header at the top of the generated source content. + /// The header contents are in a resource named `DefaultSwiftHeader.txt`. init(includeHeader: Bool = true) { if includeHeader { let defaultHeaderFileURL = Bundle.module.url(forResource: "DefaultSwiftHeader", withExtension: "txt")! @@ -25,15 +34,19 @@ class SwiftWriter { self.lines = [] } } - + + /// Indents the writer by one additional step func indent() { - indentLevel += 4 + indentLevel += indentStep } + /// Dedents the writer by one step func dedent() { - indentLevel -= 4 + indentLevel -= indentStep } + /// Writes a line of text to the source + /// - Parameter line: The text to be written. func write(_ line: String) { if line.isEmpty { // Don't write whitespace to an empty line @@ -59,7 +72,12 @@ class SwiftWriter { lines[lastIndex].removeLast(text.count) } } - + + /// Writes a "block" of text with opening text, closing text, and an indented body between. + /// - Parameters: + /// - openWith: The text to open the block + /// - closeWith: The text to close the block + /// - contents: A closure that accepts a SwiftWriter as a param, and writes the indented body of the block. func openBlock(_ openWith: String, _ closeWith: String, contents: (SwiftWriter) throws -> Void) rethrows { write(openWith) indent() @@ -67,7 +85,9 @@ class SwiftWriter { dedent() write(closeWith) } - + + /// Returns the entire source contents of the writer, from the header (if any) to the last line written, + /// with individual lines joined by newlines, suitable for writing to a Swift source file. var contents: String { return lines.joined(separator: "\n").appending("\n") } diff --git a/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift index 2fa2367e6..1f46a74d4 100644 --- a/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift +++ b/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift @@ -10,6 +10,7 @@ import struct Smithy.ShapeID import protocol Smithy.Trait import struct Smithy.TraitError +/// See https://smithy.io/2.0/spec/documentation-traits.html#deprecated-trait public struct DeprecatedTrait: Trait { public static var id: ShapeID { .init("smithy.api", "deprecated") } diff --git a/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift index 7617259e1..9e845f0cb 100644 --- a/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift +++ b/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift @@ -10,6 +10,10 @@ import struct Smithy.ShapeID import protocol Smithy.Trait import struct Smithy.TraitError +/// See https://smithy.io/2.0/spec/constraint-traits.html#smithy-api-enum-trait +/// +/// This trait is deprecated, and strings tagged with it are converted to `EnumShape` as part of loading the +/// model from AST. Therefore, it is not renered to schemas or used at runtime. public struct EnumTrait: Trait { public struct EnumMember { diff --git a/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift index 1bcb6abf5..ddf971b47 100644 --- a/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift +++ b/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift @@ -9,6 +9,7 @@ import enum Smithy.Node import struct Smithy.ShapeID import protocol Smithy.Trait +/// See https://smithy.io/2.0/spec/streaming.html#smithy-api-streaming-trait public struct StreamingTrait: Trait { public static var id: ShapeID { .init("smithy.api", "streaming") } diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift deleted file mode 100644 index 5e1f2d2dc..000000000 --- a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import enum Smithy.Node -import struct Smithy.ShapeID -import protocol Smithy.Trait - -public struct UsedAsInputTrait: Trait { - public static var id: ShapeID { .init("swift.synthetic", "usedAsInput") } - - public var node: Node { [:] } - - public init(node: Node) throws {} - - public init() {} -} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift deleted file mode 100644 index e9772600a..000000000 --- a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import enum Smithy.Node -import struct Smithy.ShapeID -import protocol Smithy.Trait - -public struct UsedAsOutputTrait: Trait { - public static var id: ShapeID { .init("swift.synthetic", "usedAsOutput") } - - public var node: Node { [:] } - - public init(node: Node) throws {} - - public init() {} -} From a8e69ec1b8c65d08107f7687af6fe9b3303c297c Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 26 Jan 2026 14:12:24 -0600 Subject: [PATCH 22/29] Fix swiftlint --- Sources/Smithy/Trait/TraitCollection.swift | 14 +++++++------- Sources/SmithyCodegenCore/CodeGenerator.swift | 4 ++-- Sources/SmithyCodegenCore/Model/Model+AST.swift | 1 + Sources/SmithyCodegenCore/Model/Model.swift | 8 ++++---- Sources/SmithyCodegenCore/Model/ShapeID+AST.swift | 2 +- .../SmithyCodegenCore/Model/ShapeType+AST.swift | 2 +- .../ModelTransformer/Model+Box.swift | 2 +- .../ModelTransformer/Model+Deprecated.swift | 2 +- .../ModelTransformer/Model+InputOutput.swift | 2 +- .../SmithyCodegenCore/Schemas/SchemasCodegen.swift | 4 ++-- .../SwiftRendering/String+Utils.swift | 6 +++--- .../SwiftRendering/SwiftWriter.swift | 6 +++--- 12 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Sources/Smithy/Trait/TraitCollection.swift b/Sources/Smithy/Trait/TraitCollection.swift index 8e9de524a..392d36bd6 100644 --- a/Sources/Smithy/Trait/TraitCollection.swift +++ b/Sources/Smithy/Trait/TraitCollection.swift @@ -17,12 +17,12 @@ public struct TraitCollection: Sendable, Hashable { public init(traits: [ShapeID: Node]) { self.traitDict = traits } - + /// Whether the trait collection is empty. public var isEmpty: Bool { traitDict.isEmpty } - + /// The number of traits in the collection. public var count: Int { traitDict.count @@ -34,14 +34,14 @@ public struct TraitCollection: Sendable, Hashable { public func hasTrait(_ type: T.Type) -> Bool { traitDict[T.id] != nil } - + /// Checks if a trait collection has a trait, by ID. /// - Parameter id: The trait ID to be checked. /// - Returns: `true` if the collection has a trait for the passed Shape ID, `false` otherwise. public func hasTrait(_ id: ShapeID) -> Bool { traitDict[id] != nil } - + /// Gets a trait from the collection. /// - Parameter type: The trait to be retrieved. /// - Returns: The requested trait, or `nil` if the collection doesn't have that trait. @@ -49,13 +49,13 @@ public struct TraitCollection: Sendable, Hashable { guard let node = traitDict[T.id] else { return nil } return try T(node: node) } - + /// Adds a new trait to the collection, overwriting an existing, matching trait. /// - Parameter trait: The trait to add to the collection. public mutating func add(_ trait: Trait) { traitDict[trait.id] = trait.node } - + /// Combines two trait collections into a single collection. /// - Parameter other: The trait collection to merge. Traits in this collection overwrite the other. /// - Returns: The merged ``TraitCollection``. @@ -63,7 +63,7 @@ public struct TraitCollection: Sendable, Hashable { let combined = self.traitDict.merging(other.traitDict) { _, new in new } return TraitCollection(traits: combined) } - + /// Returns a trait collection containing only this collection's traits that belong in a schema. public var schemaTraits: TraitCollection { let schemaTraitDict = traitDict.filter { (shapeID, _) in allSupportedTraits.contains(shapeID) } diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index f94ade8b1..4ea8195d3 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -15,7 +15,7 @@ public struct CodeGenerator { let service: String let modelFileURL: URL let schemasFileURL: URL? - + /// Creates a code generator. /// - Parameters: /// - service: The absolute shape ID of the service to be generated. A service with this ID must exist in the model. @@ -30,7 +30,7 @@ public struct CodeGenerator { self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL } - + /// Executes the code generator. /// /// The model is loaded and processed, then Swift source files are generated and written to the specified URL(s). diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift index 77d496019..01c87cbcc 100644 --- a/Sources/SmithyCodegenCore/Model/Model+AST.swift +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -85,6 +85,7 @@ extension Model { } } + // swiftlint:disable:next function_body_length private static func shapePair( id: String, astShape: ASTShape, memberShapes: [ShapeID: MemberShape] ) throws -> (ShapeID, Shape) { diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift index 857f3c0b5..5464f6dcd 100644 --- a/Sources/SmithyCodegenCore/Model/Model.swift +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -13,16 +13,16 @@ public class Model { /// The Smithy version that this model conforms to. This type supports `1.0` and `2.0`. public let version: String - + /// The model metadata. public let metadata: Node? - + /// All shapes in the model, keyed by their absolute ShapeID. public let shapes: [ShapeID: Shape] /// An array of all shapes in the model, in Smithy-sorted order. public let allShapesSorted: [Shape] - + /// Creates a new model. /// /// When a ``Model`` is created from another model, the shapes are updated to point to the new model @@ -43,7 +43,7 @@ public class Model { // Set the model on each shape to self. shapes.values.forEach { $0.model = self } } - + /// Returns the shape for the passed ID. Throws if the shape is not present. /// - Parameter id: The ShapeID for the shape to be retrieved. /// - Returns: The retrieved shape. Throws if the shape is not found. diff --git a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift index a57ea09aa..d860cf7a0 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -8,7 +8,7 @@ import struct Smithy.ShapeID extension ASTReference { - + /// Convenience accessor to create a ``ShapeID`` from an ``ASTReference``. var id: ShapeID { get throws { diff --git a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift index d3a2ae615..53e664734 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -8,7 +8,7 @@ import enum Smithy.ShapeType extension ASTType { - + /// Convenience method to convert ``ASTType`` to a ``Smithy.ShapeType``. var modelType: ShapeType { get throws { diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift index 83121c828..a496a594b 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift @@ -9,7 +9,7 @@ import struct Smithy.ClientOptionalTrait import struct Smithy.ShapeID extension Model { - + /// Adds the `clientOptional` trait to all structure members in selected services. /// Reproduces the transform in [BoxServices.kt](https://github.com/awslabs/aws-sdk-swift/blob/main/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/customization/BoxServices.kt). /// - Parameter serviceID: The ShapeID for the service that is being code generated. diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift index bd294d392..4a46c17ea 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift @@ -9,7 +9,7 @@ import class Foundation.DateFormatter import struct Foundation.Locale extension Model { - + /// Removes all shapes with the `deprecated` trait and a `since` date after `2024-09-17`, the `aws-sdk-swift` /// GA date. /// - Returns: The transformed model. diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift index 14391316e..9d625d56c 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -12,7 +12,7 @@ import struct Smithy.TargetsUnitTrait import struct Smithy.TraitCollection extension Model { - + /// Creates an empty structure in place of operation inputs & outputs that target `smithy.api#Unit` /// or that have no target. Applies the `TargetsUnitTrait` to these synthesized structures as well. /// - Returns: The transformed model. diff --git a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift index 61a9044a9..976096026 100644 --- a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift @@ -13,7 +13,7 @@ import enum Smithy.ShapeType package struct SchemasCodegen { package init() {} - + /// Generates the `Schemas.swift` file for a modeled service. /// - Parameter ctx: The generation context to be used for codegen. /// - Returns: The contents of the `Schemas.swift` source file. @@ -119,7 +119,7 @@ package struct SchemasCodegen { } extension ShapeID { - + /// Change the Shape ID into a rendered ShapeID initializer call. var rendered: String { let namespaceLiteral = namespace.literal diff --git a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift index 1c1550990..aa4b22d5f 100644 --- a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift +++ b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift @@ -24,19 +24,19 @@ extension String { .replacingOccurrences(of: "\'", with: "\\'") return "\"\(escaped)\"" } - + /// Surrounds the string in backticks. /// Used with Swift keywords that are used as identifiers. var inBackticks: String { "`\(self)`" } - + /// Capitalizes the first letter of a string. var capitalized: String { let firstChar = self.first?.uppercased() ?? "" return "\(firstChar)\(self.dropFirst())" } - + /// Converts the string to lower camel case. /// /// Follows the exact logic used in the Kotlin code generator. diff --git a/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift index 47933df29..4d0139a3c 100644 --- a/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift +++ b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift @@ -34,7 +34,7 @@ class SwiftWriter { self.lines = [] } } - + /// Indents the writer by one additional step func indent() { indentLevel += indentStep @@ -72,7 +72,7 @@ class SwiftWriter { lines[lastIndex].removeLast(text.count) } } - + /// Writes a "block" of text with opening text, closing text, and an indented body between. /// - Parameters: /// - openWith: The text to open the block @@ -85,7 +85,7 @@ class SwiftWriter { dedent() write(closeWith) } - + /// Returns the entire source contents of the writer, from the header (if any) to the last line written, /// with individual lines joined by newlines, suitable for writing to a Swift source file. var contents: String { From e38779aea4d4879b28e19fa68a10c3dbc4eae271 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 2 Feb 2026 11:52:50 -0600 Subject: [PATCH 23/29] chore: Rename codegen plugin (#1019) --- Package.swift | 4 ++-- .../SmithyCodeGeneratorPlugin.swift | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename Plugins/{SmithyCodeGenerator => SmithyCodeGeneratorPlugin}/SmithyCodeGeneratorPlugin.swift (100%) diff --git a/Package.swift b/Package.swift index feefbf16f..891853b5e 100644 --- a/Package.swift +++ b/Package.swift @@ -57,7 +57,7 @@ let package = Package( .library(name: "SmithySwiftNIO", targets: ["SmithySwiftNIO"]), .library(name: "SmithyTelemetryAPI", targets: ["SmithyTelemetryAPI"]), .library(name: "SmithyHTTPClientAPI", targets: ["SmithyHTTPClientAPI"]), - .plugin(name: "SmithyCodeGenerator", targets: ["SmithyCodeGenerator"]), + .plugin(name: "SmithyCodeGeneratorPlugin", targets: ["SmithyCodeGeneratorPlugin"]), ], dependencies: { var dependencies: [Package.Dependency] = [ @@ -294,7 +294,7 @@ let package = Package( name: "SmithyWaitersAPI" ), .plugin( - name: "SmithyCodeGenerator", + name: "SmithyCodeGeneratorPlugin", capability: .buildTool(), dependencies: [ "SmithyCodegenCLI", diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift similarity index 100% rename from Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift rename to Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift From c97fe75d6d4f612b07b3698261f80d798de5aa16 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 3 Feb 2026 14:29:46 -0600 Subject: [PATCH 24/29] feat: Serialization & deserialization interfaces & codegen --- Sources/SmithyCodegenCore/CodeGenerator.swift | 38 +++- .../Deserialize/DeserializeCodegen.swift | 183 ++++++++++++++++++ .../Shape+DeserializeMethodName.swift | 64 ++++++ .../Serialize/SerializeCodegen.swift | 134 +++++++++++++ .../Serialize/Shape+SerializeMethodName.swift | 62 ++++++ .../Deserialization/DeserializableShape.swift | 10 + .../DeserializableStruct.swift | 10 + .../Deserialization/ReadStructConsumer.swift | 10 + .../Deserialization/ReadValueConsumer.swift | 8 + .../Deserialization/ShapeDeserializer.swift | 82 ++++++++ .../Serialization/SerializableShape.swift | 12 ++ .../Serialization/SerializableStruct.swift | 21 ++ .../Serialization/ShapeSerializer.swift | 157 +++++++++++++++ .../Serialization/WriteStructConsumer.swift | 10 + .../Serialization/WriteValueConsumer.swift | 8 + 15 files changed, 808 insertions(+), 1 deletion(-) create mode 100644 Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift create mode 100644 Sources/SmithyCodegenCore/Deserialize/Shape+DeserializeMethodName.swift create mode 100644 Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift create mode 100644 Sources/SmithyCodegenCore/Serialize/Shape+SerializeMethodName.swift create mode 100644 Sources/SmithySerialization/Deserialization/DeserializableShape.swift create mode 100644 Sources/SmithySerialization/Deserialization/DeserializableStruct.swift create mode 100644 Sources/SmithySerialization/Deserialization/ReadStructConsumer.swift create mode 100644 Sources/SmithySerialization/Deserialization/ReadValueConsumer.swift create mode 100644 Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift create mode 100644 Sources/SmithySerialization/Serialization/SerializableShape.swift create mode 100644 Sources/SmithySerialization/Serialization/SerializableStruct.swift create mode 100644 Sources/SmithySerialization/Serialization/ShapeSerializer.swift create mode 100644 Sources/SmithySerialization/Serialization/WriteStructConsumer.swift create mode 100644 Sources/SmithySerialization/Serialization/WriteValueConsumer.swift diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 4ea8195d3..f37c9f3ee 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -15,6 +15,10 @@ public struct CodeGenerator { let service: String let modelFileURL: URL let schemasFileURL: URL? + let serializeFileURL: URL? + let deserializeFileURL: URL? + let typeRegistryFileURL: URL? + let operationsFileURL: URL? /// Creates a code generator. /// - Parameters: @@ -24,11 +28,19 @@ public struct CodeGenerator { public init( service: String, modelFileURL: URL, - schemasFileURL: URL? + schemasFileURL: URL?, + serializeFileURL: URL?, + deserializeFileURL: URL?, + typeRegistryFileURL: URL?, + operationsFileURL: URL? ) throws { self.service = service self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL + self.serializeFileURL = serializeFileURL + self.deserializeFileURL = deserializeFileURL + self.typeRegistryFileURL = typeRegistryFileURL + self.operationsFileURL = operationsFileURL } /// Executes the code generator. @@ -53,5 +65,29 @@ public struct CodeGenerator { let schemasContents = try SchemasCodegen().generate(ctx: ctx) try Data(schemasContents.utf8).write(to: schemasFileURL) } + + // If a Serialize file URL was provided, generate it + if let serializeFileURL { + let serializeContents = try SerializeCodegen().generate(ctx: ctx) + try Data(serializeContents.utf8).write(to: serializeFileURL) + } + + // If a Deserialize file URL was provided, generate it + if let deserializeFileURL { + 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/Deserialize/DeserializeCodegen.swift b/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift new file mode 100644 index 000000000..0b2ac8770 --- /dev/null +++ b/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift @@ -0,0 +1,183 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ErrorTrait +import struct Smithy.SparseTrait + +package struct DeserializeCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import Foundation") + writer.write("import struct Smithy.Document") + writer.write("import enum Smithy.Prelude") + writer.write("import struct Smithy.Schema") + writer.write("import protocol SmithySerialization.DeserializableStruct") + writer.write("import typealias SmithySerialization.ReadStructConsumer") + writer.write("import protocol SmithySerialization.ShapeDeserializer") + writer.write("") + + // Get structs & unions that are part of an operation output. + // These will all get a DeserializableShape conformance rendered. + let outputStructsAndUnions = try ctx.service + .outputDescendants + .filter { $0.type == .structure || $0.type == .union } + .smithySorted() + + // Render a DeserializableStruct conformance for every struct & union. + for shape in outputStructsAndUnions { + let swiftType = try ctx.symbolProvider.swiftType(shape: shape) + let varName = shape.type == .structure ? "structure" : "union" + try writer.openBlock("extension \(swiftType): SmithySerialization.DeserializableStruct {", "}") { writer in + writer.write("") + let deserializerType = "any SmithySerialization.ShapeDeserializer" + try writer.openBlock( + "public static func deserialize(_ deserializer: \(deserializerType)) throws -> Self {", "}" + ) { writer in + let initializer = shape.type == .structure ? "()" : ".sdkUnknown(\"\")" + writer.write("var \(varName) = Self\(initializer)") + let schemaVarName = try shape.schemaVarName + writer.write("try deserializer.readStruct(\(schemaVarName), &\(varName))") + writer.write("return \(varName)") + } + writer.write("") + let consumerType = "SmithySerialization.ReadStructConsumer" + try writer.openBlock( + "public static var readConsumer: \(consumerType) {", "}") { writer in + try writer.openBlock("{ memberSchema, \(varName), deserializer in", "}") { writer in + try writer.openBlock("switch memberSchema.index {", "}") { writer in + writer.dedent() + let isNonStreaming = !shape.hasTrait(StreamingTrait.self) + let nonErrorMembers = try members(of: shape) + .filter { try isNonStreaming || !$0.target.hasTrait(ErrorTrait.self) } + for (index, member) in nonErrorMembers.enumerated() { + writer.write("case \(index):") + writer.indent() + try writeDeserializeCall( + ctx: ctx, + writer: writer, + shape: shape, + member: member, + schemaVarName: "memberSchema" + ) + writer.dedent() + } + writer.write("default: break") + writer.indent() + } + } + } + } + writer.write("") + } + writer.unwrite("\n") + return writer.contents + } + + private func writeDeserializeCall( + ctx: GenerationContext, + writer: SwiftWriter, + shape: Shape, + member: MemberShape, + schemaVarName: String + ) throws { + let target = try member.target + let propertySwiftType = try ctx.symbolProvider.swiftType(shape: target) + switch try member.target.type { + case .structure, .union: + let readMethodName = try target.deserializeMethodName + if target.type == .union && target.hasTrait(StreamingTrait.self) { + writer.write("let value = try deserializer.\(readMethodName)(\(schemaVarName))") + } else { + let initializer = target.type == .structure ? "()" : ".sdkUnknown(\"\")" + writer.write("var value = \(propertySwiftType)\(initializer)") + writer.write("try deserializer.\(readMethodName)(\(schemaVarName), &value)") + } + case .list, .set: + guard let listShape = target as? ListShape else { + throw SymbolProviderError("Shape has type .list but is not a ListShape") + } + let isSparse = listShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "readSparseList" : "readList" + try writer.openBlock( + "let value: \(propertySwiftType) = try deserializer.\(methodName)(\(schemaVarName)) { deserializer in", + "}" + ) { writer in + try writeDeserializeCall( + ctx: ctx, + writer: writer, + shape: listShape, + member: listShape.member, + schemaVarName: "\(schemaVarName).target!.member" + ) + } + case .map: + guard let mapShape = target as? MapShape else { + throw SymbolProviderError("Shape has type .map but is not a MapShape") + } + let isSparse = mapShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "readSparseMap" : "readMap" + try writer.openBlock( + "let value: \(propertySwiftType) = try deserializer.\(methodName)(\(schemaVarName)) { deserializer in", + "}" + ) { writer in + try writeDeserializeCall( + ctx: ctx, + writer: writer, + shape: mapShape, + member: mapShape.value, + schemaVarName: "\(schemaVarName).target!.value" + ) + } + default: + let methodName = try target.deserializeMethodName + writer.write("let value: \(propertySwiftType) = try deserializer.\(methodName)(\(schemaVarName))") + } + try writeAssignment(ctx: ctx, writer: writer, shape: shape, member: member) + } + + private func writeAssignment( + ctx: GenerationContext, + writer: SwiftWriter, + shape: Shape, + member: MemberShape + ) throws { + let target = try member.target + + // The assignment being written is based on the shape enclosing the member. + switch shape.type { + case .structure: + // For a structure member, write the value to the appropriate structure property, + // making the appropriate adjustment for an error. + let properties = shape.hasTrait(ErrorTrait.self) ? "properties." : "" + let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id) + writer.write("structure.\(properties)\(propertyName) = value") + case .union: + if target.hasTrait(ErrorTrait.self) && shape.hasTrait(StreamingTrait.self) { + // For an event stream error, throw it + writer.write("throw value as! Swift.Error") + } else { + // For a union member or event stream event, write the appropriate union case to the union variable + let enumCaseName = try ctx.symbolProvider.enumCaseName(shapeID: member.id) + writer.write("union = .\(enumCaseName)(value)") + } + case .list, .set, .map: + // For a collection member, return it to the caller since this is being written + // into a consumer block that returns the collection element. + writer.write("return value") + default: + throw CodegenError("Unsupported shape type \(shape.type) for rendering member deserialize") + } + } + + private func members(of shape: Shape) throws -> [MemberShape] { + guard let hasMembers = shape as? HasMembers else { return [] } + return try hasMembers.members + } +} diff --git a/Sources/SmithyCodegenCore/Deserialize/Shape+DeserializeMethodName.swift b/Sources/SmithyCodegenCore/Deserialize/Shape+DeserializeMethodName.swift new file mode 100644 index 000000000..251cff544 --- /dev/null +++ b/Sources/SmithyCodegenCore/Deserialize/Shape+DeserializeMethodName.swift @@ -0,0 +1,64 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Shape { + + var deserializeMethodName: String { + get throws { + switch type { + case .blob: + if hasTrait(StreamingTrait.self) { + return "readDataStream" + } else { + return "readBlob" + } + case .boolean: + return "readBoolean" + case .string: + return "readString" + case .enum: + return "readEnum" + case .timestamp: + return "readTimestamp" + case .byte: + return "readByte" + case .short: + return "readShort" + case .integer: + return "readInteger" + case .intEnum: + return "readIntEnum" + case .long: + return "readLong" + case .float: + return "readFloat" + case .document: + return "readDocument" + case .double: + return "readDouble" + case .bigDecimal: + return "readBigDecimal" + case .bigInteger: + return "readBigInteger" + case .list, .set: + return "readList" + case .map: + return "readMap" + case .structure, .union: + if hasTrait(StreamingTrait.self) { + return "readEventStream" + } else { + return "readStruct" + } + case .member, .service, .resource, .operation: + throw ModelError("Cannot serialize type \(type)") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift b/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift new file mode 100644 index 000000000..374ddf960 --- /dev/null +++ b/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift @@ -0,0 +1,134 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ErrorTrait +import struct Smithy.SparseTrait + +package struct SerializeCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import enum Smithy.Prelude") + writer.write("import struct Smithy.Schema") + writer.write("import protocol SmithySerialization.SerializableStruct") + writer.write("import protocol SmithySerialization.ShapeSerializer") + writer.write("import typealias SmithySerialization.WriteStructConsumer") + writer.write("") + + let inputStructsAndUnions = try ctx.service.inputDescendants + .filter { $0.type == .structure || $0.type == .union } + .smithySorted() + + for shape in inputStructsAndUnions { + let swiftType = try ctx.symbolProvider.swiftType(shape: shape) + let varName = shape.type == .structure ? "structure" : "union" + try writer.openBlock("extension \(swiftType): SmithySerialization.SerializableStruct {", "}") { writer in + writer.write("") + try writer.openBlock( + "public func serialize(_ serializer: any SmithySerialization.ShapeSerializer) throws {", + "}" + ) { writer in + let schemaVarName = try shape.schemaVarName + writer.write("try serializer.writeStruct(\(schemaVarName), self)") + } + writer.write("") + try writer.openBlock( + "public static var writeConsumer: SmithySerialization.WriteStructConsumer {", "}" + ) { writer in + try writer.openBlock("{ memberSchema, \(varName), serializer in", "}") { writer in + writer.write("switch memberSchema.index {") + for (index, member) in try members(of: shape).enumerated() { + writer.write("case \(index):") + writer.indent() + if shape.type == .structure { + let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id) + let properties = shape.hasTrait(ErrorTrait.self) ? "properties." : "" + if try NullableIndex().isNonOptional(member) { + writer.write("let value = \(varName).\(properties)\(propertyName)") + } else { + writer.write( + "guard let value = \(varName).\(properties)\(propertyName) else { break }" + ) + } + try writeSerializeCall( + writer: writer, shape: shape, member: member, schemaVarName: "memberSchema" + ) + } else { // shape is a union + let enumCaseName = try ctx.symbolProvider.enumCaseName(shapeID: member.id) + writer.write("guard case .\(enumCaseName)(let value) = \(varName) else { break }") + try writeSerializeCall( + writer: writer, shape: shape, member: member, schemaVarName: "memberSchema" + ) + } + writer.dedent() + } + writer.write("default: break") + writer.write("}") + } + } + } + writer.write("") + } + writer.unwrite("\n") + return writer.contents + } + + private func writeSerializeCall( + writer: SwiftWriter, + shape: Shape, + member: MemberShape, + schemaVarName: String + ) throws { + let target = try member.target + switch target.type { + case .list, .set: + guard let listShape = target as? ListShape else { + throw ModelError("Shape \(target.id) is type .\(target.type) but not a ListShape") + } + let isSparse = listShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "writeSparseList" : "writeList" + try writer.openBlock( + "try serializer.\(methodName)(\(schemaVarName), value) { value, serializer in", + "}" + ) { writer in + try writeSerializeCall( + writer: writer, + shape: listShape, + member: listShape.member, + schemaVarName: "\(schemaVarName).target!.member" + ) + } + case .map: + guard let mapShape = target as? MapShape else { + throw ModelError("Shape \(target.id) is type .map but not a MapShape") + } + let isSparse = mapShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "writeSparseMap" : "writeMap" + try writer.openBlock( + "try serializer.\(methodName)(\(schemaVarName), value) { value, serializer in", + "}" + ) { writer in + try writeSerializeCall( + writer: writer, + shape: mapShape, + member: mapShape.value, + schemaVarName: "\(schemaVarName).target!.value" + ) + } + default: + let methodName = try target.serializeMethodName + writer.write("try serializer.\(methodName)(\(schemaVarName), value)") + } + } + + private func members(of shape: Shape) throws -> [MemberShape] { + guard let hasMembers = shape as? HasMembers else { return [] } + return try hasMembers.members + } +} diff --git a/Sources/SmithyCodegenCore/Serialize/Shape+SerializeMethodName.swift b/Sources/SmithyCodegenCore/Serialize/Shape+SerializeMethodName.swift new file mode 100644 index 000000000..651a9c325 --- /dev/null +++ b/Sources/SmithyCodegenCore/Serialize/Shape+SerializeMethodName.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension Shape { + + var serializeMethodName: String { + get throws { + switch type { + case .blob: + if hasTrait(StreamingTrait.self) { + return "writeDataStream" + } else { + return "writeBlob" + } + case .boolean: + return "writeBoolean" + case .string: + return "writeString" + case .enum: + return "writeEnum" + case .timestamp: + return "writeTimestamp" + case .byte: + return "writeByte" + case .short: + return "writeShort" + case .integer: + return "writeInteger" + case .intEnum: + return "writeIntEnum" + case .long: + return "writeLong" + case .float: + return "writeFloat" + case .document: + return "writeDocument" + case .double: + return "writeDouble" + case .bigDecimal: + return "writeBigDecimal" + case .bigInteger: + return "writeBigInteger" + case .list, .set: + return "writeList" + case .map: + return "writeMap" + case .structure, .union: + if hasTrait(StreamingTrait.self) { + return "writeEventStream" + } else { + return "writeStruct" + } + case .member, .service, .resource, .operation: + throw ModelError("Cannot serialize type \(type)") + } + } + } +} diff --git a/Sources/SmithySerialization/Deserialization/DeserializableShape.swift b/Sources/SmithySerialization/Deserialization/DeserializableShape.swift new file mode 100644 index 000000000..6ec63dca7 --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/DeserializableShape.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol DeserializableShape: Sendable { + static func deserialize(_ deserializer: ShapeDeserializer) throws -> Self +} diff --git a/Sources/SmithySerialization/Deserialization/DeserializableStruct.swift b/Sources/SmithySerialization/Deserialization/DeserializableStruct.swift new file mode 100644 index 000000000..e6e01ed90 --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/DeserializableStruct.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol DeserializableStruct: DeserializableShape { + static var readConsumer: ReadStructConsumer { get } +} diff --git a/Sources/SmithySerialization/Deserialization/ReadStructConsumer.swift b/Sources/SmithySerialization/Deserialization/ReadStructConsumer.swift new file mode 100644 index 000000000..70acbc1fe --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/ReadStructConsumer.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public typealias ReadStructConsumer = (Schema, inout T, any ShapeDeserializer) throws -> Void diff --git a/Sources/SmithySerialization/Deserialization/ReadValueConsumer.swift b/Sources/SmithySerialization/Deserialization/ReadValueConsumer.swift new file mode 100644 index 000000000..148d9e783 --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/ReadValueConsumer.swift @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias ReadValueConsumer = (any ShapeDeserializer) throws -> V diff --git a/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift b/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift new file mode 100644 index 000000000..b6ed3fcbe --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift @@ -0,0 +1,82 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import enum Smithy.ByteStream +import struct Smithy.Document +import struct Smithy.Schema +import protocol Smithy.SmithyDocument + +public protocol ShapeDeserializer { + func readStruct(_ schema: Schema, _ value: inout T) throws + func readList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E] + func readMap(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [String: V] + func readBoolean(_ schema: Schema) throws -> Bool + func readBlob(_ schema: Schema) throws -> Data + func readByte(_ schema: Schema) throws -> Int8 + func readShort(_ schema: Schema) throws -> Int16 + func readInteger(_ schema: Schema) throws -> Int + func readLong(_ schema: Schema) throws -> Int + func readFloat(_ schema: Schema) throws -> Float + func readDouble(_ schema: Schema) throws -> Double + func readBigInteger(_ schema: Schema) throws -> Int64 + func readBigDecimal(_ schema: Schema) throws -> Double + func readString(_ schema: Schema) throws -> String + func readDocument(_ schema: Schema) throws -> any SmithyDocument + func readTimestamp(_ schema: Schema) throws -> Date + func readNull(_ schema: Schema) throws -> T? + func readDataStream(_ schema: Schema) throws -> ByteStream + func readEventStream(_ schema: Schema) throws -> AsyncThrowingStream + func isNull() throws -> Bool + var containerSize: Int { get } +} + +public extension ShapeDeserializer { + + func readEnum(_ schema: Schema) throws -> T where T.RawValue == String { + // Force-unwrap is safe here because generated enums & intEnums never return nil from init(rawValue:) + try T(rawValue: readString(schema))! + } + + func readIntEnum(_ schema: Schema) throws -> T where T.RawValue == Int { + // Force-unwrap is safe here because generated enums & intEnums never return nil from init(rawValue:) + try T(rawValue: readInteger(schema))! + } + + func readSparseList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E?] { + try readList(schema) { deserializer in + if try deserializer.isNull() { + return try deserializer.readNull(schema.resolveTarget.member) + } else { + return try consumer(deserializer) + } + } + } + + func readSparseMap(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [String: V?] { + try readMap(schema) { deserializer in + if try deserializer.isNull() { + return try deserializer.readNull(schema.resolveTarget.value) + } else { + return try consumer(deserializer) + } + } + } + + func readDataStream(_ schema: Schema) throws -> ByteStream { + // by default, do nothing + return ByteStream.data(nil) + } + + func readEventStream(_ schema: Schema) throws -> AsyncThrowingStream { + // by default, do nothing + return AsyncThrowingStream { continuation in + continuation.finish() + } + } +} diff --git a/Sources/SmithySerialization/Serialization/SerializableShape.swift b/Sources/SmithySerialization/Serialization/SerializableShape.swift new file mode 100644 index 000000000..34f7d051b --- /dev/null +++ b/Sources/SmithySerialization/Serialization/SerializableShape.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public protocol SerializableShape { + func serialize(_ serializer: any ShapeSerializer) throws +} diff --git a/Sources/SmithySerialization/Serialization/SerializableStruct.swift b/Sources/SmithySerialization/Serialization/SerializableStruct.swift new file mode 100644 index 000000000..0e74c936b --- /dev/null +++ b/Sources/SmithySerialization/Serialization/SerializableStruct.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol SerializableStruct: SerializableShape, CustomDebugStringConvertible { + static var writeConsumer: WriteStructConsumer { get } +} + +public extension SerializableStruct { + + var debugDescription: String { + let serializer = StringSerializer() + // Safe to try! here because StringSerializer never throws + // swiftlint:disable:next force_try + try! serialize(serializer) + return serializer.string + } +} diff --git a/Sources/SmithySerialization/Serialization/ShapeSerializer.swift b/Sources/SmithySerialization/Serialization/ShapeSerializer.swift new file mode 100644 index 000000000..b8e91ed12 --- /dev/null +++ b/Sources/SmithySerialization/Serialization/ShapeSerializer.swift @@ -0,0 +1,157 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import enum Smithy.ByteStream +import struct Smithy.Schema +import protocol Smithy.SmithyDocument + +public protocol ShapeSerializer { + func writeStruct(_ schema: Schema, _ value: S) throws + func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws + func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws + func writeBoolean(_ schema: Schema, _ value: Bool) throws + func writeByte(_ schema: Schema, _ value: Int8) throws + func writeShort(_ schema: Schema, _ value: Int16) throws + func writeInteger(_ schema: Schema, _ value: Int) throws + func writeLong(_ schema: Schema, _ value: Int) throws + func writeFloat(_ schema: Schema, _ value: Float) throws + func writeDouble(_ schema: Schema, _ value: Double) throws + func writeBigInteger(_ schema: Schema, _ value: Int64) throws + func writeBigDecimal(_ schema: Schema, _ value: Double) throws + func writeString(_ schema: Schema, _ value: String) throws + func writeBlob(_ schema: Schema, _ value: Data) throws + func writeTimestamp(_ schema: Schema, _ value: Date) throws + func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws + func writeNull(_ schema: Schema) throws + func writeDataStream(_ schema: Schema, _ value: ByteStream) throws + func writeEventStream(_ schema: Schema, _ value: AsyncThrowingStream) throws + + var data: Data { get } +} + +public extension ShapeSerializer { + + /// Writes a Smithy enum. + /// - Parameters: + /// - schema: The schema for the Smithy Enum. + /// - value: The enum value to be written. + func writeEnum(_ schema: Schema, _ value: T) throws where T.RawValue == String { + try writeString(schema, value.rawValue) + } + + /// Writes an Smithy IntEnum. + /// - Parameters: + /// - schema: The schema for the Smithy IntEnum. + /// - value: The enum value to be written. + func writeIntEnum(_ schema: Schema, _ value: T) throws where T.RawValue == Int { + try writeInteger(schema, value.rawValue) + } + + /// Write a sparse list. + /// + /// Generated code will call this method when the list has the sparse trait. + /// - Parameters: + /// - schema: The member schema targeting the list. + /// - value: The sparse list to be written. + /// - consumer: The `WriteValueConsumer` for the non-optional element type. + func writeSparseList(_ schema: Schema, _ value: [E?], _ consumer: WriteValueConsumer) throws { + try writeList(schema, value) { element, serializer in + if let element { + try consumer(element, serializer) + } else { + try serializer.writeNull(schema.resolveTarget.member) + } + } + } + + /// Write a sparse map. + /// + /// Generated code will call this method when the map has the sparse trait. + /// - Parameters: + /// - schema: The member schema targeting the map. + /// - value: The sparse map to be written. + /// - consumer: The `WriteValueConsumer` for the non-optional element type. + func writeSparseMap(_ schema: Schema, _ value: [String: V?], _ consumer: WriteValueConsumer) throws { + try writeMap(schema, value) { element, serializer in + if let element { + try consumer(element, serializer) + } else { + try serializer.writeNull(schema.resolveTarget.value) + } + } + } + + /// Write a Smithy document. + /// + /// Based on the type of the document, the contents are written using the appropriate `write...` method for its contents. + /// - Parameters: + /// - schema: The schema for the document + /// - value: The document to be written + func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { + switch value.type { + case .blob: + try writeBlob(schema, value.asBlob()) + case .boolean: + try writeBoolean(schema, value.asBoolean()) + case .string: + try writeString(schema, value.asString()) + case .timestamp: + try writeTimestamp(schema, value.asTimestamp()) + case .byte: + try writeByte(schema, value.asByte()) + case .short: + try writeShort(schema, value.asShort()) + case .integer: + try writeInteger(schema, value.asInteger()) + case .long: + try writeLong(schema, Int(value.asLong())) + case .float: + try writeFloat(schema, value.asFloat()) + case .double: + try writeDouble(schema, value.asDouble()) + case .bigDecimal: + try writeBigDecimal(schema, value.asBigDecimal()) + case .bigInteger: + try writeBigInteger(schema, value.asBigInteger()) + case .list, .set: + try writeList(schema, value.asList()) { element, serializer in + try serializer.writeDocument(schema.member, element) + } + case .map: + try writeMap(schema, value.asStringMap()) { value, serializer in + try serializer.writeDocument(schema.value, value) + } + case .document, .enum, .intEnum, .structure, .union, .member, .service, .resource, .operation: + throw SerializerError("Unsupported or invalid document type: \(value.type)") + } + } + + func writeDataStream(_ schema: Schema, _ value: ByteStream) throws { + // by default, do nothing + } + + func writeEventStream(_ schema: Schema, _ value: AsyncThrowingStream) throws { + // by default, do nothing + } +} + +extension Schema { + + var resolveMember: Schema? { + type == .member ? self : nil + } + + var resolveTarget: Schema { + if let target { + return target + } else { + return self + } + } +} diff --git a/Sources/SmithySerialization/Serialization/WriteStructConsumer.swift b/Sources/SmithySerialization/Serialization/WriteStructConsumer.swift new file mode 100644 index 000000000..07f51b890 --- /dev/null +++ b/Sources/SmithySerialization/Serialization/WriteStructConsumer.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public typealias WriteStructConsumer = (Schema, T, any ShapeSerializer) throws -> Void diff --git a/Sources/SmithySerialization/Serialization/WriteValueConsumer.swift b/Sources/SmithySerialization/Serialization/WriteValueConsumer.swift new file mode 100644 index 000000000..a8049f70e --- /dev/null +++ b/Sources/SmithySerialization/Serialization/WriteValueConsumer.swift @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias WriteValueConsumer = (Element, any ShapeSerializer) throws -> Void From f46e7cb04a0b2ef4b9a8d6967e8411885c0c677e Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 3 Feb 2026 16:56:44 -0600 Subject: [PATCH 25/29] Bring in more codegen files --- .../SmithyCodeGeneratorPlugin.swift | 12 +- .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 16 ++- Sources/SmithyCodegenCore/CodeGenerator.swift | 20 +-- .../SmithyCodegenCore/GenerationContext.swift | 2 + .../NullableIndex/NullableIndex.swift | 76 +++++++++++ .../SymbolProvider/String+ReservedWords.swift | 125 ++++++++++++++++++ .../SymbolProvider/SymbolProvider.swift | 119 +++++++++++++++++ .../SymbolProvider/SymbolProviderError.swift | 14 ++ .../TraitLibrary/UsedAsInputTrait.swift | 20 +++ .../TraitLibrary/UsedAsOutputTrait.swift | 20 +++ .../Serialization/SerializableStruct.swift | 11 -- .../SmithySerialization/SerializerError.swift | 14 ++ 12 files changed, 417 insertions(+), 32 deletions(-) create mode 100644 Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift create mode 100644 Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift create mode 100644 Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift create mode 100644 Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift create mode 100644 Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift create mode 100644 Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift create mode 100644 Sources/SmithySerialization/SerializerError.swift diff --git a/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift index ae41dae07..62ad22234 100644 --- a/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift @@ -55,6 +55,12 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Construct the Schemas.swift path. let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") + // Construct the Serialize.swift path. + let serializeSwiftPath = outputDirectoryPath.appending("\(name)Serialize.swift") + + // Construct the Deserialize.swift path. + let deserializeSwiftPath = outputDirectoryPath.appending("\(name)Deserialize.swift") + // Construct the build command that invokes SmithyCodegenCLI. return .buildCommand( displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", @@ -62,11 +68,15 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { arguments: [ service, modelPath, - "--schemas-path", schemasSwiftPath + "--schemas-path", schemasSwiftPath, + "--serialize-path", serializeSwiftPath, + "--deserialize-path", deserializeSwiftPath ], inputFiles: [inputPath, modelPath], outputFiles: [ schemasSwiftPath, + serializeSwiftPath, + deserializeSwiftPath, ] ) } diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index 73c688caf..70f12cdd6 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -21,6 +21,12 @@ struct SmithyCodegenCLI: AsyncParsableCommand { @Option(help: "The full or relative path to write the Schemas output file.") var schemasPath: String? + @Option(help: "The full or relative path to write the Serialize output file.") + var serializePath: String? + + @Option(help: "The full or relative path to write the Deserialize output file.") + var deserializePath: String? + func run() async throws { let start = Date() @@ -36,11 +42,19 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(path: schemasPath) + // If --serialize-path was supplied, create the Serialize file URL + let serializeFileURL = resolve(path: serializePath) + + // If --deserialize-path was supplied, create the Deserialize file URL + let deserializeFileURL = resolve(path: deserializePath) + // Use resolved file URLs to run code generator try CodeGenerator( service: service, modelFileURL: modelFileURL, - schemasFileURL: schemasFileURL + schemasFileURL: schemasFileURL, + serializeFileURL: serializeFileURL, + deserializeFileURL: deserializeFileURL ).run() let duration = Date().timeIntervalSince(start) diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index f37c9f3ee..5be1e87dd 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -17,8 +17,6 @@ public struct CodeGenerator { let schemasFileURL: URL? let serializeFileURL: URL? let deserializeFileURL: URL? - let typeRegistryFileURL: URL? - let operationsFileURL: URL? /// Creates a code generator. /// - Parameters: @@ -30,17 +28,13 @@ public struct CodeGenerator { modelFileURL: URL, schemasFileURL: URL?, serializeFileURL: URL?, - deserializeFileURL: URL?, - typeRegistryFileURL: URL?, - operationsFileURL: URL? + deserializeFileURL: URL? ) throws { self.service = service self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL self.serializeFileURL = serializeFileURL self.deserializeFileURL = deserializeFileURL - self.typeRegistryFileURL = typeRegistryFileURL - self.operationsFileURL = operationsFileURL } /// Executes the code generator. @@ -77,17 +71,5 @@ 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 cdebf699e..f873ef7b1 100644 --- a/Sources/SmithyCodegenCore/GenerationContext.swift +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -11,6 +11,7 @@ import struct Smithy.ShapeID public struct GenerationContext { public let service: ServiceShape public let model: Model + public let symbolProvider: SymbolProvider /// Creates a ``GenerationContext`` from a model. /// @@ -31,5 +32,6 @@ public struct GenerationContext { // Initialize using the final, processed model self.service = try finalModel.expectServiceShape(id: serviceID) self.model = finalModel + self.symbolProvider = SymbolProvider(service: service, model: finalModel) } } diff --git a/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift b/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift new file mode 100644 index 000000000..0812ddda3 --- /dev/null +++ b/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift @@ -0,0 +1,76 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.AddedDefaultTrait +import struct Smithy.ClientOptionalTrait +import struct Smithy.DefaultTrait +import struct Smithy.InputTrait +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.SparseTrait + +struct NullableIndex { + + /// Determines whether a structure member should be rendered as non-optional. + /// - Parameter memberShape: The member for which optionality is being determined + /// - Returns: `true` if the member should be non-optional, `false` otherwise + func isNonOptional(_ memberShape: MemberShape) throws -> Bool { + let container = try memberShape.container + let target = try memberShape.target + + // Note that these are the current rules in use by smithy-swift. They are not Smithy 2.0 "correct". + + // If the container is a list/set, member is nonoptional unless sparse trait is applied + if [ShapeType.list, .set].contains(container.type), memberShape.id.member == "member" { + return !container.hasTrait(SparseTrait.self) + } + + // If the container is a map, value is nonoptional unless sparse trait is applied + if container.type == .map { + if memberShape.id.member == "value" { + return !container.hasTrait(SparseTrait.self) + } else { + // key is always non-optional + return true + } + } + + // If the containing shape has the input trait, it's definitely optional + if container.hasTrait(InputTrait.self) { + return false + } + + // If the member has the clientOptional trait, it's definitely optional + if memberShape.hasTrait(ClientOptionalTrait.self) { + return false + } + + // If the member has the addedDefault trait, it's definitely optional + if memberShape.hasTrait(AddedDefaultTrait.self) { + return false + } + + // Only number & Boolean types are allowed to be non-optional + let allowedTypes = + [ShapeType.boolean, .bigDecimal, .bigInteger, .byte, .double, .float, .intEnum, .integer, .long, .short] + guard allowedTypes.contains(target.type) else { return false } + + // Check if there is a default trait with a zero/false value. If so, member is non-optional. + let memberDefaultTrait = try memberShape.getTrait(DefaultTrait.self) + let targetDefaultTrait = try target.getTrait(DefaultTrait.self) + guard let defaultNode = (memberDefaultTrait ?? targetDefaultTrait)?.node else { + return false + } + if target.type == .boolean, let bool = defaultNode.boolean, !bool { + return true + } else if let number = defaultNode.number, number == 0.0 { + return true + } else { + return false + } + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift b/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift new file mode 100644 index 000000000..b4a25a5a9 --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift @@ -0,0 +1,125 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension String { + + /// Modifies Swift reserved words that are used in Smithy models so that they can be safely used as identifiers in rendered Swift. + /// + /// Taken from `ReservedWords.kt`: + /// https://github.com/smithy-lang/smithy-swift/blob/main/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/lang/ReservedWords.kt + var escapingReservedWords: String { + if self == "Protocol" || self == "Type" { + // Swift metatypes + "Model\(self)" + } else if reservedWords.contains(self) { + // Surround reserved words in backticks to force compiler + // to treat them as identifiers + self.inBackticks + } else { + self + } + } +} + +private let reservedWords = [ + "Any", + "#available", + "associatedtype", + "associativity", + "as", + "break", + "case", + "catch", + "class", + "#colorLiteral", + "#column", + "continue", + "convenience", + "deinit", + "default", + "defer", + "didSet", + "do", + "dynamic", + "enum", + "extension", + "else", + "#else", + "#elseif", + "#endif", + "#error", + "fallthrough", + "false", + "#file", + "#fileLiteral", + "fileprivate", + "final", + "for", + "func", + "#function", + "get", + "guard", + "indirect", + "infix", + "if", + "#if", + "#imageLiteral", + "in", + "is", + "import", + "init", + "inout", + "internal", + "lazy", + "left", + "let", + "#line", + "mutating", + "none", + "nonmutating", + "nil", + "open", + "operator", + "optional", + "override", + "package", + "postfix", + "prefix", + "private", + "protocol", + "Protocol", + "public", + "repeat", + "rethrows", + "return", + "required", + "right", + "#selector", + "self", + "Self", + "set", + "#sourceLocation", + "super", + "static", + "struct", + "subscript", + "switch", + "this", + "throw", + "throws", + "true", + "try", + "Type", + "typealias", + "unowned", + "var", + "#warning", + "weak", + "willSet", + "where", + "while", +] diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift new file mode 100644 index 000000000..b321e2f84 --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -0,0 +1,119 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Locale +import struct Foundation.NSRange +import class Foundation.NSRegularExpression +import struct Smithy.ErrorTrait +import struct Smithy.ServiceTrait +import struct Smithy.ShapeID + +public struct SymbolProvider { + let service: ServiceShape + let model: Model + + init(service: ServiceShape, model: Model) { + self.service = service + 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: + let base = shape.id.name + if shape.isTopLevel { + return base.capitalized.escapingReservedWords + } 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 + } else { + return try "\(modelNamespace).\(base.capitalized.escapingReservedWords)" + } + case .list, .set: + guard let listShape = shape as? ListShape else { + throw SymbolProviderError("Shape has type .list but is not a ListShape") + } + let elementType = try swiftType(shape: listShape.member.target) + let opt = try NullableIndex().isNonOptional(listShape.member) ? "" : "?" + return "[\(elementType)\(opt)]" + case .map: + guard let mapShape = shape as? MapShape else { + throw SymbolProviderError("Shape has type .map but is not a MapShape") + } + let valueType = try swiftType(shape: mapShape.value.target) + let opt = try NullableIndex().isNonOptional(mapShape.value) ? "" : "?" + return "[Swift.String: \(valueType)\(opt)]" + case .string: + return "Swift.String" + case .boolean: + return "Swift.Bool" + case .byte: + return "Swift.Int8" + case .short: + return "Swift.Int16" + case .integer, .long: + return "Swift.Int" + case .bigInteger: + return "Swift.Int64" + case .float: + return "Swift.Float" + case .double, .bigDecimal: + return "Swift.Double" + case .blob: + return "Foundation.Data" + case .timestamp: + return "Foundation.Date" + 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" + case .member, .operation, .resource: + throw SymbolProviderError("Cannot provide Swift symbol for shape type \(shape.type)") + } + } + + static let locale = Locale(identifier: "en_US_POSIX") + + public func operationMethodName(operation: OperationShape) throws -> String { + return operation.id.name.toLowerCamelCase().escapingReservedWords + } + + public func propertyName(shapeID: ShapeID) throws -> String { + guard let member = shapeID.member else { throw SymbolProviderError("Shape ID has no member name") } + return member.toLowerCamelCase().escapingReservedWords + } + + public func enumCaseName(shapeID: ShapeID) throws -> String { + try propertyName(shapeID: shapeID).lowercased().escapingReservedWords + } + + private var modelNamespace: String { + get throws { + try swiftType(shape: service).appending("Types") + } + } +} + +private extension Shape { + + var isTopLevel: Bool { + hasTrait(UsedAsInputTrait.self) || hasTrait(UsedAsOutputTrait.self) || hasTrait(ErrorTrait.self) + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift new file mode 100644 index 000000000..2d17de6bb --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SymbolProviderError: Error { + let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift new file mode 100644 index 000000000..5e1f2d2dc --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +public struct UsedAsInputTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "usedAsInput") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift new file mode 100644 index 000000000..e9772600a --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +public struct UsedAsOutputTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "usedAsOutput") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/SmithySerialization/Serialization/SerializableStruct.swift b/Sources/SmithySerialization/Serialization/SerializableStruct.swift index 0e74c936b..d946ddb15 100644 --- a/Sources/SmithySerialization/Serialization/SerializableStruct.swift +++ b/Sources/SmithySerialization/Serialization/SerializableStruct.swift @@ -8,14 +8,3 @@ public protocol SerializableStruct: SerializableShape, CustomDebugStringConvertible { static var writeConsumer: WriteStructConsumer { get } } - -public extension SerializableStruct { - - var debugDescription: String { - let serializer = StringSerializer() - // Safe to try! here because StringSerializer never throws - // swiftlint:disable:next force_try - try! serialize(serializer) - return serializer.string - } -} diff --git a/Sources/SmithySerialization/SerializerError.swift b/Sources/SmithySerialization/SerializerError.swift new file mode 100644 index 000000000..166595af7 --- /dev/null +++ b/Sources/SmithySerialization/SerializerError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SerializerError: Error { + public let localizedDescription: String + + public init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} From 9f04f59c6dbbecdb53f77b3df272c4420975d54e Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 3 Feb 2026 17:23:01 -0600 Subject: [PATCH 26/29] fix SmithyCodegenCore tests --- Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift index b4249f978..131e07c78 100644 --- a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift +++ b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift @@ -25,7 +25,9 @@ class SmithyCodegenCoreTests: XCTestCase { let generator = try CodeGenerator( service: "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", modelFileURL: Bundle.module.url(forResource: "smithy-rpcv2-cbor", withExtension: "json")!, - schemasFileURL: tempDirURL.appendingPathComponent("Schemas.swift") + schemasFileURL: tempDirURL.appendingPathComponent("Schemas.swift"), + serializeFileURL: tempDirURL.appendingPathComponent("Serialize.swift"), + deserializeFileURL: tempDirURL.appendingPathComponent("Deserialize.swift") ) try generator.run() } From 7def3237ac3d52ecfef7dd2c4ef4d071bd944b51 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 3 Feb 2026 18:40:24 -0600 Subject: [PATCH 27/29] Fix generated code --- .../Deserialize/DeserializeCodegen.swift | 4 +++- .../ModelTransformer/Model+InputOutput.swift | 11 +++++++---- .../SymbolProvider/SymbolProvider.swift | 6 +++++- .../Deserialization/ShapeDeserializer.swift | 2 +- .../Serialization/SerializableStruct.swift | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift b/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift index 0b2ac8770..625976c83 100644 --- a/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift +++ b/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift @@ -15,6 +15,7 @@ package struct DeserializeCodegen { package func generate(ctx: GenerationContext) throws -> String { let writer = SwiftWriter() writer.write("import Foundation") + writer.write("import enum Smithy.ByteStream") writer.write("import struct Smithy.Document") writer.write("import enum Smithy.Prelude") writer.write("import struct Smithy.Schema") @@ -93,7 +94,8 @@ package struct DeserializeCodegen { case .structure, .union: let readMethodName = try target.deserializeMethodName if target.type == .union && target.hasTrait(StreamingTrait.self) { - writer.write("let value = try deserializer.\(readMethodName)(\(schemaVarName))") + let streamType = "AsyncThrowingStream<\(propertySwiftType), any Swift.Error>" + writer.write("let value: \(streamType) = try deserializer.\(readMethodName)(\(schemaVarName))") } else { let initializer = target.type == .structure ? "()" : ".sdkUnknown(\"\")" writer.write("var value = \(propertySwiftType)\(initializer)") diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift index 9d625d56c..35858609d 100644 --- a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -45,9 +45,12 @@ extension Model { } // Make new input and output shapes, plus their members, with the new ID - let newInput = newStruct(newID: newInputShapeID, original: inputShape) + // Add UsedAsInput and UsedAsOutput traits to the input/output structures + // These traits allow us to identify inputs/outputs by trait, but allow us to + // leave the Smithy input & output traits as set on the original model. + let newInput = newStruct(newID: newInputShapeID, newTraits: [UsedAsInputTrait()], original: inputShape) let newInputShapeMembers = try renamedMembers(newID: newInputShapeID, original: inputShape) - let newOutput = newStruct(newID: newOutputShapeID, original: outputShape) + let newOutput = newStruct(newID: newOutputShapeID, newTraits: [UsedAsOutputTrait()], original: outputShape) let newOutputShapeMembers = try renamedMembers(newID: newOutputShapeID, original: outputShape) // Add the new input & output and their members to the new shape dictionary. @@ -74,10 +77,10 @@ extension Model { return Model(version: version, metadata: metadata, shapes: newShapes) } - private func newStruct(newID: ShapeID, original: StructureShape) -> StructureShape { + private func newStruct(newID: ShapeID, newTraits: TraitCollection, original: StructureShape) -> StructureShape { StructureShape( id: newID, - traits: original.traits, + traits: original.traits.adding(newTraits), memberIDs: original.memberIDs.map { .init(id: newID, member: $0.member) } ) } diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift index b321e2f84..804d1efd2 100644 --- a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -73,7 +73,11 @@ public struct SymbolProvider { case .double, .bigDecimal: return "Swift.Double" case .blob: - return "Foundation.Data" + if shape.hasTrait(StreamingTrait.self) { + return "Smithy.ByteStream" + } else { + return "Foundation.Data" + } case .timestamp: return "Foundation.Date" case .document: diff --git a/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift b/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift index b6ed3fcbe..099f9d9e9 100644 --- a/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift +++ b/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift @@ -27,7 +27,7 @@ public protocol ShapeDeserializer { func readBigInteger(_ schema: Schema) throws -> Int64 func readBigDecimal(_ schema: Schema) throws -> Double func readString(_ schema: Schema) throws -> String - func readDocument(_ schema: Schema) throws -> any SmithyDocument + func readDocument(_ schema: Schema) throws -> Smithy.Document func readTimestamp(_ schema: Schema) throws -> Date func readNull(_ schema: Schema) throws -> T? func readDataStream(_ schema: Schema) throws -> ByteStream diff --git a/Sources/SmithySerialization/Serialization/SerializableStruct.swift b/Sources/SmithySerialization/Serialization/SerializableStruct.swift index d946ddb15..b0d5e4169 100644 --- a/Sources/SmithySerialization/Serialization/SerializableStruct.swift +++ b/Sources/SmithySerialization/Serialization/SerializableStruct.swift @@ -5,6 +5,6 @@ // SPDX-License-Identifier: Apache-2.0 // -public protocol SerializableStruct: SerializableShape, CustomDebugStringConvertible { +public protocol SerializableStruct: SerializableShape { static var writeConsumer: WriteStructConsumer { get } } From 4e3ea80ae468c32caa71e220acc045f27ee62a06 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 16 Feb 2026 10:40:58 -0600 Subject: [PATCH 28/29] Fix ktlint --- .../software/amazon/smithy/swift/codegen/SwiftSettings.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3d4e844fc..9c2397927 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 @@ -24,9 +24,9 @@ import software.amazon.smithy.protocol.traits.Rpcv2CborTrait import software.amazon.smithy.swift.codegen.utils.clientName import software.amazon.smithy.swift.codegen.utils.sdkId import java.util.logging.Logger -import kotlin.jvm.optionals.getOrNull import kotlin.collections.listOf import kotlin.jvm.optionals.getOrElse +import kotlin.jvm.optionals.getOrNull import kotlin.streams.toList private const val SERVICE = "service" From b51a5461c13cdbe03f9d02deb78c08a08a5b49e7 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Fri, 20 Feb 2026 15:18:32 -0600 Subject: [PATCH 29/29] Pin swift-argument-parser --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index c628cec6f..2bf2b8885 100644 --- a/Package.swift +++ b/Package.swift @@ -62,7 +62,7 @@ let package = Package( dependencies: { var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.58.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.6.2"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.22.0"),