From 495f2e1eeaec06c484a4c477feac47608f5d9d94 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 4 Nov 2025 15:23:33 -0600 Subject: [PATCH 01/22] 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 ca5b3314f0ae8eaddca9574f3d1f000bfb3c0f2a Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 5 Nov 2025 09:21:51 -0600 Subject: [PATCH 02/22] 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 a781bbcb8..c8b62c889 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 212120efe53751192510aa81569690069b7e839c Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 5 Nov 2025 10:17:42 -0600 Subject: [PATCH 03/22] 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 fcdf1bf6822c445ac7b15e2ee7c4bc29627c7ba1 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 5 Nov 2025 11:12:14 -0600 Subject: [PATCH 04/22] 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 aa823be48321468663fd3ba689ad2ec6484772cc Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 10:53:04 -0600 Subject: [PATCH 05/22] feat: Add Swift-native codegen plugin --- Package.swift | 19 ++++++ .../SmithyCodeGeneratorPlugin.swift | 59 +++++++++++++++++++ .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 41 +++++++++++++ Sources/SmithyCodegenCore/SmithyModel.swift | 18 ++++++ .../swift/codegen/DirectedSwiftCodegen.kt | 3 + .../codegen/SmithyModelFileInfoGenerator.kt | 19 ++++++ .../smithy/swift/codegen/SwiftWriter.kt | 2 +- .../HTTPBindingProtocolGenerator.kt | 5 +- 8 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift create mode 100644 Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift create mode 100644 Sources/SmithyCodegenCore/SmithyModel.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..03e338843 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,23 @@ let package = Package( .target( name: "SmithyWaitersAPI" ), + .plugin( + name: "SmithyCodeGenerator", + capability: .buildTool(), + dependencies: [ + "SmithyCodegenCLI", + ] + ), + .executableTarget( + name: "SmithyCodegenCLI", + dependencies: [ + "SmithyCodegenCore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), + .target( + name: "SmithyCodegenCore" + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift new file mode 100644 index 000000000..dd19b6e2f --- /dev/null +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -0,0 +1,59 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +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 `SmithySchemaCodegenTool` from the plugin's tools. + let generatorTool = try context.tool(named: "SmithyCodegenCLI") + + // Construct a build command for each source file with a particular suffix. + return try sourceFiles.map(\.path).compactMap { + try createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: generatorTool.path) + } + } + + private func createBuildCommand(for inputPath: Path, in outputDirectoryPath: Path, with generatorToolPath: Path) throws -> Command? { + // Skip any file that isn't the model.json for this service. + guard inputPath.lastComponent == "smithy-model-file-info.txt" else { return nil } + + // Get the smithy model path. + let locationData = try Data(contentsOf: URL(filePath: inputPath.string)) + guard let location = String(data: locationData, encoding: .utf8) else { + throw SmithySchemaGeneratorPluginError("smithy-model-file-info.txt did not contain valid UTF-8") + } + let modelPathURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .appendingPathComponent(location.trimmingCharacters(in: .whitespacesAndNewlines)) + let modelPath = Path(modelPathURL.path) + + // Return a command that will run during the build to generate the output file. + let inputName = inputPath.lastComponent + let outputPath = outputDirectoryPath.appending("Schemas.swift") + return .buildCommand( + displayName: "Generating Schemas.swift from \(inputName)", + executable: generatorToolPath, + arguments: [modelPath, outputPath], + inputFiles: [inputPath, modelPath], + outputFiles: [outputPath] + ) + } +} + +struct SmithySchemaGeneratorPluginError: Error { + let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift new file mode 100644 index 000000000..d4dd7f0b6 --- /dev/null +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -0,0 +1,41 @@ +// +// 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 path to the JSON model file.") + var modelPath: String + + @Argument(help: "The full path to write the output file. Intermediate directories will be created as needed.") + var outputPath: String + + func run() async throws { + guard FileManager.default.fileExists(atPath: modelPath) else { + throw SmithySchemaCodegenToolError(localizedDescription: "no file at model path") + } + let model = try SmithyModel(modelFileURL: URL(fileURLWithPath: modelPath)) + + let outputFileURL = URL(fileURLWithPath: outputPath) + let contents = """ + // My Awesome File + + /// The count of bytes in the model. + public let modelCount = \(model.count) + """ + try FileManager.default.createDirectory(at: outputFileURL.deletingLastPathComponent(), withIntermediateDirectories: true) + try Data(contents.utf8).write(to: outputFileURL) + } +} + +struct SmithySchemaCodegenToolError: Error { + let localizedDescription: String +} diff --git a/Sources/SmithyCodegenCore/SmithyModel.swift b/Sources/SmithyCodegenCore/SmithyModel.swift new file mode 100644 index 000000000..f5dbc7b4a --- /dev/null +++ b/Sources/SmithyCodegenCore/SmithyModel.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.URL + +public struct SmithyModel { + public let count: Int + + public init(modelFileURL: URL) throws { + let modelData = try Data(contentsOf: modelFileURL) + self.count = modelData.count + } +} 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..e8b588349 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt @@ -0,0 +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.expectTrait + +class SmithyModelFileInfoGenerator( + val ctx: ProtocolGenerator.GenerationContext, +) { + fun writeSmithyModelFileInfo() { + val model = ctx.service.expectTrait().sdkId + .replace(",", "") + .lowercase() + .replace(" ", "-") + ctx.delegator.useFileWriter("Sources/${ctx.settings.moduleName}/smithy-model-file-info.txt") { writer -> + writer.write("codegen/sdk-codegen/aws-models/${model}.json") + } + } +} \ No newline at end of file diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt index 49c989f82..e6520e37a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt @@ -175,7 +175,7 @@ class SwiftWriter( // Also leave out the headers when JSON or the version file is being written, // as indicated by the file extension. val isPackageManifest = fullPackageName == "Package" - val isNonSwiftSourceFile = listOf(".json", ".version").any { fullPackageName.endsWith(it) } + val isNonSwiftSourceFile = listOf(".json", ".version", ".txt").any { fullPackageName.endsWith(it) } val noHeader = isPackageManifest || isNonSwiftSourceFile return contents.takeIf { noHeader } ?: (copyrightNotice + imports + contents) } 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 cfd98792a..9c940f40b 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 @@ -209,8 +209,9 @@ 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" } + false +// ctx.service.allTraits.keys +// .any { it.name == "rpcv2Cbor" } override fun generateSchemas(ctx: ProtocolGenerator.GenerationContext) { if (!usesSchemaBasedSerialization(ctx)) return // temporary condition From e817d55c1c4c4f956e4c89995281a162c115a5c5 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 11:09:48 -0600 Subject: [PATCH 06/22] Fix lint --- .../SmithyCodeGeneratorPlugin.swift | 6 +++++- Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift | 7 +++++-- .../swift/codegen/SmithyModelFileInfoGenerator.kt | 15 +++++++++------ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index dd19b6e2f..5d51bf3a3 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -24,7 +24,11 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { } } - private func createBuildCommand(for inputPath: Path, in outputDirectoryPath: Path, with generatorToolPath: Path) throws -> Command? { + private func createBuildCommand( + for inputPath: Path, + in outputDirectoryPath: Path, + with generatorToolPath: Path + ) throws -> Command? { // Skip any file that isn't the model.json for this service. guard inputPath.lastComponent == "smithy-model-file-info.txt" else { return nil } diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index d4dd7f0b6..f965c8ad4 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -27,11 +27,14 @@ struct SmithyCodegenCLI: AsyncParsableCommand { let outputFileURL = URL(fileURLWithPath: outputPath) let contents = """ // My Awesome File - + /// The count of bytes in the model. public let modelCount = \(model.count) """ - try FileManager.default.createDirectory(at: outputFileURL.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.createDirectory( + at: outputFileURL.deletingLastPathComponent(), + withIntermediateDirectories: true + ) try Data(contents.utf8).write(to: outputFileURL) } } 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 e8b588349..65533e24e 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 @@ -8,12 +8,15 @@ class SmithyModelFileInfoGenerator( val ctx: ProtocolGenerator.GenerationContext, ) { fun writeSmithyModelFileInfo() { - val model = ctx.service.expectTrait().sdkId - .replace(",", "") - .lowercase() - .replace(" ", "-") + val model = + ctx.service + .expectTrait() + .sdkId + .replace(",", "") + .lowercase() + .replace(" ", "-") ctx.delegator.useFileWriter("Sources/${ctx.settings.moduleName}/smithy-model-file-info.txt") { writer -> - writer.write("codegen/sdk-codegen/aws-models/${model}.json") + writer.write("codegen/sdk-codegen/aws-models/$model.json") } } -} \ No newline at end of file +} From 6a9c537af096532f9e70f1b3a5845cf6a5db26ee Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 11:29:43 -0600 Subject: [PATCH 07/22] Make service trait optional --- .../codegen/SmithyModelFileInfoGenerator.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 65533e24e..8e1a14f25 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 @@ -2,21 +2,23 @@ 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.expectTrait +import software.amazon.smithy.swift.codegen.model.getTrait class SmithyModelFileInfoGenerator( val ctx: ProtocolGenerator.GenerationContext, ) { fun writeSmithyModelFileInfo() { - val model = - ctx.service - .expectTrait() + ctx.service.getTrait()?.let { serviceTrait -> + val filename = "Sources/${ctx.settings.moduleName}/smithy-model-file-info.txt" + val modelFileName = serviceTrait .sdkId - .replace(",", "") .lowercase() + .replace(",", "") .replace(" ", "-") - ctx.delegator.useFileWriter("Sources/${ctx.settings.moduleName}/smithy-model-file-info.txt") { writer -> - writer.write("codegen/sdk-codegen/aws-models/$model.json") + val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" + ctx.delegator.useFileWriter(filename) { writer -> + writer.write(contents) + } } } } From 33cc093b8eb2f0a9ed6177d77d7cc99500634819 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 11:34:09 -0600 Subject: [PATCH 08/22] Fix ktlint again --- .../swift/codegen/SmithyModelFileInfoGenerator.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 8e1a14f25..ae02c4ff4 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 @@ -10,11 +10,12 @@ class SmithyModelFileInfoGenerator( fun writeSmithyModelFileInfo() { ctx.service.getTrait()?.let { serviceTrait -> val filename = "Sources/${ctx.settings.moduleName}/smithy-model-file-info.txt" - val modelFileName = serviceTrait - .sdkId - .lowercase() - .replace(",", "") - .replace(" ", "-") + val modelFileName = + serviceTrait + .sdkId + .lowercase() + .replace(",", "") + .replace(" ", "-") val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" ctx.delegator.useFileWriter(filename) { writer -> writer.write(contents) From 6b545756f277b9471f4d34e79758d455b57a282c Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 11:44:12 -0600 Subject: [PATCH 09/22] Refactor code generator plugin --- .../SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 5d51bf3a3..616dffbb0 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -34,11 +34,12 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Get the smithy model path. let locationData = try Data(contentsOf: URL(filePath: inputPath.string)) - guard let location = String(data: locationData, encoding: .utf8) else { + let locationString = String(data: locationData, encoding: .utf8) + guard let location = locationString?.trimmingCharacters(in: .whitespacesAndNewlines) else { throw SmithySchemaGeneratorPluginError("smithy-model-file-info.txt did not contain valid UTF-8") } - let modelPathURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent(location.trimmingCharacters(in: .whitespacesAndNewlines)) + let currentWorkingDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + let modelPathURL = currentWorkingDirectoryURL.appendingPathComponent(location) let modelPath = Path(modelPathURL.path) // Return a command that will run during the build to generate the output file. From ae474f12c5019617f75fa23e1fc22b91d002fd3b Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 15:52:23 -0600 Subject: [PATCH 10/22] Convert smithy model info file to .json --- .../SmithyCodeGeneratorPlugin.swift | 26 ++++++++++--------- .../codegen/SmithyModelFileInfoGenerator.kt | 4 +-- .../smithy/swift/codegen/SwiftWriter.kt | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 616dffbb0..7e2e9b5b7 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -29,28 +29,26 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { in outputDirectoryPath: Path, with generatorToolPath: Path ) throws -> Command? { - // Skip any file that isn't the model.json for this service. - guard inputPath.lastComponent == "smithy-model-file-info.txt" else { return nil } + let currentWorkingDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + // Skip any file that isn't the smithy-model-info.json for this service. + guard inputPath.lastComponent == "smithy-model-info.json" else { return nil } // Get the smithy model path. - let locationData = try Data(contentsOf: URL(filePath: inputPath.string)) - let locationString = String(data: locationData, encoding: .utf8) - guard let location = locationString?.trimmingCharacters(in: .whitespacesAndNewlines) else { - throw SmithySchemaGeneratorPluginError("smithy-model-file-info.txt did not contain valid UTF-8") - } - let currentWorkingDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - let modelPathURL = currentWorkingDirectoryURL.appendingPathComponent(location) + let modelInfoData = try Data(contentsOf: URL(filePath: inputPath.string)) + let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData) + let modelPathURL = currentWorkingDirectoryURL.appendingPathComponent(smithyModelInfo.path) let modelPath = Path(modelPathURL.path) // Return a command that will run during the build to generate the output file. let inputName = inputPath.lastComponent - let outputPath = outputDirectoryPath.appending("Schemas.swift") + let schemasSwiftPath = outputDirectoryPath.appending("Schemas.swift") return .buildCommand( displayName: "Generating Schemas.swift from \(inputName)", executable: generatorToolPath, - arguments: [modelPath, outputPath], + arguments: [modelPath, schemasSwiftPath], inputFiles: [inputPath, modelPath], - outputFiles: [outputPath] + outputFiles: [schemasSwiftPath] ) } } @@ -62,3 +60,7 @@ struct SmithySchemaGeneratorPluginError: Error { self.localizedDescription = localizedDescription } } + +struct SmithyModelInfo: Codable { + let path: String +} 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 ae02c4ff4..0662d8d7d 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 @@ -9,7 +9,7 @@ class SmithyModelFileInfoGenerator( ) { fun writeSmithyModelFileInfo() { ctx.service.getTrait()?.let { serviceTrait -> - val filename = "Sources/${ctx.settings.moduleName}/smithy-model-file-info.txt" + val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" val modelFileName = serviceTrait .sdkId @@ -18,7 +18,7 @@ class SmithyModelFileInfoGenerator( .replace(" ", "-") val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" ctx.delegator.useFileWriter(filename) { writer -> - writer.write(contents) + writer.write("{\"path\":\"$contents\"}") } } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt index e6520e37a..49c989f82 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt @@ -175,7 +175,7 @@ class SwiftWriter( // Also leave out the headers when JSON or the version file is being written, // as indicated by the file extension. val isPackageManifest = fullPackageName == "Package" - val isNonSwiftSourceFile = listOf(".json", ".version", ".txt").any { fullPackageName.endsWith(it) } + val isNonSwiftSourceFile = listOf(".json", ".version").any { fullPackageName.endsWith(it) } val noHeader = isPackageManifest || isNonSwiftSourceFile return contents.takeIf { noHeader } ?: (copyrightNotice + imports + contents) } From 70f76690662eb3761fc5f5221b6d2fdf3fc25da4 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 15:58:10 -0600 Subject: [PATCH 11/22] Fix Swift 5.9 URL initializer --- Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 7e2e9b5b7..4f830d1c7 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -35,7 +35,7 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { guard inputPath.lastComponent == "smithy-model-info.json" else { return nil } // Get the smithy model path. - let modelInfoData = try Data(contentsOf: URL(filePath: inputPath.string)) + let modelInfoData = try Data(contentsOf: URL(fileURLWithPath: inputPath.string)) let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData) let modelPathURL = currentWorkingDirectoryURL.appendingPathComponent(smithyModelInfo.path) let modelPath = Path(modelPathURL.path) From 3d623e6fb7cf32d5ad774982f02f2576dff31fd4 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 17:24:09 -0600 Subject: [PATCH 12/22] Code cleanup --- .../SmithyCodeGeneratorPlugin.swift | 34 +++++++++---------- .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 9 ++++- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 4f830d1c7..d3e1bede5 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -5,7 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // -import Foundation +import struct Foundation.Data +import class Foundation.FileManager +import class Foundation.JSONDecoder +import struct Foundation.URL import PackagePlugin @main @@ -15,12 +18,12 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // This plugin only runs for package targets that can have source files. guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] } - // Retrieve the `SmithySchemaCodegenTool` from the plugin's tools. - let generatorTool = try context.tool(named: "SmithyCodegenCLI") + // 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(for: $0, in: context.pluginWorkDirectory, with: generatorTool.path) + try createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path) } } @@ -41,26 +44,23 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { let modelPath = Path(modelPathURL.path) // Return a command that will run during the build to generate the output file. - let inputName = inputPath.lastComponent - let schemasSwiftPath = outputDirectoryPath.appending("Schemas.swift") + let modelCountSwiftPath = outputDirectoryPath.appending("ModelCount.swift") return .buildCommand( - displayName: "Generating Schemas.swift from \(inputName)", + displayName: "Generating Swift source files from \(smithyModelInfo.path)", executable: generatorToolPath, - arguments: [modelPath, schemasSwiftPath], + arguments: [modelPath, modelCountSwiftPath], inputFiles: [inputPath, modelPath], - outputFiles: [schemasSwiftPath] + outputFiles: [modelCountSwiftPath] ) } } -struct SmithySchemaGeneratorPluginError: Error { - let localizedDescription: String - - init(_ localizedDescription: String) { - self.localizedDescription = localizedDescription - } +/// 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 } -struct SmithyModelInfo: Codable { - let path: String +struct Err: Error { + var localizedDescription: String { "boom" } } diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index f965c8ad4..4aa65a65d 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -26,7 +26,14 @@ struct SmithyCodegenCLI: AsyncParsableCommand { let outputFileURL = URL(fileURLWithPath: outputPath) let contents = """ - // My Awesome File + // + // Copyright Amazon.com Inc. or its affiliates. + // All Rights Reserved. + // + // SPDX-License-Identifier: Apache-2.0 + // + + // Code generated by SmithyCodegenCLI. DO NOT EDIT! /// The count of bytes in the model. public let modelCount = \(model.count) From d535e662bbfc658bcf6bb4f0666cc1e928498fb0 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sun, 9 Nov 2025 17:54:24 -0600 Subject: [PATCH 13/22] Revert disabling of schemas --- .../codegen/integration/HTTPBindingProtocolGenerator.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 9c940f40b..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 @@ -209,9 +209,8 @@ 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 - false -// ctx.service.allTraits.keys -// .any { it.name == "rpcv2Cbor" } + ctx.service.allTraits.keys + .any { it.name == "rpcv2Cbor" } override fun generateSchemas(ctx: ProtocolGenerator.GenerationContext) { if (!usesSchemaBasedSerialization(ctx)) return // temporary condition From 52f9532844a610034f4a5b19c2fb2e4dcc60234f Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 11:37:58 -0600 Subject: [PATCH 14/22] Schemas generate for entire SDK --- Package.swift | 8 +- .../SmithyCodeGeneratorPlugin.swift | 28 +++--- Sources/Smithy/Schema/SchemaTraits.swift | 21 ++++ .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 65 +++++++------ Sources/SmithyCodegenCore/AST/ASTError.swift | 14 +++ Sources/SmithyCodegenCore/AST/ASTMember.swift | 11 +++ Sources/SmithyCodegenCore/AST/ASTModel.swift | 12 +++ Sources/SmithyCodegenCore/AST/ASTNode.swift | 45 +++++++++ .../SmithyCodegenCore/AST/ASTReference.swift | 10 ++ Sources/SmithyCodegenCore/AST/ASTShape.swift | 31 ++++++ Sources/SmithyCodegenCore/AST/ASTType.swift | 37 ++++++++ Sources/SmithyCodegenCore/CodeGenerator.swift | 30 ++++++ Sources/SmithyCodegenCore/CodegenError.swift | 14 +++ Sources/SmithyCodegenCore/Model/Model.swift | 87 +++++++++++++++++ .../SmithyCodegenCore/Model/ModelError.swift | 14 +++ Sources/SmithyCodegenCore/Model/Node.swift | 63 ++++++++++++ Sources/SmithyCodegenCore/Model/Shape.swift | 86 +++++++++++++++++ Sources/SmithyCodegenCore/Model/ShapeID.swift | 61 ++++++++++++ .../SmithyCodegenCore/Model/ShapeType.swift | 91 ++++++++++++++++++ .../Resources/DefaultSwiftHeader.txt | 8 ++ .../Schemas/Shape+Schema.swift | 53 +++++++++++ .../Schemas/SmithySchemaCodegen.swift | 95 +++++++++++++++++++ Sources/SmithyCodegenCore/SmithyModel.swift | 18 ---- Sources/SmithyCodegenCore/String+Utils.swift | 23 +++++ Sources/SmithyCodegenCore/SwiftWriter.swift | 58 +++++++++++ .../SmithyCodegenCoreTests/ShapeIDTests.swift | 32 +++++++ .../HTTPBindingProtocolGenerator.kt | 30 ++++-- .../serde/schema/SchemaGenerator.kt | 2 +- .../integration/serde/schema/SchemaTraits.kt | 4 +- 29 files changed, 982 insertions(+), 69 deletions(-) create mode 100644 Sources/Smithy/Schema/SchemaTraits.swift 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 create mode 100644 Sources/SmithyCodegenCore/CodegenError.swift create mode 100644 Sources/SmithyCodegenCore/Model/Model.swift create mode 100644 Sources/SmithyCodegenCore/Model/ModelError.swift create mode 100644 Sources/SmithyCodegenCore/Model/Node.swift create mode 100644 Sources/SmithyCodegenCore/Model/Shape.swift create mode 100644 Sources/SmithyCodegenCore/Model/ShapeID.swift create mode 100644 Sources/SmithyCodegenCore/Model/ShapeType.swift create mode 100644 Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt create mode 100644 Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift create mode 100644 Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift delete mode 100644 Sources/SmithyCodegenCore/SmithyModel.swift create mode 100644 Sources/SmithyCodegenCore/String+Utils.swift create mode 100644 Sources/SmithyCodegenCore/SwiftWriter.swift create mode 100644 Tests/SmithyCodegenCoreTests/ShapeIDTests.swift diff --git a/Package.swift b/Package.swift index 03e338843..c34f0dcfc 100644 --- a/Package.swift +++ b/Package.swift @@ -275,7 +275,9 @@ let package = Package( ] ), .target( - name: "SmithyCodegenCore" + name: "SmithyCodegenCore", + dependencies: ["Smithy"], + resources: [ .process("Resources") ] ), .testTarget( name: "ClientRuntimeTests", @@ -353,5 +355,9 @@ let package = Package( name: "SmithyHTTPAPITests", dependencies: ["SmithyHTTPAPI"] ), + .testTarget( + name: "SmithyCodegenCoreTests", + dependencies: ["SmithyCodegenCore"] + ), ].compactMap { $0 } ) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index d3e1bede5..116d4b9d0 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -23,34 +23,40 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Construct a build command for each source file with a particular suffix. return try sourceFiles.map(\.path).compactMap { - try createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path) + 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? { - let currentWorkingDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - // Skip any file that isn't the smithy-model-info.json for this service. guard inputPath.lastComponent == "smithy-model-info.json" else { return nil } + 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 = currentWorkingDirectoryURL.appendingPathComponent(smithyModelInfo.path) + let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path) let modelPath = Path(modelPathURL.path) - // Return a command that will run during the build to generate the output file. - let modelCountSwiftPath = outputDirectoryPath.appending("ModelCount.swift") + // 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 \(smithyModelInfo.path)", + displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", executable: generatorToolPath, - arguments: [modelPath, modelCountSwiftPath], + arguments: [ + "--schemas-path", schemasSwiftPath, + modelPath + ], inputFiles: [inputPath, modelPath], - outputFiles: [modelCountSwiftPath] + outputFiles: [schemasSwiftPath] ) } } @@ -60,7 +66,3 @@ private struct SmithyModelInfo: Decodable { /// The path to the model, from the root of the target's project. Required. let path: String } - -struct Err: Error { - var localizedDescription: String { "boom" } -} diff --git a/Sources/Smithy/Schema/SchemaTraits.swift b/Sources/Smithy/Schema/SchemaTraits.swift new file mode 100644 index 000000000..43d9ce385 --- /dev/null +++ b/Sources/Smithy/Schema/SchemaTraits.swift @@ -0,0 +1,21 @@ +// +// 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 permittedTraitIDs: Set = [ + "smithy.api#sparse", + "smithy.api#input", + "smithy.api#output", + "smithy.api#error", + "smithy.api#enumValue", + "smithy.api#jsonName", + "smithy.api#required", + "smithy.api#default", + "smithy.api#timestampFormat", +] diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index 4aa65a65d..bf8598d60 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -12,40 +12,51 @@ import SmithyCodegenCore @main struct SmithyCodegenCLI: AsyncParsableCommand { - @Argument(help: "The full path to the JSON model file.") + @Argument(help: "The full or relative path to the JSON model file.") var modelPath: String - @Argument(help: "The full path to write the output file. Intermediate directories will be created as needed.") - var outputPath: String + @Option(help: "The full or relative path to write the schemas output file.") + var schemasPath: String? func run() async throws { - guard FileManager.default.fileExists(atPath: modelPath) else { - throw SmithySchemaCodegenToolError(localizedDescription: "no file at model path") + 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 } - let model = try SmithyModel(modelFileURL: URL(fileURLWithPath: modelPath)) - - let outputFileURL = URL(fileURLWithPath: outputPath) - let contents = """ - // - // Copyright Amazon.com Inc. or its affiliates. - // All Rights Reserved. - // - // SPDX-License-Identifier: Apache-2.0 - // - - // Code generated by SmithyCodegenCLI. DO NOT EDIT! - - /// The count of bytes in the model. - public let modelCount = \(model.count) - """ - try FileManager.default.createDirectory( - at: outputFileURL.deletingLastPathComponent(), - withIntermediateDirectories: true - ) - try Data(contents.utf8).write(to: outputFileURL) } } -struct SmithySchemaCodegenToolError: Error { +struct SmithyCodegenCLIError: Error { let localizedDescription: String } 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..f03fefef9 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +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..5ff69fb15 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +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..8d5a61c44 --- /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: Sendable { + 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..6132359df --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTReference.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +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..92cb16bd7 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -0,0 +1,31 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +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..cdf3758bf --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTType.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +enum ASTType: String, Decodable { + // These cases are all the 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..3c735faa0 --- /dev/null +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +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 model from the model file + let model = try Model(modelFileURL: modelFileURL) + + // If a schema file URL was provided, generate it + if let schemasFileURL { + let schemaContents = try SmithySchemaCodegen().generate(model: model) + try Data(schemaContents.utf8).write(to: schemasFileURL) + } + } +} diff --git a/Sources/SmithyCodegenCore/CodegenError.swift b/Sources/SmithyCodegenCore/CodegenError.swift new file mode 100644 index 000000000..6734b7329 --- /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 +// + +struct CodegenError: Error { + let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift new file mode 100644 index 000000000..6a081b9c9 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -0,0 +1,87 @@ +// +// 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 class Model { + public let version: String + public let metadata: Node? + public let shapes: [ShapeID: Shape] + + public convenience init(modelFileURL: URL) throws { + let modelData = try Data(contentsOf: modelFileURL) + let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) + try self.init(astModel: astModel) + } + + init(astModel: ASTModel) throws { + self.version = astModel.smithy + self.metadata = astModel.metadata?.modelNode + let idToShapePairs = try astModel.shapes.map { try Self.shapePair(id: $0.key, astShape: $0.value) } + let idToMemberShapePairs = try astModel.shapes.flatMap { astShape in + try Self.memberShapePairs(id: astShape.key, astShape: astShape.value) + } + self.shapes = Dictionary(uniqueKeysWithValues: idToShapePairs + idToMemberShapePairs) + + // self is now initialized, perform post-initialization wireup + + // set the Shapes with references back to this model + self.shapes.values.forEach { $0.model = self } + + // set the memberIDs for each Shape + self.shapes.values.filter { $0.type != .member }.forEach { shape in + let namespace = shape.id.namespace + let name = shape.id.name + let memberIDs: [ShapeID] = Array(self.shapes.keys) + let filteredMemberIDs = memberIDs.filter { + $0.namespace == namespace && $0.name == name && $0.member != nil + } + shape.memberIDs = filteredMemberIDs.sorted() + } + } + + private static func shapePair(id: String, astShape: ASTShape) throws -> (ShapeID, Shape) { + let shapeID = try ShapeID(id) + let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value.modelNode) } ?? [] + let shape = Shape( + id: shapeID, + type: astShape.type.modelType, + traits: Dictionary(uniqueKeysWithValues: idToTraitPairs), + targetID: nil + ) + return (shapeID, shape) + } + + private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, Shape)] { + var baseMembers = (astShape.members ?? [:]) + if let member = astShape.member { + baseMembers["member"] = member + } + if let key = astShape.key { + baseMembers["key"] = key + } + if let value = astShape.value { + baseMembers["value"] = value + } + return try baseMembers.map { astMember in + let memberID = ShapeID(id: try ShapeID(id), member: astMember.key) + let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value.modelNode) } + let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? []) + let targetID = try ShapeID(astMember.value.target) + return (memberID, Shape(id: memberID, type: .member, traits: traits, targetID: targetID)) + } + } + + 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/Node.swift b/Sources/SmithyCodegenCore/Model/Node.swift new file mode 100644 index 000000000..7076405a8 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Node.swift @@ -0,0 +1,63 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Contains the value of a Smithy Node. +/// +/// Smithy node data is basically the same as the data that can be stored in JSON. +/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. +/// +/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values +public enum Node: Sendable { + case object([String: Node]) + case list([Node]) + case string(String) + case number(Double) + case boolean(Bool) + case null +} + +extension ASTNode { + + /// Creates a model Node from a AST-specific ASTNode. + var modelNode: Node { + switch self { + case .object(let object): + return .object(object.mapValues { $0.modelNode }) + case .list(let list): + return .list(list.map { $0.modelNode }) + case .string(let value): + return .string(value) + case .number(let value): + return .number(value) + case .boolean(let value): + return .boolean(value) + case .null: + return .null + } + } +} + +extension Node { + + 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/Model/Shape.swift b/Sources/SmithyCodegenCore/Model/Shape.swift new file mode 100644 index 000000000..73b36dcc7 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Shape.swift @@ -0,0 +1,86 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public class Shape { + public let id: ShapeID + public let type: ShapeType + public internal(set) var traits: [ShapeID: Node] + var targetID: ShapeID? + var memberIDs: [ShapeID] = [] + weak var model: Model? + + public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node], targetID: ShapeID?) { + self.id = id + self.type = type + self.traits = traits + self.targetID = targetID + } + + public func hasTrait(_ traitID: ShapeID) -> Bool { + traits[traitID] != nil + } + + public var members: [Shape] { + get throws { + guard let model else { throw ModelError("id \"\(id)\" model not set") } + return memberIDs.map { model.shapes[$0]! } + } + } + + public var target: Shape? { + get throws { + guard let targetID else { return nil } + guard let model else { throw ModelError("id \"\(id)\" model not set") } + return model.shapes[targetID] + } + } + + public var children: Set { + get throws { + var c = Set() + try children(children: &c) + return c + } + } + + private func children(children: inout Set) throws { + let shapes = try candidates(for: self) + for shape in shapes { + if children.contains(shape) { continue } + children.insert(shape) + try shape.members.map { try $0.target }.compactMap { $0 }.forEach { + children.insert($0) + try $0.children(children: &children) + } + } + } + + private func candidates(for shape: Shape) throws -> [Shape] { + (try [try shape.target] + shape.members.map { try $0.target }).compactMap { $0 } + } +} + +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/Model/ShapeID.swift b/Sources/SmithyCodegenCore/Model/ShapeID.swift new file mode 100644 index 000000000..2b9c9e775 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeID.swift @@ -0,0 +1,61 @@ +// +// 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 { + public let namespace: String + public let name: String + public let member: String? + + public init(_ id: String) throws { + let splitOnPound = id.split(separator: "#") + guard splitOnPound.count == 2 else { + throw ModelError("id \"\(id)\" does not have a #") + } + guard let namespace = splitOnPound.first, !namespace.isEmpty else { + throw ModelError("id \"\(id)\" does not have a nonempty namespace") + } + self.namespace = String(namespace) + guard let name = splitOnPound.last, !name.isEmpty else { + throw ModelError("id \"\(id)\" does not have a nonempty name") + } + self.name = String(name) + self.member = nil + } + + 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 } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeType.swift b/Sources/SmithyCodegenCore/Model/ShapeType.swift new file mode 100644 index 000000000..7d0d47dec --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeType.swift @@ -0,0 +1,91 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// 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). +public enum ShapeType: String { + 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 +} + +extension ASTType { + var modelType: ShapeType { + 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: + fatalError("\"apply\" AST shapes not implemented") + } + } +} 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/Shape+Schema.swift b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift new file mode 100644 index 000000000..8f11c82f6 --- /dev/null +++ b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift @@ -0,0 +1,53 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension Shape { + func schemaVarName() throws -> String { + if id.namespace == "smithy.api" { + try id.preludeSchemaVarName() + } else { + try id.schemaVarName() + } + } +} + +extension ShapeID { + + func preludeSchemaVarName() throws -> String { + 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 CodegenError("Unhandled prelude type converted to schemaVar: \"\(name)\"") + } + return "Smithy.Prelude.\(propertyName)" + } + + func schemaVarName() throws -> String { + guard member == nil else { throw CodegenError("Assigning member schema to a var") } + let namespacePortion = namespace.replacingOccurrences(of: ".", with: "_") + let namePortion = name + return "schema2__\(namespacePortion)__\(namePortion)" + } +} diff --git a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift new file mode 100644 index 000000000..5c375c938 --- /dev/null +++ b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift @@ -0,0 +1,95 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Smithy + +package struct SmithySchemaCodegen { + + package init() {} + + package func generate(model: Model) throws -> String { + let writer = SwiftWriter() + writer.write("import class Smithy.Schema") + writer.write("import enum Smithy.Prelude") + writer.write("") + + writer.write("// Model has \(model.shapes.count) shapes") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .service }.count) services") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .operation }.count) operations") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .resource }.count) resources") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .structure }.count) structures") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .union }.count) unions") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .enum }.count) enums") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .intEnum }.count) intEnums") + writer.write("// Model has \(model.shapes.values.filter { $0.type == .member }.count) members") + writer.write("") + + // Write schemas for all inputs & outputs and their descendants. + let shapes = try model.shapes.values + .filter { $0.type == .structure } + .filter { try $0.hasTrait(try .init("smithy.api#input")) || $0.hasTrait(try .init("smithy.api#output")) || $0.hasTrait(try .init("smithy.api#error"))} + .map { try [$0] + $0.children } + .flatMap { $0 } + let sortedShapes = Array(Set(shapes)).sorted { $0.id.id.lowercased() < $1.id.id.lowercased() } + writer.write("// Number of schemas: \(sortedShapes.count)") + writer.write("") + for shape in sortedShapes { + try writer.openBlock("public var \(try shape.schemaVarName()): Smithy.Schema {", "}") { writer in + try writeSchema(writer: writer, shape: shape) + writer.unwrite(",") + } + writer.write("") + } + return writer.finalize() + } + + 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.keys.filter { Smithy.permittedTraitIDs.contains($0.id) } + let traitIDs = Array(relevantTraitIDs).sorted() + if !traitIDs.isEmpty { + writer.openBlock("traits: [", "],") { writer in + for traitID in traitIDs { + let trait = shape.traits[traitID]! + writer.write("\(traitID.rendered): \(trait.rendered),") + } + } + } + let members = try shape.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 target = try shape.target { + writer.write("target: \(try 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/SmithyModel.swift b/Sources/SmithyCodegenCore/SmithyModel.swift deleted file mode 100644 index f5dbc7b4a..000000000 --- a/Sources/SmithyCodegenCore/SmithyModel.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import struct Foundation.Data -import struct Foundation.URL - -public struct SmithyModel { - public let count: Int - - public init(modelFileURL: URL) throws { - let modelData = try Data(contentsOf: modelFileURL) - self.count = modelData.count - } -} diff --git a/Sources/SmithyCodegenCore/String+Utils.swift b/Sources/SmithyCodegenCore/String+Utils.swift new file mode 100644 index 000000000..d32605650 --- /dev/null +++ b/Sources/SmithyCodegenCore/String+Utils.swift @@ -0,0 +1,23 @@ +// +// 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)\"" + } +} diff --git a/Sources/SmithyCodegenCore/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftWriter.swift new file mode 100644 index 000000000..7019af41f --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftWriter.swift @@ -0,0 +1,58 @@ +// +// 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")! + 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) { + lines.append(String(repeating: " ", count: indentLevel) + line) + } + + func unwrite(_ text: String) { + guard let lastIndex = lines.indices.last else { return } + 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) + } + + func finalize() -> String { + return lines.joined(separator: "\n").appending("\n") + } +} diff --git a/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift b/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift new file mode 100644 index 000000000..a9f15590a --- /dev/null +++ b/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SmithyCodegenCore + +final class ShapeIDTests: XCTestCase { + + func test_init_createsShapeIDWithNamespace() throws { + let subject = ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.namespace, "smithy.test") + } + + func test_init_createsShapeIDWithName() throws { + let subject = ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.name, "TestName") + } + + func test_init_createsShapeIDWithMember() throws { + let subject = ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.member, "TestMember") + } + + func test_init_createsShapeIDWithoutMember() throws { + let subject = ShapeID("smithy.test#TestName") + XCTAssertNil(subject.member) + } +} 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 cfd98792a..227d6663c 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 @@ -209,18 +209,35 @@ 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" } + true +// ctx.service.allTraits.keys +// .any { it.name == "rpcv2Cbor" } override fun generateSchemas(ctx: ProtocolGenerator.GenerationContext) { if (!usesSchemaBasedSerialization(ctx)) return // temporary condition val nestedShapes = resolveShapesNeedingSchema(ctx) .filter { it.type != ShapeType.MEMBER } // Member schemas are only rendered in-line - nestedShapes.forEach { renderSchemas(ctx, it) } + .sorted() + val file = SchemaFileUtils.filename(ctx.settings, "whatever") + ctx.delegator.useFileWriter(file) { writer -> + writer.write("// Model has ${ctx.model.shapes().count()} shapes") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.SERVICE }.count()} services") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.OPERATION }.count()} operations") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.RESOURCE }.count()} resources") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.STRUCTURE }.count()} structures") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.UNION }.count()} unions") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.ENUM }.count()} enums") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.INT_ENUM }.count()} intEnums") + writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.MEMBER }.count()} members") + writer.write("") + writer.write("// Number of schemas: ${nestedShapes.count()}") + writer.write("") + } + nestedShapes.forEach { renderSchema(ctx, it) } } - private fun renderSchemas( + private fun renderSchema( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { @@ -404,10 +421,7 @@ 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) } + .map { ctx.model.expectShape(it.input.get()) } .toSet() val topLevelOutputMembers = 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 dd666794b..b1201bf91 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 @@ -49,7 +49,7 @@ class SchemaGenerator( } if (shape.members().isNotEmpty()) { writer.openBlock("members: [", "],") { - shape.members().withIndex().forEach { renderSchemaStruct(it.value, it.index) } + shape.members().sorted().withIndex().forEach { renderSchemaStruct(it.value, it.index) } } } targetShape(shape)?.let { 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 index 91a446af6..dd0ee07c0 100644 --- 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 @@ -3,10 +3,12 @@ package software.amazon.smithy.swift.codegen.integration.serde.schema val permittedTraitIDs: Set = setOf( "smithy.api#sparse", + "smithy.api#input", + "smithy.api#output", + "smithy.api#error", "smithy.api#enumValue", "smithy.api#jsonName", "smithy.api#required", "smithy.api#default", "smithy.api#timestampFormat", - "smithy.api#httpPayload", ) From 70e8a1544f649dbb9246b25aa61d40f8044c1a0b Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 19 Nov 2025 13:41:35 -0600 Subject: [PATCH 15/22] Fix method name renderSchema --- .../swift/codegen/integration/HTTPBindingProtocolGenerator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 013c420c8..227d6663c 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 @@ -237,7 +237,7 @@ abstract class HTTPBindingProtocolGenerator( nestedShapes.forEach { renderSchema(ctx, it) } } - private fun renderSchemas( + private fun renderSchema( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { From b1548a74d837fa0e07340c55203a85d2dc28e124 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 19 Nov 2025 13:46:29 -0600 Subject: [PATCH 16/22] Fix swiftlint --- Sources/SmithyCodegenCore/Model/Node.swift | 2 +- Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift | 5 ++++- Sources/SmithyCodegenCore/String+Utils.swift | 2 +- Sources/SmithyCodegenCore/SwiftWriter.swift | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/SmithyCodegenCore/Model/Node.swift b/Sources/SmithyCodegenCore/Model/Node.swift index 7076405a8..c56c50736 100644 --- a/Sources/SmithyCodegenCore/Model/Node.swift +++ b/Sources/SmithyCodegenCore/Model/Node.swift @@ -21,7 +21,7 @@ public enum Node: Sendable { } extension ASTNode { - + /// Creates a model Node from a AST-specific ASTNode. var modelNode: Node { switch self { diff --git a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift index 5c375c938..2a32b2f9f 100644 --- a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift @@ -31,7 +31,10 @@ package struct SmithySchemaCodegen { // Write schemas for all inputs & outputs and their descendants. let shapes = try model.shapes.values .filter { $0.type == .structure } - .filter { try $0.hasTrait(try .init("smithy.api#input")) || $0.hasTrait(try .init("smithy.api#output")) || $0.hasTrait(try .init("smithy.api#error"))} + .filter { + try $0.hasTrait(try .init("smithy.api#input")) || + $0.hasTrait(try .init("smithy.api#output")) || + $0.hasTrait(try .init("smithy.api#error"))} .map { try [$0] + $0.children } .flatMap { $0 } let sortedShapes = Array(Set(shapes)).sorted { $0.id.id.lowercased() < $1.id.id.lowercased() } diff --git a/Sources/SmithyCodegenCore/String+Utils.swift b/Sources/SmithyCodegenCore/String+Utils.swift index d32605650..1bb57c035 100644 --- a/Sources/SmithyCodegenCore/String+Utils.swift +++ b/Sources/SmithyCodegenCore/String+Utils.swift @@ -6,7 +6,7 @@ // extension String { - + /// Escapes special characters in the string, then surrounds it in double quotes /// to form a Swift string literal. var literal: String { diff --git a/Sources/SmithyCodegenCore/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftWriter.swift index 7019af41f..a6b30a202 100644 --- a/Sources/SmithyCodegenCore/SwiftWriter.swift +++ b/Sources/SmithyCodegenCore/SwiftWriter.swift @@ -18,6 +18,7 @@ class SwiftWriter { 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 { From 94a901dd832ca23555dc2671074f891444c04e89 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 19 Nov 2025 13:47:15 -0600 Subject: [PATCH 17/22] Fix ktlint --- .../codegen/integration/serde/schema/SchemaGenerator.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 b1201bf91..843821d46 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 @@ -49,7 +49,11 @@ class SchemaGenerator( } if (shape.members().isNotEmpty()) { writer.openBlock("members: [", "],") { - shape.members().sorted().withIndex().forEach { renderSchemaStruct(it.value, it.index) } + shape + .members() + .sorted() + .withIndex() + .forEach { renderSchemaStruct(it.value, it.index) } } } targetShape(shape)?.let { From 54a43007628b5c208e53c90510dad5993efb57d7 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 19 Nov 2025 14:00:36 -0600 Subject: [PATCH 18/22] Fix SmithyCodegenCoreTests --- Sources/SmithyCodegenCore/Model/ShapeID.swift | 14 ++++++++++---- Tests/SmithyCodegenCoreTests/ShapeIDTests.swift | 8 ++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/SmithyCodegenCore/Model/ShapeID.swift b/Sources/SmithyCodegenCore/Model/ShapeID.swift index 2b9c9e775..9792290bc 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeID.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeID.swift @@ -25,11 +25,17 @@ public struct ShapeID: Hashable { throw ModelError("id \"\(id)\" does not have a nonempty namespace") } self.namespace = String(namespace) - guard let name = splitOnPound.last, !name.isEmpty else { - throw ModelError("id \"\(id)\" does not have a nonempty name") + 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 ModelError("id \"\(id)\" has more than one $") } - self.name = String(name) - self.member = nil } public init(id: ShapeID, member: String) { diff --git a/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift b/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift index a9f15590a..928aedfbd 100644 --- a/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift +++ b/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift @@ -11,22 +11,22 @@ import SmithyCodegenCore final class ShapeIDTests: XCTestCase { func test_init_createsShapeIDWithNamespace() throws { - let subject = ShapeID("smithy.test#TestName$TestMember") + let subject = try ShapeID("smithy.test#TestName$TestMember") XCTAssertEqual(subject.namespace, "smithy.test") } func test_init_createsShapeIDWithName() throws { - let subject = ShapeID("smithy.test#TestName$TestMember") + let subject = try ShapeID("smithy.test#TestName$TestMember") XCTAssertEqual(subject.name, "TestName") } func test_init_createsShapeIDWithMember() throws { - let subject = ShapeID("smithy.test#TestName$TestMember") + let subject = try ShapeID("smithy.test#TestName$TestMember") XCTAssertEqual(subject.member, "TestMember") } func test_init_createsShapeIDWithoutMember() throws { - let subject = ShapeID("smithy.test#TestName") + let subject = try ShapeID("smithy.test#TestName") XCTAssertNil(subject.member) } } From 3fd9854e9dd7cc746d1d1dd7f1469ad7a92ac18c Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Fri, 5 Dec 2025 13:51:48 -0600 Subject: [PATCH 19/22] Cleanup --- Package.swift | 2 +- Sources/Smithy/{Schema => }/ShapeType.swift | 0 Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift | 5 +++-- .../Schemas/SmithySchemaCodegen.swift | 10 +++++----- .../{Smithy => SmithySerialization}/Schema/Node.swift | 0 .../Schema/Prelude.swift | 0 .../Schema/Schema.swift | 2 ++ .../Schema/SchemaTraits.swift | 0 .../Schema/ShapeID.swift | 0 .../integration/HTTPBindingProtocolGenerator.kt | 2 +- 10 files changed, 12 insertions(+), 9 deletions(-) rename Sources/Smithy/{Schema => }/ShapeType.swift (100%) rename Sources/{Smithy => SmithySerialization}/Schema/Node.swift (100%) rename Sources/{Smithy => SmithySerialization}/Schema/Prelude.swift (100%) rename Sources/{Smithy => SmithySerialization}/Schema/Schema.swift (98%) rename Sources/{Smithy => SmithySerialization}/Schema/SchemaTraits.swift (100%) rename Sources/{Smithy => SmithySerialization}/Schema/ShapeID.swift (100%) diff --git a/Package.swift b/Package.swift index 8dc9f8cc1..a6ea3fb89 100644 --- a/Package.swift +++ b/Package.swift @@ -309,7 +309,7 @@ let package = Package( ), .target( name: "SmithyCodegenCore", - dependencies: ["Smithy"], + dependencies: ["SmithySerialization"], resources: [ .process("Resources") ] ), .testTarget( 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/Schemas/Shape+Schema.swift b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift index 8f11c82f6..16cbb532b 100644 --- a/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift +++ b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift @@ -6,6 +6,7 @@ // extension Shape { + func schemaVarName() throws -> String { if id.namespace == "smithy.api" { try id.preludeSchemaVarName() @@ -15,7 +16,7 @@ extension Shape { } } -extension ShapeID { +private extension ShapeID { func preludeSchemaVarName() throws -> String { let propertyName = @@ -41,7 +42,7 @@ extension ShapeID { case "Document": "documentSchema" default: throw CodegenError("Unhandled prelude type converted to schemaVar: \"\(name)\"") } - return "Smithy.Prelude.\(propertyName)" + return "SmithySerialization.Prelude.\(propertyName)" } func schemaVarName() throws -> String { diff --git a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift index 2a32b2f9f..d2aefa445 100644 --- a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Smithy +import SmithySerialization package struct SmithySchemaCodegen { @@ -13,8 +13,8 @@ package struct SmithySchemaCodegen { package func generate(model: Model) throws -> String { let writer = SwiftWriter() - writer.write("import class Smithy.Schema") - writer.write("import enum Smithy.Prelude") + writer.write("import class SmithySerialization.Schema") + writer.write("import enum SmithySerialization.Prelude") writer.write("") writer.write("// Model has \(model.shapes.count) shapes") @@ -41,7 +41,7 @@ package struct SmithySchemaCodegen { writer.write("// Number of schemas: \(sortedShapes.count)") writer.write("") for shape in sortedShapes { - try writer.openBlock("public var \(try shape.schemaVarName()): Smithy.Schema {", "}") { writer in + try writer.openBlock("public var \(try shape.schemaVarName()): SmithySerialization.Schema {", "}") { writer in try writeSchema(writer: writer, shape: shape) writer.unwrite(",") } @@ -54,7 +54,7 @@ package struct SmithySchemaCodegen { try writer.openBlock(".init(", "),") { writer in writer.write("id: \(shape.id.rendered),") writer.write("type: .\(shape.type),") - let relevantTraitIDs = shape.traits.keys.filter { Smithy.permittedTraitIDs.contains($0.id) } + let relevantTraitIDs = shape.traits.keys.filter { permittedTraitIDs.contains($0.id) } let traitIDs = Array(relevantTraitIDs).sorted() if !traitIDs.isEmpty { writer.openBlock("traits: [", "],") { writer in diff --git a/Sources/Smithy/Schema/Node.swift b/Sources/SmithySerialization/Schema/Node.swift similarity index 100% rename from Sources/Smithy/Schema/Node.swift rename to Sources/SmithySerialization/Schema/Node.swift diff --git a/Sources/Smithy/Schema/Prelude.swift b/Sources/SmithySerialization/Schema/Prelude.swift similarity index 100% rename from Sources/Smithy/Schema/Prelude.swift rename to Sources/SmithySerialization/Schema/Prelude.swift diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/SmithySerialization/Schema/Schema.swift similarity index 98% rename from Sources/Smithy/Schema/Schema.swift rename to Sources/SmithySerialization/Schema/Schema.swift index f8cac05fb..720e9800a 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/SmithySerialization/Schema/Schema.swift @@ -5,6 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.ShapeType + /// 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 diff --git a/Sources/Smithy/Schema/SchemaTraits.swift b/Sources/SmithySerialization/Schema/SchemaTraits.swift similarity index 100% rename from Sources/Smithy/Schema/SchemaTraits.swift rename to Sources/SmithySerialization/Schema/SchemaTraits.swift diff --git a/Sources/Smithy/Schema/ShapeID.swift b/Sources/SmithySerialization/Schema/ShapeID.swift similarity index 100% rename from Sources/Smithy/Schema/ShapeID.swift rename to Sources/SmithySerialization/Schema/ShapeID.swift 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 227d6663c..5c383e3cd 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 @@ -209,7 +209,7 @@ 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 - true + false // ctx.service.allTraits.keys // .any { it.name == "rpcv2Cbor" } From 6a6565520ea0f1042ab5ab35621035d7acce746d Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 9 Dec 2025 17:22:14 -0600 Subject: [PATCH 20/22] Fixes --- .../Config/DefaultSDKRuntimeConfiguration.swift | 2 +- .../Networking/Http/CRT/CRTClientEngine.swift | 2 +- .../Networking/Http/CRT/HTTP2Stream+ByteStream.swift | 2 +- .../Http/URLSession/FoundationStreamBridge.swift | 4 ++-- .../Networking/Http/URLSession/URLSessionHTTPClient.swift | 6 +++--- Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift | 6 +++--- .../amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt | 3 +++ .../smithy/swift/codegen/SmithyModelFileInfoGenerator.kt | 8 ++++---- 8 files changed, 18 insertions(+), 15 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/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. 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..ed05e4a00 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 @@ -99,6 +99,9 @@ class DirectedSwiftCodegen( integrations.forEach { it.writeAdditionalFiles(context, ctx, writers) } } + LOGGER.info("[${service.id}] Generating Smithy model file info") + SmithyModelFileInfoGenerator(context).writeSmithyModelFileInfo() + LOGGER.info("[${service.id}] Generating package manifest file") PackageManifestGenerator(context).writePackageManifest(writers.dependencies) 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..8f4a69d8a 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,14 +1,14 @@ 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.core.GenerationContext import software.amazon.smithy.swift.codegen.model.getTrait class SmithyModelFileInfoGenerator( - val ctx: ProtocolGenerator.GenerationContext, + val ctx: GenerationContext, ) { fun writeSmithyModelFileInfo() { - ctx.service.getTrait()?.let { serviceTrait -> + ctx.model.serviceShapes.firstOrNull()?.getTrait()?.let { serviceTrait -> val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" val modelFileName = serviceTrait @@ -17,7 +17,7 @@ class SmithyModelFileInfoGenerator( .replace(",", "") .replace(" ", "-") val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" - ctx.delegator.useFileWriter(filename) { writer -> + ctx.writerDelegator().useFileWriter(filename) { writer -> writer.write("{\"path\":\"$contents\"}") } } From 64eeff9d133eea955610d7d5f57d99edc838752a Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Fri, 12 Dec 2025 13:25:55 -0600 Subject: [PATCH 21/22] More codegen added --- .../SmithyCodeGeneratorPlugin.swift | 6 +- .../Schema => Smithy}/Node.swift | 0 .../Schema/Prelude.swift | 0 .../Schema/Schema.swift | 2 - .../Model => Smithy}/ShapeID.swift | 31 ++++- .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 12 +- Sources/SmithyCodegenCore/AST/ASTMember.swift | 1 + Sources/SmithyCodegenCore/AST/ASTModel.swift | 1 + Sources/SmithyCodegenCore/AST/ASTNode.swift | 2 +- .../SmithyCodegenCore/AST/ASTReference.swift | 1 + Sources/SmithyCodegenCore/AST/ASTShape.swift | 2 + Sources/SmithyCodegenCore/AST/ASTType.swift | 3 +- Sources/SmithyCodegenCore/CodeGenerator.swift | 24 +++- Sources/SmithyCodegenCore/Model/Model.swift | 47 ++++--- .../SmithyCodegenCore/Model/Node+AST.swift | 29 ++++ Sources/SmithyCodegenCore/Model/Node.swift | 63 --------- Sources/SmithyCodegenCore/Model/Shape.swift | 86 ------------ .../SmithyCodegenCore/Model/ShapeID+AST.swift | 19 +++ .../{ShapeType.swift => ShapeType+AST.swift} | 29 +--- .../Model/SymbolProvider.swift | 102 ++++++++++++++ Sources/SmithyCodegenCore/Node+Rendered.swift | 32 +++++ .../Schemas/Shape+Schema.swift | 23 ++-- .../Schemas/SmithySchemaCodegen.swift | 19 +-- .../Serialization/Shape+StructConsumer.swift | 57 ++++++++ .../StructConsumersCodegen.swift | 57 ++++++++ .../Shape/OperationShape.swift | 45 ++++++ .../Shape/Shape+Prelude.swift | 128 ++++++++++++++++++ Sources/SmithyCodegenCore/Shape/Shape.swift | 95 +++++++++++++ Sources/SmithyCodegenCore/SwiftWriter.swift | 4 +- Sources/SmithySerialization/Consumer.swift | 8 ++ .../SmithySerialization/MapSerializer.swift | 12 ++ .../SmithySerialization/Schema/ShapeID.swift | 42 ------ .../SerializableShape.swift | 10 ++ .../SerializableStruct.swift | 10 ++ .../SmithySerialization/ShapeSerializer.swift | 53 ++++++++ .../HTTPBindingProtocolGenerator.kt | 2 +- 36 files changed, 786 insertions(+), 271 deletions(-) rename Sources/{SmithySerialization/Schema => Smithy}/Node.swift (100%) rename Sources/{SmithySerialization => Smithy}/Schema/Prelude.swift (100%) rename Sources/{SmithySerialization => Smithy}/Schema/Schema.swift (98%) rename Sources/{SmithyCodegenCore/Model => Smithy}/ShapeID.swift (61%) create mode 100644 Sources/SmithyCodegenCore/Model/Node+AST.swift delete mode 100644 Sources/SmithyCodegenCore/Model/Node.swift delete mode 100644 Sources/SmithyCodegenCore/Model/Shape.swift create mode 100644 Sources/SmithyCodegenCore/Model/ShapeID+AST.swift rename Sources/SmithyCodegenCore/Model/{ShapeType.swift => ShapeType+AST.swift} (71%) create mode 100644 Sources/SmithyCodegenCore/Model/SymbolProvider.swift create mode 100644 Sources/SmithyCodegenCore/Node+Rendered.swift create mode 100644 Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift create mode 100644 Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.swift create mode 100644 Sources/SmithyCodegenCore/Shape/OperationShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift create mode 100644 Sources/SmithyCodegenCore/Shape/Shape.swift create mode 100644 Sources/SmithySerialization/Consumer.swift create mode 100644 Sources/SmithySerialization/MapSerializer.swift delete mode 100644 Sources/SmithySerialization/Schema/ShapeID.swift create mode 100644 Sources/SmithySerialization/SerializableShape.swift create mode 100644 Sources/SmithySerialization/SerializableStruct.swift create mode 100644 Sources/SmithySerialization/ShapeSerializer.swift diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 180e3bb5e..fe5c82810 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -52,16 +52,20 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Construct the schemas.swift path. let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") + // Construct the structconsumers.swift path. + let structConsumersSwiftPath = outputDirectoryPath.appending("\(name)StructConsumers.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, + "--struct-consumers-path", structConsumersSwiftPath, modelPath ], inputFiles: [inputPath, modelPath], - outputFiles: [schemasSwiftPath] + outputFiles: [schemasSwiftPath, structConsumersSwiftPath] ) } } diff --git a/Sources/SmithySerialization/Schema/Node.swift b/Sources/Smithy/Node.swift similarity index 100% rename from Sources/SmithySerialization/Schema/Node.swift rename to Sources/Smithy/Node.swift diff --git a/Sources/SmithySerialization/Schema/Prelude.swift b/Sources/Smithy/Schema/Prelude.swift similarity index 100% rename from Sources/SmithySerialization/Schema/Prelude.swift rename to Sources/Smithy/Schema/Prelude.swift diff --git a/Sources/SmithySerialization/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift similarity index 98% rename from Sources/SmithySerialization/Schema/Schema.swift rename to Sources/Smithy/Schema/Schema.swift index 720e9800a..f8cac05fb 100644 --- a/Sources/SmithySerialization/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -5,8 +5,6 @@ // SPDX-License-Identifier: Apache-2.0 // -import enum Smithy.ShapeType - /// 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 diff --git a/Sources/SmithyCodegenCore/Model/ShapeID.swift b/Sources/Smithy/ShapeID.swift similarity index 61% rename from Sources/SmithyCodegenCore/Model/ShapeID.swift rename to Sources/Smithy/ShapeID.swift index 9792290bc..a429b0fed 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeID.swift +++ b/Sources/Smithy/ShapeID.swift @@ -11,18 +11,33 @@ /// 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 { +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 ModelError("id \"\(id)\" does not have a #") + throw ShapeIDError("id \"\(id)\" does not have a #") } guard let namespace = splitOnPound.first, !namespace.isEmpty else { - throw ModelError("id \"\(id)\" does not have a nonempty namespace") + throw ShapeIDError("id \"\(id)\" does not have a nonempty namespace") } self.namespace = String(namespace) let splitOnDollar = splitOnPound.last!.split(separator: "$") @@ -34,7 +49,7 @@ public struct ShapeID: Hashable { self.name = String(splitOnDollar.first!) self.member = nil default: - throw ModelError("id \"\(id)\" has more than one $") + throw ShapeIDError("id \"\(id)\" has more than one $") } } @@ -65,3 +80,11 @@ 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/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index d4e7c320f..0e3e80bb7 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -18,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)") @@ -32,8 +35,15 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) + // If --struct-consumers-path was supplied, create the struct consumers file URL + let structConsumersFileURL = resolve(paramName: "--struct-consumers-path", path: structConsumersPath) + // Use resolved file URLs to run code generator - try CodeGenerator(modelFileURL: modelFileURL, schemasFileURL: schemasFileURL).run() + try CodeGenerator( + modelFileURL: modelFileURL, + schemasFileURL: schemasFileURL, + structConsumersFileURL: structConsumersFileURL + ).run() } private func currentWorkingDirectoryFileURL() -> URL { diff --git a/Sources/SmithyCodegenCore/AST/ASTMember.swift b/Sources/SmithyCodegenCore/AST/ASTMember.swift index f03fefef9..5d3a30c35 100644 --- a/Sources/SmithyCodegenCore/AST/ASTMember.swift +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -5,6 +5,7 @@ // 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 index 5ff69fb15..07de8bf89 100644 --- a/Sources/SmithyCodegenCore/AST/ASTModel.swift +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -5,6 +5,7 @@ // 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? diff --git a/Sources/SmithyCodegenCore/AST/ASTNode.swift b/Sources/SmithyCodegenCore/AST/ASTNode.swift index 8d5a61c44..a3d6fd58b 100644 --- a/Sources/SmithyCodegenCore/AST/ASTNode.swift +++ b/Sources/SmithyCodegenCore/AST/ASTNode.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 -enum ASTNode: Sendable { +enum ASTNode { case object([String: ASTNode]) case list([ASTNode]) case string(String) diff --git a/Sources/SmithyCodegenCore/AST/ASTReference.swift b/Sources/SmithyCodegenCore/AST/ASTReference.swift index 6132359df..4cdce691a 100644 --- a/Sources/SmithyCodegenCore/AST/ASTReference.swift +++ b/Sources/SmithyCodegenCore/AST/ASTReference.swift @@ -5,6 +5,7 @@ // 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 index 92cb16bd7..abeb8d425 100644 --- a/Sources/SmithyCodegenCore/AST/ASTShape.swift +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -5,6 +5,8 @@ // 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]? diff --git a/Sources/SmithyCodegenCore/AST/ASTType.swift b/Sources/SmithyCodegenCore/AST/ASTType.swift index cdf3758bf..adf1e6b52 100644 --- a/Sources/SmithyCodegenCore/AST/ASTType.swift +++ b/Sources/SmithyCodegenCore/AST/ASTType.swift @@ -5,8 +5,9 @@ // 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 Smithy shape types + // These cases are all the standard Smithy shape types case blob case boolean case string diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 3c735faa0..1e53c6d82 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -6,25 +6,43 @@ // import struct Foundation.Data +import class Foundation.JSONDecoder import struct Foundation.URL + public struct CodeGenerator { let modelFileURL: URL let schemasFileURL: URL? + let structConsumersFileURL: URL? - public init(modelFileURL: URL, schemasFileURL: URL?) { + public init( + modelFileURL: URL, + schemasFileURL: URL?, + structConsumersFileURL: URL? + ) { self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL + self.structConsumersFileURL = structConsumersFileURL } public func run() throws { - // Load the model from the model file - let model = try Model(modelFileURL: modelFileURL) + // Load the AST from the model file + let modelData = try Data(contentsOf: modelFileURL) + let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) + + // Create the model from the AST + let model = try Model(astModel: astModel) // If a schema file URL was provided, generate it if let schemasFileURL { let schemaContents = try SmithySchemaCodegen().generate(model: model) try Data(schemaContents.utf8).write(to: schemasFileURL) } + + // If a struct consumers file URL was provided, generate it + if let structConsumersFileURL { + let structConsumersContents = try StructConsumersCodegen().generate(model: model) + try Data(structConsumersContents.utf8).write(to: structConsumersFileURL) + } } } diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift index 6a081b9c9..486e0fc5b 100644 --- a/Sources/SmithyCodegenCore/Model/Model.swift +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -5,24 +5,17 @@ // SPDX-License-Identifier: Apache-2.0 // -import struct Foundation.Data -import class Foundation.JSONDecoder -import struct Foundation.URL +import enum Smithy.Node +import struct Smithy.ShapeID public class Model { public let version: String public let metadata: Node? public let shapes: [ShapeID: Shape] - public convenience init(modelFileURL: URL) throws { - let modelData = try Data(contentsOf: modelFileURL) - let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) - try self.init(astModel: astModel) - } - init(astModel: ASTModel) throws { self.version = astModel.smithy - self.metadata = astModel.metadata?.modelNode + self.metadata = astModel.metadata?.node let idToShapePairs = try astModel.shapes.map { try Self.shapePair(id: $0.key, astShape: $0.value) } let idToMemberShapePairs = try astModel.shapes.flatMap { astShape in try Self.memberShapePairs(id: astShape.key, astShape: astShape.value) @@ -44,18 +37,34 @@ public class Model { } shape.memberIDs = filteredMemberIDs.sorted() } + + let services = self.shapes.values.filter { $0.type == .service } + guard services.count == 1 else { fatalError("Model has \(services.count) services") } } private static func shapePair(id: String, astShape: ASTShape) throws -> (ShapeID, Shape) { let shapeID = try ShapeID(id) - let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value.modelNode) } ?? [] - let shape = Shape( - id: shapeID, - type: astShape.type.modelType, - traits: Dictionary(uniqueKeysWithValues: idToTraitPairs), - targetID: nil - ) - return (shapeID, shape) + let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value.node) } ?? [] + switch astShape.type { + case .operation: + let shape = OperationShape( + id: shapeID, + type: astShape.type.modelType, + traits: Dictionary(uniqueKeysWithValues: idToTraitPairs), + targetID: nil, + input: astShape.input?.id, + output: astShape.output?.id + ) + return (shapeID, shape) + default: + let shape = Shape( + id: shapeID, + type: astShape.type.modelType, + traits: Dictionary(uniqueKeysWithValues: idToTraitPairs), + targetID: nil + ) + return (shapeID, shape) + } } private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, Shape)] { @@ -71,7 +80,7 @@ public class Model { } return try baseMembers.map { astMember in let memberID = ShapeID(id: try ShapeID(id), member: astMember.key) - let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value.modelNode) } + let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value.node) } let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? []) let targetID = try ShapeID(astMember.value.target) return (memberID, Shape(id: memberID, type: .member, traits: traits, targetID: targetID)) diff --git a/Sources/SmithyCodegenCore/Model/Node+AST.swift b/Sources/SmithyCodegenCore/Model/Node+AST.swift new file mode 100644 index 000000000..c042c5e18 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Node+AST.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node + +extension ASTNode { + + /// Creates a Smithy Node from a AST-specific ASTNode. + var node: Smithy.Node { + switch self { + case .object(let object): + return .object(object.mapValues { $0.node }) + case .list(let list): + return .list(list.map { $0.node }) + case .string(let value): + return .string(value) + case .number(let value): + return .number(value) + case .boolean(let value): + return .boolean(value) + case .null: + return .null + } + } +} diff --git a/Sources/SmithyCodegenCore/Model/Node.swift b/Sources/SmithyCodegenCore/Model/Node.swift deleted file mode 100644 index c56c50736..000000000 --- a/Sources/SmithyCodegenCore/Model/Node.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -/// Contains the value of a Smithy Node. -/// -/// Smithy node data is basically the same as the data that can be stored in JSON. -/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. -/// -/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values -public enum Node: Sendable { - case object([String: Node]) - case list([Node]) - case string(String) - case number(Double) - case boolean(Bool) - case null -} - -extension ASTNode { - - /// Creates a model Node from a AST-specific ASTNode. - var modelNode: Node { - switch self { - case .object(let object): - return .object(object.mapValues { $0.modelNode }) - case .list(let list): - return .list(list.map { $0.modelNode }) - case .string(let value): - return .string(value) - case .number(let value): - return .number(value) - case .boolean(let value): - return .boolean(value) - case .null: - return .null - } - } -} - -extension Node { - - 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/Model/Shape.swift b/Sources/SmithyCodegenCore/Model/Shape.swift deleted file mode 100644 index 73b36dcc7..000000000 --- a/Sources/SmithyCodegenCore/Model/Shape.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -public class Shape { - public let id: ShapeID - public let type: ShapeType - public internal(set) var traits: [ShapeID: Node] - var targetID: ShapeID? - var memberIDs: [ShapeID] = [] - weak var model: Model? - - public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node], targetID: ShapeID?) { - self.id = id - self.type = type - self.traits = traits - self.targetID = targetID - } - - public func hasTrait(_ traitID: ShapeID) -> Bool { - traits[traitID] != nil - } - - public var members: [Shape] { - get throws { - guard let model else { throw ModelError("id \"\(id)\" model not set") } - return memberIDs.map { model.shapes[$0]! } - } - } - - public var target: Shape? { - get throws { - guard let targetID else { return nil } - guard let model else { throw ModelError("id \"\(id)\" model not set") } - return model.shapes[targetID] - } - } - - public var children: Set { - get throws { - var c = Set() - try children(children: &c) - return c - } - } - - private func children(children: inout Set) throws { - let shapes = try candidates(for: self) - for shape in shapes { - if children.contains(shape) { continue } - children.insert(shape) - try shape.members.map { try $0.target }.compactMap { $0 }.forEach { - children.insert($0) - try $0.children(children: &children) - } - } - } - - private func candidates(for shape: Shape) throws -> [Shape] { - (try [try shape.target] + shape.members.map { try $0.target }).compactMap { $0 } - } -} - -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/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift new file mode 100644 index 000000000..652536de9 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -0,0 +1,19 @@ +// +// 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 { + do { + return try ShapeID(target) + } catch { + fatalError("Creation of ShapeID from ASTReference failed: \(error.localizedDescription)") + } + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeType.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift similarity index 71% rename from Sources/SmithyCodegenCore/Model/ShapeType.swift rename to Sources/SmithyCodegenCore/Model/ShapeType+AST.swift index 7d0d47dec..a5d721863 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeType.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -5,35 +5,10 @@ // 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). -public enum ShapeType: String { - 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 -} +import enum Smithy.ShapeType extension ASTType { + var modelType: ShapeType { switch self { case .blob: diff --git a/Sources/SmithyCodegenCore/Model/SymbolProvider.swift b/Sources/SmithyCodegenCore/Model/SymbolProvider.swift new file mode 100644 index 000000000..aa506e16e --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/SymbolProvider.swift @@ -0,0 +1,102 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import struct Smithy.ShapeID + +public struct SymbolProvider { + let model: Model + + init(model: Model) { + self.model = model + } + + func serviceName(service: Shape) -> String { + guard service.type == .service else { fatalError("Called serviceName on non-service shape") } + guard case .object(let serviceInfo) = service.getTrait(.init("aws.api", "service")) else { fatalError("No service trait on service") } + guard case .string(let sdkID) = serviceInfo["sdkId"] else { fatalError("No sdkId on service trait") } + return sdkID.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "Service", with: "") + } + + func swiftType(shape: Shape) -> String { + if case .string(let name) = shape.getTrait(.init("swift.synthetic", "operationName")), shape.hasTrait(.init("smithy.api", "input")) { + return name + "Input" + } else if shape.hasTrait(.init("smithy.api", "input")) { + guard let operation = model.shapes.values + .filter({ $0.type == .operation }) + .map({ $0 as! OperationShape }) + .first(where: { $0.inputShapeID == shape.id }) else { fatalError("Operation for input \(shape.id) not found") } + return operation.id.name + "Input" + } else if case .string(let name) = shape.getTrait(.init("swift.synthetic", "operationName")), shape.hasTrait(.init("smithy.api", "output")){ + return name + "Output" + } else if shape.hasTrait(.init("smithy.api", "output")) { + guard let operation = model.shapes.values + .filter({ $0.type == .operation }) + .map({ $0 as! OperationShape }) + .first(where: { $0.outputShapeID == shape.id }) else { fatalError("Operation for output \(shape.id) not found") } + return operation.id.name + "Output" + } else if shape.hasTrait(.init("smithy.api", "error")) { + return shape.id.name + } else { + guard let service = model.shapes.values.first(where: { $0.type == .service }) else { fatalError("service not found") } + return serviceName(service: service) + "ClientTypes." + shape.id.name + } + } + + func methodName(shapeID: ShapeID) -> String { + guard let member = shapeID.member else { fatalError("Shape ID has no member name") } + return member.toLowerCamelCase() + } +} + +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: NSRange(location: 0, length: result.count), withTemplate: " ") + + // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" + let underscoreRegex = try! NSRegularExpression(pattern: "_") + result = underscoreRegex.stringByReplacingMatches(in: result, range: NSRange(location: 0, length: result.count), 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: NSRange(location: 0, length: result.count), withTemplate: "$1 v$2") + + let largeVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})V([0-9]+)") + result = largeVRegex.stringByReplacingMatches(in: result, range: NSRange(location: 0, length: result.count), 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: NSRange(location: 0, length: result.count), withTemplate: " ") + + // add a space after acronyms + let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") + result = acronymSplitRegex.stringByReplacingMatches(in: result, range: NSRange(location: 0, length: result.count), 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: NSRange(location: 0, length: result.count), 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: NSRange(location: 0, length: result.count), withTemplate: " ").trimmingCharacters(in: .whitespaces) + + return result.components(separatedBy: " ") + } +} diff --git a/Sources/SmithyCodegenCore/Node+Rendered.swift b/Sources/SmithyCodegenCore/Node+Rendered.swift new file mode 100644 index 000000000..78b17977a --- /dev/null +++ b/Sources/SmithyCodegenCore/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/Schemas/Shape+Schema.swift b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift index 16cbb532b..64944581a 100644 --- a/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift +++ b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift @@ -5,22 +5,23 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Smithy.ShapeID + extension Shape { - func schemaVarName() throws -> String { + var schemaVarName: String { if id.namespace == "smithy.api" { - try id.preludeSchemaVarName() + id.preludeSchemaVarName } else { - try id.schemaVarName() + id.schemaVarName } } } private extension ShapeID { - func preludeSchemaVarName() throws -> String { - let propertyName = - switch name { + var preludeSchemaVarName: String { + let propertyName = switch name { case "Unit": "unitSchema" case "String": "stringSchema" case "Blob": "blobSchema" @@ -40,15 +41,15 @@ private extension ShapeID { case "PrimitiveShort": "primitiveShortSchema" case "PrimitiveByte": "primitiveByteSchema" case "Document": "documentSchema" - default: throw CodegenError("Unhandled prelude type converted to schemaVar: \"\(name)\"") + default: fatalError("Unhandled prelude type converted to schemaVar: \"\(name)\"") } - return "SmithySerialization.Prelude.\(propertyName)" + return "Smithy.Prelude.\(propertyName)" } - func schemaVarName() throws -> String { - guard member == nil else { throw CodegenError("Assigning member schema to a var") } + var schemaVarName: String { + guard member == nil else { fatalError("Assigning member schema to a var") } let namespacePortion = namespace.replacingOccurrences(of: ".", with: "_") let namePortion = name - return "schema2__\(namespacePortion)__\(namePortion)" + return "schema__\(namespacePortion)__\(namePortion)" } } diff --git a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift index d2aefa445..99e8d6f80 100644 --- a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift @@ -5,7 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // -import SmithySerialization +import struct Smithy.ShapeID +import let SmithySerialization.permittedTraitIDs package struct SmithySchemaCodegen { @@ -13,8 +14,8 @@ package struct SmithySchemaCodegen { package func generate(model: Model) throws -> String { let writer = SwiftWriter() - writer.write("import class SmithySerialization.Schema") - writer.write("import enum SmithySerialization.Prelude") + writer.write("import class Smithy.Schema") + writer.write("import enum Smithy.Prelude") writer.write("") writer.write("// Model has \(model.shapes.count) shapes") @@ -35,18 +36,20 @@ package struct SmithySchemaCodegen { try $0.hasTrait(try .init("smithy.api#input")) || $0.hasTrait(try .init("smithy.api#output")) || $0.hasTrait(try .init("smithy.api#error"))} - .map { try [$0] + $0.children } + .map { [$0] + $0.descendants } .flatMap { $0 } + .filter { $0.id.namespace != "smithy.api" } let sortedShapes = Array(Set(shapes)).sorted { $0.id.id.lowercased() < $1.id.id.lowercased() } writer.write("// Number of schemas: \(sortedShapes.count)") writer.write("") for shape in sortedShapes { - try writer.openBlock("public var \(try shape.schemaVarName()): SmithySerialization.Schema {", "}") { writer in + try writer.openBlock("public var \(shape.schemaVarName): Smithy.Schema {", "}") { writer in try writeSchema(writer: writer, shape: shape) writer.unwrite(",") } writer.write("") } + writer.unwrite("\n") return writer.finalize() } @@ -64,7 +67,7 @@ package struct SmithySchemaCodegen { } } } - let members = try shape.members + let members = shape.members if !members.isEmpty { try writer.openBlock("members: [", "],") { writer in for (index, member) in members.enumerated() { @@ -72,8 +75,8 @@ package struct SmithySchemaCodegen { } } } - if let target = try shape.target { - writer.write("target: \(try target.schemaVarName()),") + if let target = shape.target { + writer.write("target: \(target.schemaVarName),") } if let index { writer.write("index: \(index),") diff --git a/Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift b/Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift new file mode 100644 index 000000000..02b55b95b --- /dev/null +++ b/Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift @@ -0,0 +1,57 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Shape { + + var structConsumerMethod: String { + switch type { + case .blob: + return "writeBlob" + case .boolean: + return "writeBoolean" + case .string, .enum: + return "writeString" + case .timestamp: + return "writeTimestamp" + case .byte: + return "writeByte" + case .short: + return "writeShort" + case .integer, .intEnum: + return "writeInteger" + 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: + return "writeStruct" + case .member, .service, .resource, .operation: + fatalError("Cannot serialize type \(type)") + } + } + + var structConsumerVarName: String { + guard id.member == nil else { fatalError("Constructing struct consumer for a member") } + let namespacePortion = id.namespace.replacingOccurrences(of: ".", with: "_") + let namePortion = id.name + return "structconsumer__\(namespacePortion)__\(namePortion)" + } +} diff --git a/Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.swift b/Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.swift new file mode 100644 index 000000000..586cff3cb --- /dev/null +++ b/Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.swift @@ -0,0 +1,57 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +package struct StructConsumersCodegen { + + package init() {} + + package func generate(model: Model) throws -> String { + let writer = SwiftWriter() + writer.write("import enum Smithy.Prelude") + writer.write("import protocol SmithySerialization.SerializableStruct") + writer.write("import protocol SmithySerialization.ShapeSerializer") + writer.write("") + + for shape in model.shapes.values where shape.type == .structure || shape.type == .union { + let symbolProvider = SymbolProvider(model: model) + let swiftType = symbolProvider.swiftType(shape: shape) + let varName = shape.type == .structure ? "structure" : "union" + writer.openBlock("let \(shape.structConsumerVarName) = { (\(varName): \(swiftType), serializer: any ShapeSerializer) in", "}") { writer in + for member in shape.members { + guard let target = member.target else { fatalError("Member \(member.id) does not have target") } + let memberName = symbolProvider.methodName(shapeID: member.id) + if shape.type == .structure { + let properties = shape.hasTrait(.init("smithy.api", "error")) ? "properties." : "" + writer.openBlock("if let value = \(varName).\(properties)\(memberName) {", "}") { writer in + writeSerializeCall(writer: writer, target: target) + } + } else { // shape is a union + writer.openBlock("if case .\(memberName)(let value) = \(varName) {", "}") { writer in + writeSerializeCall(writer: writer, target: target) + } + } + } + } + writer.write("") + } + writer.unwrite("\n") + return writer.finalize() + } + + private func writeSerializeCall(writer: SwiftWriter, target: Shape) { + switch target.type { + case .structure, .union: + writer.write("// serialize struct or union here") + case .list: + writer.write("// serialize list here") + case .map: + writer.write("// serialize map here") + default: + writer.write("serializer.\(target.structConsumerMethod)(schema: \(target.schemaVarName), value: value)") + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/OperationShape.swift b/Sources/SmithyCodegenCore/Shape/OperationShape.swift new file mode 100644 index 000000000..7d3a36aab --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/OperationShape.swift @@ -0,0 +1,45 @@ +// +// 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 + +class OperationShape: Shape { + let inputShapeID: ShapeID? + let outputShapeID: ShapeID? + + public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node], targetID: ShapeID?, input: ShapeID?, output: ShapeID?) { + self.inputShapeID = input + self.outputShapeID = output + super.init(id: id, type: type, traits: traits, targetID: targetID) + } + + 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/Shape+Prelude.swift b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift new file mode 100644 index 000000000..8129bbed7 --- /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, targetID: nil) + } + + static var boolean: Shape { + let schema = Smithy.Prelude.booleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var string: Shape { + let schema = Smithy.Prelude.stringSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var integer: Shape { + let schema = Smithy.Prelude.integerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var blob: Shape { + let schema = Smithy.Prelude.blobSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var timestamp: Shape { + let schema = Smithy.Prelude.timestampSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var byte: Shape { + let schema = Smithy.Prelude.byteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var short: Shape { + let schema = Smithy.Prelude.shortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var long: Shape { + let schema = Smithy.Prelude.longSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var float: Shape { + let schema = Smithy.Prelude.floatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var double: Shape { + let schema = Smithy.Prelude.doubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var document: Shape { + let schema = Smithy.Prelude.documentSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var primitiveBoolean: Shape { + let schema = Smithy.Prelude.primitiveBooleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var primitiveInteger: Shape { + let schema = Smithy.Prelude.primitiveIntegerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var primitiveByte: Shape { + let schema = Smithy.Prelude.primitiveByteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var primitiveShort: Shape { + let schema = Smithy.Prelude.primitiveShortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var primitiveLong: Shape { + let schema = Smithy.Prelude.primitiveLongSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var primitiveFloat: Shape { + let schema = Smithy.Prelude.primitiveFloatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } + + static var primitiveDouble: Shape { + let schema = Smithy.Prelude.primitiveDoubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/Shape.swift b/Sources/SmithyCodegenCore/Shape/Shape.swift new file mode 100644 index 000000000..bd874acf0 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -0,0 +1,95 @@ +// +// 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 internal(set) var traits: [ShapeID: Node] + var targetID: ShapeID? + var memberIDs: [ShapeID] = [] + weak var model: Model? + + public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node], targetID: ShapeID?) { + self.id = id + self.type = type + self.traits = traits + self.targetID = targetID + } + + public func hasTrait(_ traitID: ShapeID) -> Bool { + traits[traitID] != nil + } + + public func getTrait(_ traitID: ShapeID) -> Node? { + traits[traitID] + } + + public var members: [Shape] { + guard let model else { return [] } + return memberIDs.map { model.shapes[$0]! } + } + + public var target: Shape? { + guard let targetID else { return nil } + return model?.shapes[targetID] ?? Shape.prelude[targetID] + } + + 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, targetID: targetID) + new.memberIDs = memberIDs + 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.members.map { $0.target }.compactMap { $0 }.forEach { + descendants.insert($0) + $0.descendants(&descendants) + } + } + } + + private func candidates(for shape: Shape) -> [Shape] { + ([shape.target] + shape.members.map { $0.target }).compactMap { $0 } + } +} + +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/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftWriter.swift index a6b30a202..8907ee708 100644 --- a/Sources/SmithyCodegenCore/SwiftWriter.swift +++ b/Sources/SmithyCodegenCore/SwiftWriter.swift @@ -40,7 +40,9 @@ class SwiftWriter { func unwrite(_ text: String) { guard let lastIndex = lines.indices.last else { return } - if lines[lastIndex].hasSuffix(text) { + if text == "\n" && lines[lastIndex] == "" { + _ = lines.removeLast() + } else if lines[lastIndex].hasSuffix(text) { lines[lastIndex].removeLast(text.count) } } diff --git a/Sources/SmithySerialization/Consumer.swift b/Sources/SmithySerialization/Consumer.swift new file mode 100644 index 000000000..1296821af --- /dev/null +++ b/Sources/SmithySerialization/Consumer.swift @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias Consumer = (Serializer) throws -> Void diff --git a/Sources/SmithySerialization/MapSerializer.swift b/Sources/SmithySerialization/MapSerializer.swift new file mode 100644 index 000000000..2832e873f --- /dev/null +++ b/Sources/SmithySerialization/MapSerializer.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class Smithy.Schema + +public protocol MapSerializer { + func writeEntry(keySchema: Schema, key: String, valueSerializer: Consumer) +} diff --git a/Sources/SmithySerialization/Schema/ShapeID.swift b/Sources/SmithySerialization/Schema/ShapeID.swift deleted file mode 100644 index 647bbfeb8..000000000 --- a/Sources/SmithySerialization/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/SmithySerialization/SerializableShape.swift b/Sources/SmithySerialization/SerializableShape.swift new file mode 100644 index 000000000..491fdb7fd --- /dev/null +++ b/Sources/SmithySerialization/SerializableShape.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol SerializableShape { + func serialize(serializer: ShapeSerializer) throws +} diff --git a/Sources/SmithySerialization/SerializableStruct.swift b/Sources/SmithySerialization/SerializableStruct.swift new file mode 100644 index 000000000..5109b04b3 --- /dev/null +++ b/Sources/SmithySerialization/SerializableStruct.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol SerializableStruct: SerializableShape { + func serializeMembers(encoder: ShapeSerializer) throws +} diff --git a/Sources/SmithySerialization/ShapeSerializer.swift b/Sources/SmithySerialization/ShapeSerializer.swift new file mode 100644 index 000000000..5a215e29b --- /dev/null +++ b/Sources/SmithySerialization/ShapeSerializer.swift @@ -0,0 +1,53 @@ +// +// 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 class Smithy.Schema + +public protocol ShapeSerializer { + func writeStruct(schema: Schema, struct: SerializableStruct) + func writeList(schema: Schema, size: Int, consumer: Consumer) + func writeMap(schema: Schema, size: Int, consumer: Consumer) + func writeBoolean(schema: Schema, value: Bool) + func writeByte(schema: Schema, value: Int8) + func writeShort(schema: Schema, value: Int16) + func writeInteger(schema: Schema, value: Int) + func writeLong(schema: Schema, value: Int64) + func writeFloat(schema: Schema, value: Float) + func writeDouble(schema: Schema, value: Double) + func writeBigInteger(schema: Schema, value: Int64) + func writeBigDecimal(schema: Schema, value: Double) + func writeString(schema: Schema, value: String) + func writeBlob(schema: Schema, value: Data) + func writeTimestamp(schema: Schema, value: Date) + func writeDocument(schema: Schema, value: Document) + func writeNull(schema: Schema) + func writeDataStream(schema: Schema, value: ByteStream) + func writeEventStream(schema: Schema, value: AsyncThrowingStream) +} + +public extension ShapeSerializer { + + func writeString(schema: Schema, value: T) where T.RawValue == String { + writeString(schema: schema, value: value.rawValue) + } + + func writeInteger(schema: Schema, value: T) where T.RawValue == Int { + writeInteger(schema: schema, value: value.rawValue) + } + + func writeDataStream(schema: Schema, value: ByteStream) { + // by default, do nothing + } + + func writeEventStream(schema: Schema, value: AsyncThrowingStream) { + // by default, do nothing + } +} 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 5c383e3cd..227d6663c 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 @@ -209,7 +209,7 @@ 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 - false + true // ctx.service.allTraits.keys // .any { it.name == "rpcv2Cbor" } From 260e5ca5f15cb8f042b650542b664cea93c5fdd2 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 17 Dec 2025 09:50:49 -0600 Subject: [PATCH 22/22] Serializes to string --- .../SmithyCodeGeneratorPlugin.swift | 6 +- Sources/Smithy/Schema/Schema.swift | 24 ++- .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 10 +- 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 | 23 +-- .../SmithyCodegenCore/GenerationContext.swift | 27 +++ .../SmithyCodegenCore/Model/Model+AST.swift | 162 ++++++++++++++++++ Sources/SmithyCodegenCore/Model/Model.swift | 76 +------- .../SmithyCodegenCore/Model/Node+AST.swift | 29 ---- .../SmithyCodegenCore/Model/ShapeID+AST.swift | 4 +- .../Model/ShapeType+AST.swift | 104 +++++------ .../ASTShape+isDeprecated.swift | 41 +++++ .../Schemas/Shape+Schema.swift | 68 ++++---- .../Schemas/SmithySchemaCodegen.swift | 21 +-- .../SerializableStructsCodegen.swift | 68 ++++++++ .../Shape+StructConsumer.swift | 52 ++++++ .../Serialization/Shape+StructConsumer.swift | 57 ------ .../StructConsumersCodegen.swift | 57 ------ .../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 | 9 +- .../Shape/ServiceShape.swift | 23 +++ .../Shape/Shape+Prelude.swift | 38 ++-- Sources/SmithyCodegenCore/Shape/Shape.swift | 31 +--- .../Shape/StructureShape.swift | 28 +++ .../SmithyCodegenCore/Shape/UnionShape.swift | 28 +++ .../{ => SwiftRendering}/Node+Rendered.swift | 0 .../{ => SwiftRendering}/String+Utils.swift | 0 .../{ => SwiftRendering}/SwiftWriter.swift | 0 .../SymbolProvider.swift | 89 ++++++---- .../SymbolProviderError.swift} | 2 +- .../Schema/SchemaTraits.swift | 1 + .../SerializableShape.swift | 5 +- .../SerializableStruct.swift | 9 +- .../SmithySerialization/ShapeSerializer.swift | 14 +- .../StringSerializer.swift | 122 +++++++++++++ .../SmithyCodegenCoreTests/ShapeIDTests.swift | 32 ---- .../SmithyCodegenCoreTests.swift | 11 ++ Tests/SmithyTests/ShapeIDTests.swift | 22 ++- .../HTTPBindingProtocolGenerator.kt | 17 +- 47 files changed, 992 insertions(+), 511 deletions(-) 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 delete mode 100644 Sources/SmithyCodegenCore/Model/Node+AST.swift create mode 100644 Sources/SmithyCodegenCore/ModelTransformer/ASTShape+isDeprecated.swift create mode 100644 Sources/SmithyCodegenCore/SerializableStructs/SerializableStructsCodegen.swift create mode 100644 Sources/SmithyCodegenCore/SerializableStructs/Shape+StructConsumer.swift delete mode 100644 Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift delete mode 100644 Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.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/ServiceShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/StructureShape.swift create mode 100644 Sources/SmithyCodegenCore/Shape/UnionShape.swift rename Sources/SmithyCodegenCore/{ => SwiftRendering}/Node+Rendered.swift (100%) rename Sources/SmithyCodegenCore/{ => SwiftRendering}/String+Utils.swift (100%) rename Sources/SmithyCodegenCore/{ => SwiftRendering}/SwiftWriter.swift (100%) rename Sources/SmithyCodegenCore/{Model => SymbolProvider}/SymbolProvider.swift (50%) rename Sources/SmithyCodegenCore/{CodegenError.swift => SymbolProvider/SymbolProviderError.swift} (86%) create mode 100644 Sources/SmithySerialization/StringSerializer.swift delete mode 100644 Tests/SmithyCodegenCoreTests/ShapeIDTests.swift create mode 100644 Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index fe5c82810..560ce9ced 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -53,7 +53,7 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") // Construct the structconsumers.swift path. - let structConsumersSwiftPath = outputDirectoryPath.appending("\(name)StructConsumers.swift") + let serializableStructsSwiftPath = outputDirectoryPath.appending("\(name)SerializableStructs.swift") // Construct the build command that invokes SmithyCodegenCLI. return .buildCommand( @@ -61,11 +61,11 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { executable: generatorToolPath, arguments: [ "--schemas-path", schemasSwiftPath, - "--struct-consumers-path", structConsumersSwiftPath, + "--serializable-structs-path", serializableStructsSwiftPath, modelPath ], inputFiles: [inputPath, modelPath], - outputFiles: [schemasSwiftPath, structConsumersSwiftPath] + outputFiles: [schemasSwiftPath, serializableStructsSwiftPath] ) } } 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/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index 0e3e80bb7..6c75a3f96 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -18,8 +18,8 @@ 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? + @Option(help: "The full or relative path to write the SerializableStructs output file.") + var serializableStructsPath: String? func run() async throws { let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() @@ -35,14 +35,14 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) - // If --struct-consumers-path was supplied, create the struct consumers file URL - let structConsumersFileURL = resolve(paramName: "--struct-consumers-path", path: structConsumersPath) + // If --serializable-structs-path was supplied, create the serializable structs file URL + let serializableStructsFileURL = resolve(paramName: "--serializable-structs-path", path: serializableStructsPath) // Use resolved file URLs to run code generator try CodeGenerator( modelFileURL: modelFileURL, schemasFileURL: schemasFileURL, - structConsumersFileURL: structConsumersFileURL + serializableStructsFileURL: serializableStructsFileURL ).run() } 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 1e53c6d82..ed857e16f 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -13,16 +13,16 @@ import struct Foundation.URL public struct CodeGenerator { let modelFileURL: URL let schemasFileURL: URL? - let structConsumersFileURL: URL? + let serializableStructsFileURL: URL? public init( modelFileURL: URL, schemasFileURL: URL?, - structConsumersFileURL: URL? + serializableStructsFileURL: URL? ) { self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL - self.structConsumersFileURL = structConsumersFileURL + self.serializableStructsFileURL = serializableStructsFileURL } public func run() throws { @@ -33,16 +33,19 @@ public struct CodeGenerator { // Create the model from the AST let model = try Model(astModel: astModel) - // If a schema file URL was provided, generate it + // Create a generation context from the model + let ctx = try GenerationContext(model: model) + + // If a schemas file URL was provided, generate it if let schemasFileURL { - let schemaContents = try SmithySchemaCodegen().generate(model: model) - try Data(schemaContents.utf8).write(to: schemasFileURL) + let schemasContents = try SmithySchemaCodegen().generate(ctx: ctx) + try Data(schemasContents.utf8).write(to: schemasFileURL) } - // If a struct consumers file URL was provided, generate it - if let structConsumersFileURL { - let structConsumersContents = try StructConsumersCodegen().generate(model: model) - try Data(structConsumersContents.utf8).write(to: structConsumersFileURL) + // If a serializable structs file URL was provided, generate it + if let serializableStructsFileURL { + let serializableStructsContents = try SerializableStructsCodegen().generate(ctx: ctx) + try Data(serializableStructsContents.utf8).write(to: serializableStructsFileURL) } } } 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 index 486e0fc5b..2ee603c47 100644 --- a/Sources/SmithyCodegenCore/Model/Model.swift +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -13,78 +13,10 @@ public class Model { public let metadata: Node? public let shapes: [ShapeID: Shape] - init(astModel: ASTModel) throws { - self.version = astModel.smithy - self.metadata = astModel.metadata?.node - let idToShapePairs = try astModel.shapes.map { try Self.shapePair(id: $0.key, astShape: $0.value) } - let idToMemberShapePairs = try astModel.shapes.flatMap { astShape in - try Self.memberShapePairs(id: astShape.key, astShape: astShape.value) - } - self.shapes = Dictionary(uniqueKeysWithValues: idToShapePairs + idToMemberShapePairs) - - // self is now initialized, perform post-initialization wireup - - // set the Shapes with references back to this model - self.shapes.values.forEach { $0.model = self } - - // set the memberIDs for each Shape - self.shapes.values.filter { $0.type != .member }.forEach { shape in - let namespace = shape.id.namespace - let name = shape.id.name - let memberIDs: [ShapeID] = Array(self.shapes.keys) - let filteredMemberIDs = memberIDs.filter { - $0.namespace == namespace && $0.name == name && $0.member != nil - } - shape.memberIDs = filteredMemberIDs.sorted() - } - - let services = self.shapes.values.filter { $0.type == .service } - guard services.count == 1 else { fatalError("Model has \(services.count) services") } - } - - private static func shapePair(id: String, astShape: ASTShape) throws -> (ShapeID, Shape) { - let shapeID = try ShapeID(id) - let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value.node) } ?? [] - switch astShape.type { - case .operation: - let shape = OperationShape( - id: shapeID, - type: astShape.type.modelType, - traits: Dictionary(uniqueKeysWithValues: idToTraitPairs), - targetID: nil, - input: astShape.input?.id, - output: astShape.output?.id - ) - return (shapeID, shape) - default: - let shape = Shape( - id: shapeID, - type: astShape.type.modelType, - traits: Dictionary(uniqueKeysWithValues: idToTraitPairs), - targetID: nil - ) - return (shapeID, shape) - } - } - - private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, Shape)] { - var baseMembers = (astShape.members ?? [:]) - if let member = astShape.member { - baseMembers["member"] = member - } - if let key = astShape.key { - baseMembers["key"] = key - } - if let value = astShape.value { - baseMembers["value"] = value - } - return try baseMembers.map { astMember in - let memberID = ShapeID(id: try ShapeID(id), member: astMember.key) - let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value.node) } - let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? []) - let targetID = try ShapeID(astMember.value.target) - return (memberID, Shape(id: memberID, type: .member, traits: traits, targetID: targetID)) - } + init(version: String, metadata: Node?, shapes: [ShapeID: Shape]) { + self.version = version + self.metadata = metadata + self.shapes = shapes } func expectShape(id: ShapeID) throws -> Shape { diff --git a/Sources/SmithyCodegenCore/Model/Node+AST.swift b/Sources/SmithyCodegenCore/Model/Node+AST.swift deleted file mode 100644 index c042c5e18..000000000 --- a/Sources/SmithyCodegenCore/Model/Node+AST.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import enum Smithy.Node - -extension ASTNode { - - /// Creates a Smithy Node from a AST-specific ASTNode. - var node: Smithy.Node { - switch self { - case .object(let object): - return .object(object.mapValues { $0.node }) - case .list(let list): - return .list(list.map { $0.node }) - case .string(let value): - return .string(value) - case .number(let value): - return .number(value) - case .boolean(let value): - return .boolean(value) - case .null: - return .null - } - } -} diff --git a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift index 652536de9..e6d67a6a7 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -10,10 +10,8 @@ import struct Smithy.ShapeID extension ASTReference { var id: ShapeID { - do { + get throws { return try ShapeID(target) - } catch { - fatalError("Creation of ShapeID from ASTReference failed: \(error.localizedDescription)") } } } diff --git a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift index a5d721863..3f8332c5b 100644 --- a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -10,57 +10,59 @@ import enum Smithy.ShapeType extension ASTType { var modelType: ShapeType { - 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: - fatalError("\"apply\" AST shapes not implemented") + 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/ModelTransformer/ASTShape+isDeprecated.swift b/Sources/SmithyCodegenCore/ModelTransformer/ASTShape+isDeprecated.swift new file mode 100644 index 000000000..99ffbc26a --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/ASTShape+isDeprecated.swift @@ -0,0 +1,41 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Date +import class Foundation.DateFormatter +import struct Foundation.Locale +import struct Foundation.TimeZone +import enum Smithy.Node + +//protocol HasASTTraits { +// var traits: [String: ASTNode]? { get } +//} +// +//extension ASTShape: HasASTTraits {} +// +//extension ASTMember: HasASTTraits {} +// +//extension HasASTTraits { +// +// var isDeprecated: Bool { +// guard let node = traits?["smithy.api#deprecated"] else { return false } +// guard case .object(let object) = node else { fatalError("Deprecated trait content is not an object") } +// guard case .string(let since) = object["since"] else { return false } +// guard let deprecationDate = formatter.date(from: since) else { return false } +// +// let removeIfDeprecatedBefore = Date(timeIntervalSince1970: 1726531200.0) // this is '2024-09-17' +// return deprecationDate < removeIfDeprecatedBefore +// } +//} +// +//private let formatter = { +// let df = DateFormatter() +// df.dateFormat = "yyyy-MM-dd" +// df.timeZone = TimeZone(identifier: "UTC") +// df.locale = Locale(identifier: "en_US_POSIX") +// return df +//}() diff --git a/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift index 64944581a..9fcfeb628 100644 --- a/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift +++ b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift @@ -10,10 +10,12 @@ import struct Smithy.ShapeID extension Shape { var schemaVarName: String { - if id.namespace == "smithy.api" { - id.preludeSchemaVarName - } else { - id.schemaVarName + get throws { + if id.namespace == "smithy.api" { + try id.preludeSchemaVarName + } else { + try id.schemaVarName + } } } } @@ -21,35 +23,39 @@ extension Shape { private extension ShapeID { var preludeSchemaVarName: String { - 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: fatalError("Unhandled prelude type converted to schemaVar: \"\(name)\"") - } - return "Smithy.Prelude.\(propertyName)" + 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 { - guard member == nil else { fatalError("Assigning member schema to a var") } - let namespacePortion = namespace.replacingOccurrences(of: ".", with: "_") - let namePortion = name - return "schema__\(namespacePortion)__\(namePortion)" + 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/Schemas/SmithySchemaCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift index 99e8d6f80..5da9854f9 100644 --- a/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift +++ b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift @@ -12,25 +12,14 @@ package struct SmithySchemaCodegen { package init() {} - package func generate(model: Model) throws -> String { + package func generate(ctx: GenerationContext) throws -> String { let writer = SwiftWriter() writer.write("import class Smithy.Schema") writer.write("import enum Smithy.Prelude") writer.write("") - writer.write("// Model has \(model.shapes.count) shapes") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .service }.count) services") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .operation }.count) operations") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .resource }.count) resources") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .structure }.count) structures") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .union }.count) unions") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .enum }.count) enums") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .intEnum }.count) intEnums") - writer.write("// Model has \(model.shapes.values.filter { $0.type == .member }.count) members") - writer.write("") - // Write schemas for all inputs & outputs and their descendants. - let shapes = try model.shapes.values + let shapes = try ctx.model.shapes.values .filter { $0.type == .structure } .filter { try $0.hasTrait(try .init("smithy.api#input")) || @@ -67,7 +56,7 @@ package struct SmithySchemaCodegen { } } } - let members = shape.members + let members = (shape as? HasMembers)?.members ?? [] if !members.isEmpty { try writer.openBlock("members: [", "],") { writer in for (index, member) in members.enumerated() { @@ -75,8 +64,8 @@ package struct SmithySchemaCodegen { } } } - if let target = shape.target { - writer.write("target: \(target.schemaVarName),") + if let target = (shape as? MemberShape)?.target { + writer.write(try "target: \(target.schemaVarName),") } if let index { writer.write("index: \(index),") diff --git a/Sources/SmithyCodegenCore/SerializableStructs/SerializableStructsCodegen.swift b/Sources/SmithyCodegenCore/SerializableStructs/SerializableStructsCodegen.swift new file mode 100644 index 000000000..efba7f06e --- /dev/null +++ b/Sources/SmithyCodegenCore/SerializableStructs/SerializableStructsCodegen.swift @@ -0,0 +1,68 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +package struct SerializableStructsCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import enum Smithy.Prelude") + writer.write("import class Smithy.Schema") + writer.write("import protocol SmithySerialization.SerializableStruct") + writer.write("import protocol SmithySerialization.ShapeSerializer") + writer.write("") + + for shape in ctx.model.shapes.values where shape.type == .structure || shape.type == .union { + let swiftType = try ctx.symbolProvider.swiftType(shape: shape) + try writer.openBlock("extension \(swiftType): SmithySerialization.SerializableStruct {", "}") { writer in + writer.write("") + writer.write("public static var schema: Smithy.Schema { \(try shape.schemaVarName) }") + writer.write("") + try writer.openBlock( + "public func serializeMembers(_ serializer: any SmithySerialization.ShapeSerializer) {", "}" + ) { writer in + for (index, member) in members(of: shape).enumerated() { + if shape.type == .structure { + let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id) + let properties = shape.hasTrait(.init("smithy.api", "error")) ? "properties." : "" + try writer.openBlock("if let value = self.\(properties)\(propertyName) {", "}") { writer in + try writeSerializeCall(writer: writer, shape: shape, member: member, index: index) + } + } else { // shape is a union + let enumCaseName = try ctx.symbolProvider.enumCaseName(shapeID: member.id) + try writer.openBlock("if case .\(enumCaseName)(let value) = self {", "}") { writer in + try writeSerializeCall(writer: writer, shape: shape, member: member, index: index) + } + } + } + } + } + writer.write("") + } + writer.unwrite("\n") + return writer.finalize() + } + + private func writeSerializeCall(writer: SwiftWriter, shape: Shape, member: MemberShape, index: Int) throws { + switch member.target.type { + case .list: + writer.write("// serialize list here") + case .map: + writer.write("// serialize map here") + default: + let methodName = try member.target.structConsumerMethod + let schemaVarName = try shape.schemaVarName + writer.write("serializer.\(methodName)(schema: \(schemaVarName).members[\(index)], value: value)") + } + } + + private func members(of shape: Shape) -> [MemberShape] { + guard let hasMembers = shape as? HasMembers else { return [] } + return hasMembers.members + } +} diff --git a/Sources/SmithyCodegenCore/SerializableStructs/Shape+StructConsumer.swift b/Sources/SmithyCodegenCore/SerializableStructs/Shape+StructConsumer.swift new file mode 100644 index 000000000..a3d9e1cff --- /dev/null +++ b/Sources/SmithyCodegenCore/SerializableStructs/Shape+StructConsumer.swift @@ -0,0 +1,52 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Shape { + + var structConsumerMethod: String { + get throws { + switch type { + case .blob: + return "writeBlob" + case .boolean: + return "writeBoolean" + case .string, .enum: + return "writeString" + case .timestamp: + return "writeTimestamp" + case .byte: + return "writeByte" + case .short: + return "writeShort" + case .integer, .intEnum: + return "writeInteger" + 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: + return "writeStruct" + case .member, .service, .resource, .operation: + throw ModelError("Cannot serialize type \(type)") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift b/Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift deleted file mode 100644 index 02b55b95b..000000000 --- a/Sources/SmithyCodegenCore/Serialization/Shape+StructConsumer.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import struct Smithy.ShapeID - -extension Shape { - - var structConsumerMethod: String { - switch type { - case .blob: - return "writeBlob" - case .boolean: - return "writeBoolean" - case .string, .enum: - return "writeString" - case .timestamp: - return "writeTimestamp" - case .byte: - return "writeByte" - case .short: - return "writeShort" - case .integer, .intEnum: - return "writeInteger" - 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: - return "writeStruct" - case .member, .service, .resource, .operation: - fatalError("Cannot serialize type \(type)") - } - } - - var structConsumerVarName: String { - guard id.member == nil else { fatalError("Constructing struct consumer for a member") } - let namespacePortion = id.namespace.replacingOccurrences(of: ".", with: "_") - let namePortion = id.name - return "structconsumer__\(namespacePortion)__\(namePortion)" - } -} diff --git a/Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.swift b/Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.swift deleted file mode 100644 index 586cff3cb..000000000 --- a/Sources/SmithyCodegenCore/Serialization/StructConsumersCodegen.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -package struct StructConsumersCodegen { - - package init() {} - - package func generate(model: Model) throws -> String { - let writer = SwiftWriter() - writer.write("import enum Smithy.Prelude") - writer.write("import protocol SmithySerialization.SerializableStruct") - writer.write("import protocol SmithySerialization.ShapeSerializer") - writer.write("") - - for shape in model.shapes.values where shape.type == .structure || shape.type == .union { - let symbolProvider = SymbolProvider(model: model) - let swiftType = symbolProvider.swiftType(shape: shape) - let varName = shape.type == .structure ? "structure" : "union" - writer.openBlock("let \(shape.structConsumerVarName) = { (\(varName): \(swiftType), serializer: any ShapeSerializer) in", "}") { writer in - for member in shape.members { - guard let target = member.target else { fatalError("Member \(member.id) does not have target") } - let memberName = symbolProvider.methodName(shapeID: member.id) - if shape.type == .structure { - let properties = shape.hasTrait(.init("smithy.api", "error")) ? "properties." : "" - writer.openBlock("if let value = \(varName).\(properties)\(memberName) {", "}") { writer in - writeSerializeCall(writer: writer, target: target) - } - } else { // shape is a union - writer.openBlock("if case .\(memberName)(let value) = \(varName) {", "}") { writer in - writeSerializeCall(writer: writer, target: target) - } - } - } - } - writer.write("") - } - writer.unwrite("\n") - return writer.finalize() - } - - private func writeSerializeCall(writer: SwiftWriter, target: Shape) { - switch target.type { - case .structure, .union: - writer.write("// serialize struct or union here") - case .list: - writer.write("// serialize list here") - case .map: - writer.write("// serialize map here") - default: - writer.write("serializer.\(target.structConsumerMethod)(schema: \(target.schemaVarName), value: value)") - } - } -} 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 index 7d3a36aab..53a582b2f 100644 --- a/Sources/SmithyCodegenCore/Shape/OperationShape.swift +++ b/Sources/SmithyCodegenCore/Shape/OperationShape.swift @@ -9,19 +9,20 @@ 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, type: ShapeType, traits: [ShapeID: Node], targetID: ShapeID?, input: ShapeID?, output: ShapeID?) { + public init(id: ShapeID, traits: [ShapeID: Node], input: ShapeID?, output: ShapeID?) { self.inputShapeID = input self.outputShapeID = output - super.init(id: id, type: type, traits: traits, targetID: targetID) + 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"): [:]]) + return model.shapes[inputShapeID]!.adding(traits: [.init("smithy.api", "input"): [:]]) } else { let traits: [ShapeID: Node] = [ .init("smithy.api", "input"): [:], @@ -33,7 +34,7 @@ class OperationShape: Shape { public var output: Shape { if let outputShapeID { - return model!.shapes[outputShapeID]!.adding(traits: [.init("smithy.api", "output"): [:]]) + return model.shapes[outputShapeID]!.adding(traits: [.init("smithy.api", "output"): [:]]) } else { let traits: [ShapeID: Node] = [ .init("smithy.api", "input"): [:], 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 index 8129bbed7..9d5635159 100644 --- a/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift +++ b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift @@ -33,96 +33,96 @@ extension Shape { static var unit: Shape { let schema = Smithy.Prelude.unitSchema - return Shape(id: schema.id, type: schema.type, traits: schema.traits, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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, targetID: nil) + 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 index bd874acf0..35f541bec 100644 --- a/Sources/SmithyCodegenCore/Shape/Shape.swift +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -12,16 +12,13 @@ import enum Smithy.Node public class Shape { public let id: ShapeID public let type: ShapeType - public internal(set) var traits: [ShapeID: Node] - var targetID: ShapeID? - var memberIDs: [ShapeID] = [] - weak var model: Model? + public let traits: [ShapeID: Node] + weak var model: Model! - public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node], targetID: ShapeID?) { + public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node]) { self.id = id self.type = type self.traits = traits - self.targetID = targetID } public func hasTrait(_ traitID: ShapeID) -> Bool { @@ -32,20 +29,9 @@ public class Shape { traits[traitID] } - public var members: [Shape] { - guard let model else { return [] } - return memberIDs.map { model.shapes[$0]! } - } - - public var target: Shape? { - guard let targetID else { return nil } - return model?.shapes[targetID] ?? Shape.prelude[targetID] - } - 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, targetID: targetID) - new.memberIDs = memberIDs + let new = Shape(id: id, type: type, traits: combinedTraits) new.model = model return new } @@ -61,15 +47,12 @@ public class Shape { for shape in shapes { if descendants.contains(shape) { continue } descendants.insert(shape) - shape.members.map { $0.target }.compactMap { $0 }.forEach { - descendants.insert($0) - $0.descendants(&descendants) - } + shape.descendants(&descendants) } } - private func candidates(for shape: Shape) -> [Shape] { - ([shape.target] + shape.members.map { $0.target }).compactMap { $0 } + func candidates(for shape: Shape) -> [Shape] { + [] // default. May be overridden by Shape subclasses. } } 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/Node+Rendered.swift b/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift similarity index 100% rename from Sources/SmithyCodegenCore/Node+Rendered.swift rename to Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift diff --git a/Sources/SmithyCodegenCore/String+Utils.swift b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift similarity index 100% rename from Sources/SmithyCodegenCore/String+Utils.swift rename to Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift diff --git a/Sources/SmithyCodegenCore/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift similarity index 100% rename from Sources/SmithyCodegenCore/SwiftWriter.swift rename to Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift diff --git a/Sources/SmithyCodegenCore/Model/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift similarity index 50% rename from Sources/SmithyCodegenCore/Model/SymbolProvider.swift rename to Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift index aa506e16e..4eca55b5b 100644 --- a/Sources/SmithyCodegenCore/Model/SymbolProvider.swift +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -5,52 +5,74 @@ // SPDX-License-Identifier: Apache-2.0 // -import Foundation +import struct Foundation.NSRange +import class Foundation.NSRegularExpression import struct Smithy.ShapeID public struct SymbolProvider { + let service: ServiceShape let model: Model - init(model: Model) { + init(service: ServiceShape, model: Model) { + self.service = service self.model = model } - func serviceName(service: Shape) -> String { - guard service.type == .service else { fatalError("Called serviceName on non-service shape") } - guard case .object(let serviceInfo) = service.getTrait(.init("aws.api", "service")) else { fatalError("No service trait on service") } - guard case .string(let sdkID) = serviceInfo["sdkId"] else { fatalError("No sdkId on service trait") } - return sdkID.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "Service", with: "") + 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: "") + } } - func swiftType(shape: Shape) -> String { - if case .string(let name) = shape.getTrait(.init("swift.synthetic", "operationName")), shape.hasTrait(.init("smithy.api", "input")) { - return name + "Input" - } else if shape.hasTrait(.init("smithy.api", "input")) { + 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 { fatalError("Operation for input \(shape.id) not found") } - return operation.id.name + "Input" - } else if case .string(let name) = shape.getTrait(.init("swift.synthetic", "operationName")), shape.hasTrait(.init("smithy.api", "output")){ - return name + "Output" - } else if shape.hasTrait(.init("smithy.api", "output")) { + .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 { fatalError("Operation for output \(shape.id) not found") } - return operation.id.name + "Output" - } else if shape.hasTrait(.init("smithy.api", "error")) { + .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 { - guard let service = model.shapes.values.first(where: { $0.type == .service }) else { fatalError("service not found") } - return serviceName(service: service) + "ClientTypes." + shape.id.name + return try "\(serviceName)ClientTypes.\(shape.id.name)" } } - func methodName(shapeID: ShapeID) -> String { - guard let member = shapeID.member else { fatalError("Shape ID has no member 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 { @@ -68,35 +90,40 @@ private extension String { // all non-alphanumeric characters: "acm-success"-> "acm success" let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") - result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: NSRange(location: 0, length: result.count), withTemplate: " ") + 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: NSRange(location: 0, length: result.count), withTemplate: " _ ") + 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: NSRange(location: 0, length: result.count), withTemplate: "$1 v$2") + 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: NSRange(location: 0, length: result.count), withTemplate: "$1 V$2") + 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: NSRange(location: 0, length: result.count), withTemplate: " ") + 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: NSRange(location: 0, length: result.count), withTemplate: "$1 $2") + 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: NSRange(location: 0, length: result.count), withTemplate: "$1 $2") + 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: NSRange(location: 0, length: result.count), withTemplate: " ").trimmingCharacters(in: .whitespaces) + 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/CodegenError.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift similarity index 86% rename from Sources/SmithyCodegenCore/CodegenError.swift rename to Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift index 6734b7329..2d17de6bb 100644 --- a/Sources/SmithyCodegenCore/CodegenError.swift +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -struct CodegenError: Error { +public struct SymbolProviderError: Error { let localizedDescription: String init(_ localizedDescription: String) { diff --git a/Sources/SmithySerialization/Schema/SchemaTraits.swift b/Sources/SmithySerialization/Schema/SchemaTraits.swift index 43d9ce385..4b34c2abe 100644 --- a/Sources/SmithySerialization/Schema/SchemaTraits.swift +++ b/Sources/SmithySerialization/Schema/SchemaTraits.swift @@ -18,4 +18,5 @@ public let permittedTraitIDs: Set = [ "smithy.api#required", "smithy.api#default", "smithy.api#timestampFormat", + "smithy.api#sensitive", ] diff --git a/Sources/SmithySerialization/SerializableShape.swift b/Sources/SmithySerialization/SerializableShape.swift index 491fdb7fd..8a15b5ff8 100644 --- a/Sources/SmithySerialization/SerializableShape.swift +++ b/Sources/SmithySerialization/SerializableShape.swift @@ -5,6 +5,9 @@ // SPDX-License-Identifier: Apache-2.0 // +import class Smithy.Schema + public protocol SerializableShape { - func serialize(serializer: ShapeSerializer) throws + static var schema: Smithy.Schema { get } + func serialize(_ serializer: any ShapeSerializer) } diff --git a/Sources/SmithySerialization/SerializableStruct.swift b/Sources/SmithySerialization/SerializableStruct.swift index 5109b04b3..02a3c97ad 100644 --- a/Sources/SmithySerialization/SerializableStruct.swift +++ b/Sources/SmithySerialization/SerializableStruct.swift @@ -6,5 +6,12 @@ // public protocol SerializableStruct: SerializableShape { - func serializeMembers(encoder: ShapeSerializer) throws + func serializeMembers(_ serializer: any ShapeSerializer) +} + +public extension SerializableStruct { + + func serialize(_ serializer: any ShapeSerializer) { + serializer.writeStruct(schema: Self.schema, value: self) + } } diff --git a/Sources/SmithySerialization/ShapeSerializer.swift b/Sources/SmithySerialization/ShapeSerializer.swift index 5a215e29b..ec1f6a7da 100644 --- a/Sources/SmithySerialization/ShapeSerializer.swift +++ b/Sources/SmithySerialization/ShapeSerializer.swift @@ -8,18 +8,18 @@ import struct Foundation.Data import struct Foundation.Date import enum Smithy.ByteStream -import struct Smithy.Document +import protocol Smithy.SmithyDocument import class Smithy.Schema public protocol ShapeSerializer { - func writeStruct(schema: Schema, struct: SerializableStruct) + func writeStruct(schema: Schema, value: any SerializableStruct) func writeList(schema: Schema, size: Int, consumer: Consumer) func writeMap(schema: Schema, size: Int, consumer: Consumer) func writeBoolean(schema: Schema, value: Bool) func writeByte(schema: Schema, value: Int8) func writeShort(schema: Schema, value: Int16) func writeInteger(schema: Schema, value: Int) - func writeLong(schema: Schema, value: Int64) + func writeLong(schema: Schema, value: Int) func writeFloat(schema: Schema, value: Float) func writeDouble(schema: Schema, value: Double) func writeBigInteger(schema: Schema, value: Int64) @@ -27,14 +27,14 @@ public protocol ShapeSerializer { func writeString(schema: Schema, value: String) func writeBlob(schema: Schema, value: Data) func writeTimestamp(schema: Schema, value: Date) - func writeDocument(schema: Schema, value: Document) + func writeDocument(schema: Schema, value: any SmithyDocument) func writeNull(schema: Schema) func writeDataStream(schema: Schema, value: ByteStream) - func writeEventStream(schema: Schema, value: AsyncThrowingStream) + func writeEventStream(schema: Schema, value: AsyncThrowingStream) } public extension ShapeSerializer { - + func writeString(schema: Schema, value: T) where T.RawValue == String { writeString(schema: schema, value: value.rawValue) } @@ -47,7 +47,7 @@ public extension ShapeSerializer { // by default, do nothing } - func writeEventStream(schema: Schema, value: AsyncThrowingStream) { + func writeEventStream(schema: Schema, value: AsyncThrowingStream) { // by default, do nothing } } diff --git a/Sources/SmithySerialization/StringSerializer.swift b/Sources/SmithySerialization/StringSerializer.swift new file mode 100644 index 000000000..23cd53b18 --- /dev/null +++ b/Sources/SmithySerialization/StringSerializer.swift @@ -0,0 +1,122 @@ +// +// 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 class Foundation.ISO8601DateFormatter +import class Smithy.Schema +import struct Smithy.ShapeID +import protocol Smithy.SmithyDocument + +public class StringSerializer: ShapeSerializer { + private var _string = "" + let suffix = ", " + let redacted = "[REDACTED]" + + public init() {} + + public func writeStruct(schema: Smithy.Schema, value: any SerializableStruct) { + addNameAndValue(schema) { + let typeName = type(of: value) + let serializer = StringSerializer() + value.serializeMembers(serializer) + serializer.removeTrailingSuffix() + return "\(typeName)(\(serializer.string()))" + } + } + + public func string() -> String { + removeTrailingSuffix() + return _string + } + + public func writeList(schema: Smithy.Schema, size: Int, consumer: (any ShapeSerializer) throws -> Void) { + addNameAndValue(schema) { "" } + } + + public func writeMap(schema: Smithy.Schema, size: Int, consumer: (any MapSerializer) throws -> Void) { + addNameAndValue(schema) { "" } + } + + public func writeBoolean(schema: Smithy.Schema, value: Bool) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeByte(schema: Smithy.Schema, value: Int8) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeShort(schema: Smithy.Schema, value: Int16) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeInteger(schema: Smithy.Schema, value: Int) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeLong(schema: Smithy.Schema, value: Int) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeFloat(schema: Smithy.Schema, value: Float) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeDouble(schema: Smithy.Schema, value: Double) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeBigInteger(schema: Smithy.Schema, value: Int64) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeBigDecimal(schema: Smithy.Schema, value: Double) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeString(schema: Smithy.Schema, value: String) { + addNameAndValue(schema) { "\"\(value)\"" } + } + + public func writeBlob(schema: Smithy.Schema, value: Data) { + addNameAndValue(schema) { "<\(value.count) bytes>" } + } + + public func writeTimestamp(schema: Smithy.Schema, value: Date) { + addNameAndValue(schema) { df.string(from: value) } + } + + public func writeDocument(schema: Smithy.Schema, value: any Smithy.SmithyDocument) { + addNameAndValue(schema) { "" } + } + + public func writeNull(schema: Smithy.Schema) { + addNameAndValue(schema) { "nil" } + } + + private func addNameAndValue(_ schema: Schema, _ value: () -> String) { + if schema.type == .member, let name = schema.id.member { + _string += "\(name): " + } + _string += schema.isSensitive ? redacted : value() + _string += suffix + } + + private func removeTrailingSuffix() { + if _string.hasSuffix(suffix) { _string.removeLast(suffix.count) } + } +} + +private let df = ISO8601DateFormatter() + +extension Smithy.Schema { + + var isSensitive: Bool { + let sensitive = ShapeID("smithy.api", "sensitive") + return traits[sensitive] ?? target?.traits[sensitive] != nil + } +} diff --git a/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift b/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift deleted file mode 100644 index 928aedfbd..000000000 --- a/Tests/SmithyCodegenCoreTests/ShapeIDTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import XCTest -import SmithyCodegenCore - -final class ShapeIDTests: XCTestCase { - - func test_init_createsShapeIDWithNamespace() throws { - let subject = try ShapeID("smithy.test#TestName$TestMember") - XCTAssertEqual(subject.namespace, "smithy.test") - } - - func test_init_createsShapeIDWithName() throws { - let subject = try ShapeID("smithy.test#TestName$TestMember") - XCTAssertEqual(subject.name, "TestName") - } - - func test_init_createsShapeIDWithMember() throws { - let subject = try ShapeID("smithy.test#TestName$TestMember") - XCTAssertEqual(subject.member, "TestMember") - } - - func test_init_createsShapeIDWithoutMember() throws { - let subject = try ShapeID("smithy.test#TestName") - XCTAssertNil(subject.member) - } -} diff --git a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift new file mode 100644 index 000000000..069cfca7a --- /dev/null +++ b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SmithyCodegenCore + +// No tests diff --git a/Tests/SmithyTests/ShapeIDTests.swift b/Tests/SmithyTests/ShapeIDTests.swift index d4b28de16..51b35b9f1 100644 --- a/Tests/SmithyTests/ShapeIDTests.swift +++ b/Tests/SmithyTests/ShapeIDTests.swift @@ -10,13 +10,23 @@ import Smithy class ShapeIDTests: XCTestCase { - func test_description_noMember() { - let subject = ShapeID("smithy.test", "TestShape") - XCTAssertEqual(subject.description, "smithy.test#TestShape") + func test_init_createsShapeIDWithNamespace() throws { + let subject = try ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.namespace, "smithy.test") } - func test_description_withMember() { - let subject = ShapeID("smithy.test", "TestShape", "TestMember") - XCTAssertEqual(subject.description, "smithy.test#TestShape$TestMember") + func test_init_createsShapeIDWithName() throws { + let subject = try ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.name, "TestName") + } + + func test_init_createsShapeIDWithMember() throws { + let subject = try ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.member, "TestMember") + } + + func test_init_createsShapeIDWithoutMember() throws { + let subject = try ShapeID("smithy.test#TestName") + XCTAssertNil(subject.member) } } 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 227d6663c..31c19f640 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 @@ -209,7 +209,7 @@ 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 - true + false // ctx.service.allTraits.keys // .any { it.name == "rpcv2Cbor" } @@ -219,21 +219,6 @@ abstract class HTTPBindingProtocolGenerator( resolveShapesNeedingSchema(ctx) .filter { it.type != ShapeType.MEMBER } // Member schemas are only rendered in-line .sorted() - val file = SchemaFileUtils.filename(ctx.settings, "whatever") - ctx.delegator.useFileWriter(file) { writer -> - writer.write("// Model has ${ctx.model.shapes().count()} shapes") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.SERVICE }.count()} services") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.OPERATION }.count()} operations") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.RESOURCE }.count()} resources") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.STRUCTURE }.count()} structures") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.UNION }.count()} unions") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.ENUM }.count()} enums") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.INT_ENUM }.count()} intEnums") - writer.write("// Model has ${ctx.model.shapes().filter { it.type == ShapeType.MEMBER }.count()} members") - writer.write("") - writer.write("// Number of schemas: ${nestedShapes.count()}") - writer.write("") - } nestedShapes.forEach { renderSchema(ctx, it) } }