diff --git a/Package.swift b/Package.swift index 755b40342..940b1e6bf 100644 --- a/Package.swift +++ b/Package.swift @@ -268,6 +268,10 @@ let package = Package( ], resources: [ .process("Resources") ] ), + .testTarget( + name: "SmithyTests", + dependencies: ["Smithy"] + ), .testTarget( name: "SmithyCBORTests", dependencies: ["SmithyCBOR", "ClientRuntime", "SmithyTestUtil"] diff --git a/Sources/Smithy/Schema/Node.swift b/Sources/Smithy/Schema/Node.swift new file mode 100644 index 000000000..ce6c2d64e --- /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 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 +} + +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..4658ccfc2 --- /dev/null +++ b/Sources/Smithy/Schema/Prelude.swift @@ -0,0 +1,90 @@ +// +// 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 enum Prelude { + + public static var unitSchema: Schema { + Schema(id: .init("smithy.api", "Unit"), type: .structure) + } + + public static var booleanSchema: Schema { + Schema(id: .init("smithy.api", "Boolean"), type: .boolean) + } + + public static var stringSchema: Schema { + Schema(id: .init("smithy.api", "String"), type: .string) + } + + public static var integerSchema: Schema { + Schema(id: .init("smithy.api", "Integer"), type: .integer) + } + + public static var blobSchema: Schema { + Schema(id: .init("smithy.api", "Blob"), type: .blob) + } + + public static var timestampSchema: Schema { + Schema(id: .init("smithy.api", "Timestamp"), type: .timestamp) + } + + public static var byteSchema: Schema { + Schema(id: .init("smithy.api", "Byte"), type: .byte) + } + + public static var shortSchema: Schema { + Schema(id: .init("smithy.api", "Short"), type: .short) + } + + public static var longSchema: Schema { + Schema(id: .init("smithy.api", "Long"), type: .long) + } + + public static var floatSchema: Schema { + Schema(id: .init("smithy.api", "Float"), type: .float) + } + + public static var doubleSchema: Schema { + Schema(id: .init("smithy.api", "Double"), type: .double) + } + + public static var documentSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveDocument"), type: .document) + } + + public static var primitiveBooleanSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveBoolean"), type: .boolean, traits: [defaultTraitID: false]) + } + + public static var primitiveIntegerSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveInteger"), type: .integer, traits: [defaultTraitID: 0]) + } + + public static var primitiveByteSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveByte"), type: .byte, traits: [defaultTraitID: 0]) + } + + public static var primitiveShortSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveShort"), type: .short, traits: [defaultTraitID: 0]) + } + + public static var primitiveLongSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveLong"), type: .long, traits: [defaultTraitID: 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 new file mode 100644 index 000000000..f8cac05fb --- /dev/null +++ b/Sources/Smithy/Schema/Schema.swift @@ -0,0 +1,72 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// 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 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. + public let members: [Schema] + + /// The target schema for this schema. Will only be used when this is a member schema. + public let target: Schema? + + /// 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: ShapeID, + type: ShapeType, + traits: [ShapeID: Node] = [:], + members: [Schema] = [], + target: Schema? = nil, + index: Int = -1 + ) { + self.id = id + self.type = type + self.traits = traits + self.members = members + 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..647bbfeb8 --- /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: Sendable, Hashable { + public let namespace: String + public let name: String + public let member: String? + + /// Creates a Shape ID for a Smithy shape. + /// + /// This initializer does no validation of length or of allowed characters in the Shape ID; + /// that is to be ensured by the caller (typically calls to this initializer will be code-generated + /// from previously validated Smithy models.) + /// - Parameters: + /// - namespace: The namespace for this shape, i.e. `smithy.api`. + /// - name: The name for this shape, i.e. `Integer`. + /// - member: The optional member name for this shape. + public init(_ namespace: String, _ name: String, _ member: String? = nil) { + self.namespace = namespace + self.name = name + self.member = member + } +} + +extension ShapeID: CustomStringConvertible { + + /// Returns the absolute Shape ID in a single, printable string. + public var description: String { + if let member = self.member { + return "\(namespace)#\(name)$\(member)" + } else { + return "\(namespace)#\(name)" + } + } +} diff --git a/Sources/Smithy/Document/ShapeType.swift b/Sources/Smithy/Schema/ShapeType.swift similarity index 71% rename from Sources/Smithy/Document/ShapeType.swift rename to Sources/Smithy/Schema/ShapeType.swift index 5b3de70a1..43a97cc5b 100644 --- a/Sources/Smithy/Document/ShapeType.swift +++ b/Sources/Smithy/Schema/ShapeType.swift @@ -5,9 +5,8 @@ // 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 { +/// 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: Sendable { case blob case boolean case string 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/DirectedSwiftCodegen.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt index 19f04bf0c..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}") @@ -81,12 +80,12 @@ class DirectedSwiftCodegen( generateMessageMarshallable(ctx) generateMessageUnmarshallable(ctx) generateCodableConformanceForNestedTypes(ctx) + generateSchemas(ctx) initializeMiddleware(ctx) 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 3b277f731..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 @@ -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)) { @@ -187,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 } @@ -201,10 +207,42 @@ abstract class HTTPBindingProtocolGenerator( } } + private fun usesSchemaBasedSerialization(ctx: ProtocolGenerator.GenerationContext): Boolean = + // This fun is temporary; it will be eliminated when all services/protocols are moved to schema-based + ctx.service.allTraits.keys + .any { it.name == "rpcv2Cbor" } + + override fun generateSchemas(ctx: ProtocolGenerator.GenerationContext) { + if (!usesSchemaBasedSerialization(ctx)) return // temporary condition + val nestedShapes = + resolveShapesNeedingSchema(ctx) + .filter { it.type != ShapeType.MEMBER } // Member schemas are only rendered in-line + nestedShapes.forEach { renderSchemas(ctx, it) } + } + + private fun renderSchemas( + ctx: ProtocolGenerator.GenerationContext, + shape: Shape, + ) { + val symbol: Symbol = ctx.symbolProvider.toSymbol(shape) + val symbolName = symbol.name + val filename = SchemaFileUtils.filename(ctx.settings, "${shape.id.name}+Schema") + val encodeSymbol = + Symbol + .builder() + .definitionFile(filename) + .name(symbolName) + .build() + ctx.delegator.useShapeWriter(encodeSymbol) { writer -> + SchemaGenerator(ctx, writer).renderSchema(shape) + } + } + fun renderCodableExtension( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { +// if (!usesSchemaBasedSerialization(ctx)) return // temporary condition if (!shape.hasTrait() && !shape.hasTrait()) { return } @@ -250,11 +288,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 +401,74 @@ 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..dd666794b --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt @@ -0,0 +1,77 @@ +package software.amazon.smithy.swift.codegen.integration.serde.schema + +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes +import kotlin.jvm.optionals.getOrNull + +class SchemaGenerator( + val ctx: ProtocolGenerator.GenerationContext, + val writer: SwiftWriter, +) { + fun renderSchema(shape: Shape) { + writer.openBlock( + "var \$L: \$N {", + "}", + shape.schemaVar(writer), + SmithyTypes.Schema, + ) { + renderSchemaStruct(shape) + writer.unwrite(",\n") + writer.write("") + } + } + + private fun renderSchemaStruct( + shape: Shape, + index: Int? = null, + ) { + writer.openBlock(".init(", "),") { + writer.write( + "id: \$L,", + shapeID(shape.id), + ) + writer.write("type: .\$L,", shape.type) + val relevantTraits = shape.allTraits.filter { permittedTraitIDs.contains(it.key.toString()) } + if (relevantTraits.isNotEmpty()) { + writer.openBlock("traits: [", "],") { + relevantTraits.forEach { trait -> + writer.write( + "\$L: \$L,", + shapeID(trait.key), + trait.value.toNode().toSwiftNode(writer), + ) + } + } + } + if (shape.members().isNotEmpty()) { + writer.openBlock("members: [", "],") { + shape.members().withIndex().forEach { renderSchemaStruct(it.value, it.index) } + } + } + targetShape(shape)?.let { + writer.write("target: \$L,", it.schemaVar(writer)) + } + index?.let { + writer.write("index: \$L,", it) + } + writer.unwrite(",\n") + writer.write("") + } + } + + private fun shapeID(id: ShapeId): String = + writer.format( + ".init(\$S, \$S\$L)", + id.namespace, + id.name, + id.member.getOrNull()?.let { writer.format(", \$S", it) } ?: "", + ) + + private fun targetShape(shape: Shape): Shape? = memberShape(shape)?.let { ctx.model.expectShape(it.target) } + + private fun memberShape(shape: Shape): MemberShape? = shape.asMemberShape().getOrNull() +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt new file mode 100644 index 000000000..74af64df7 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt @@ -0,0 +1,48 @@ +package software.amazon.smithy.swift.codegen.integration.serde.schema + +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes +import kotlin.jvm.optionals.getOrNull + +fun Shape.schemaVar(writer: SwiftWriter): String = + if (this.id.namespace == "smithy.api") { + this.id.preludeSchemaVarName(writer) + } else { + this.id.schemaVarName() + } + +private fun ShapeId.preludeSchemaVarName(writer: SwiftWriter): String { + val propertyName = + when (this.name) { + "Unit" -> "unitSchema" + "String" -> "stringSchema" + "Blob" -> "blobSchema" + "Integer" -> "integerSchema" + "Timestamp" -> "timestampSchema" + "Boolean" -> "booleanSchema" + "Float" -> "floatSchema" + "Double" -> "doubleSchema" + "Long" -> "longSchema" + "Short" -> "shortSchema" + "Byte" -> "byteSchema" + "PrimitiveInteger" -> "primitiveIntegerSchema" + "PrimitiveBoolean" -> "primitiveBooleanSchema" + "PrimitiveFloat" -> "primitiveFloatSchema" + "PrimitiveDouble" -> "primitiveDoubleSchema" + "PrimitiveLong" -> "primitiveLongSchema" + "PrimitiveShort" -> "primitiveShortSchema" + "PrimitiveByte" -> "primitiveByteSchema" + "Document" -> "documentSchema" + else -> throw Exception("Unhandled prelude type converted to schemaVar: ${this.name}") + } + return writer.format("\$N.\$L", SmithyTypes.Prelude, propertyName) +} + +private fun ShapeId.schemaVarName(): String { + assert(this.member.getOrNull() == null) + val namespacePortion = this.namespace.replace(".", "_") + val namePortion = this.name + return "schema__${namespacePortion}__$namePortion" +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt 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/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/SmithyTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt index 4e8bd7caf..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 @@ -21,16 +21,20 @@ 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 Prelude = runtimeSymbol("Prelude", SwiftDeclaration.ENUM) } 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" + } + } +}