diff --git a/Package.swift b/Package.swift index eea124918..2bf2b8885 100644 --- a/Package.swift +++ b/Package.swift @@ -57,10 +57,12 @@ let package = Package( .library(name: "SmithySwiftNIO", targets: ["SmithySwiftNIO"]), .library(name: "SmithyTelemetryAPI", targets: ["SmithyTelemetryAPI"]), .library(name: "SmithyHTTPClientAPI", targets: ["SmithyHTTPClientAPI"]), + .plugin(name: "SmithyCodeGeneratorPlugin", targets: ["SmithyCodeGeneratorPlugin"]), ], dependencies: { var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.58.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.6.2"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.22.0"), @@ -119,6 +121,7 @@ let package = Package( "SmithyChecksumsAPI", "SmithyChecksums", "SmithyCBOR", + "SmithySerialization", .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"), // Only include these on macOS, iOS, tvOS, watchOS, and macCatalyst (visionOS and Linux are excluded) .product( @@ -285,12 +288,36 @@ let package = Package( dependencies: [ "SmithyReadWrite", "SmithyTimestamps", + "Smithy", + "SmithySerialization", .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift") ] ), .target( name: "SmithyWaitersAPI" ), + .plugin( + name: "SmithyCodeGeneratorPlugin", + capability: .buildTool(), + dependencies: [ + "SmithyCodegenCLI", + ] + ), + .executableTarget( + name: "SmithyCodegenCLI", + dependencies: [ + "SmithyCodegenCore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), + .target( + name: "SmithyCodegenCore", + dependencies: [ + "Smithy", + "SmithySerialization", + ], + resources: [ .process("Resources") ] + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ @@ -301,6 +328,10 @@ let package = Package( ], resources: [ .process("Resources") ] ), + .testTarget( + name: "SmithyTests", + dependencies: ["Smithy"] + ), .testTarget( name: "SmithySwiftNIOTests", dependencies: [ @@ -374,5 +405,10 @@ let package = Package( name: "SmithyStreamsTests", dependencies: ["SmithyStreams", "Smithy"] ), + .testTarget( + name: "SmithyCodegenCoreTests", + dependencies: ["SmithyCodegenCore"], + resources: [ .process("Resources") ] + ), ].compactMap { $0 } ) diff --git a/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift new file mode 100644 index 000000000..62ad22234 --- /dev/null +++ b/Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift @@ -0,0 +1,92 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import class Foundation.FileManager +import class Foundation.JSONDecoder +import struct Foundation.URL +import PackagePlugin + +@main +struct SmithyCodeGeneratorPlugin: BuildToolPlugin { + + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + // This plugin only runs for package targets that can have source files. + guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] } + + // Retrieve the `SmithyCodegenCLI` tool from the plugin's tools. + let smithyCodegenCLITool = try context.tool(named: "SmithyCodegenCLI") + + // Construct a build command for each source file with a particular suffix. + return try sourceFiles.map(\.path).compactMap { + try createBuildCommand( + name: target.name, + for: $0, + in: context.pluginWorkDirectory, + with: smithyCodegenCLITool.path + ) + } + } + + private func createBuildCommand( + name: String, + for inputPath: Path, + in outputDirectoryPath: Path, + with generatorToolPath: Path + ) throws -> Command? { + // Skip any file that isn't the smithy-model-info.json for this service. + guard inputPath.lastComponent == "smithy-model-info.json" else { return nil } + + let currentWorkingDirectoryFileURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + // Get the smithy-model-info.json file contents. + let modelInfoData = try Data(contentsOf: URL(fileURLWithPath: inputPath.string)) + let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData) + + // Get the service ID & model path & settings sdkId from smithy-model-info. + let service = smithyModelInfo.service + let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path) + let modelPath = Path(modelPathURL.path) + + // Construct the Schemas.swift path. + let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") + + // Construct the Serialize.swift path. + let serializeSwiftPath = outputDirectoryPath.appending("\(name)Serialize.swift") + + // Construct the Deserialize.swift path. + let deserializeSwiftPath = outputDirectoryPath.appending("\(name)Deserialize.swift") + + // Construct the build command that invokes SmithyCodegenCLI. + return .buildCommand( + displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", + executable: generatorToolPath, + arguments: [ + service, + modelPath, + "--schemas-path", schemasSwiftPath, + "--serialize-path", serializeSwiftPath, + "--deserialize-path", deserializeSwiftPath + ], + inputFiles: [inputPath, modelPath], + outputFiles: [ + schemasSwiftPath, + serializeSwiftPath, + deserializeSwiftPath, + ] + ) + } +} + +/// Codable structure for reading the contents of `smithy-model-info.json` +private struct SmithyModelInfo: Decodable { + /// The shape ID of the service being generated. Must exist in the model. + let service: String + + /// The path to the model, from the root of the target's project. Required. + let path: String +} diff --git a/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift b/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift index 6c6d80e4c..ecc3781c9 100644 --- a/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift +++ b/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift @@ -8,9 +8,9 @@ @_spi(SmithyDocumentImpl) public struct BigIntegerDocument: SmithyDocument { public var type: ShapeType { .bigInteger } - let value: Int + let value: Int64 - public init(value: Int) { + public init(value: Int64) { self.value = value } @@ -29,7 +29,7 @@ public struct BigIntegerDocument: SmithyDocument { } public func asInteger() throws -> Int { - value + Int(value) } public func asLong() throws -> Int64 { @@ -45,7 +45,7 @@ public struct BigIntegerDocument: SmithyDocument { } public func asBigInteger() throws -> Int64 { - Int64(value) + value } public func asBigDecimal() throws -> Double { diff --git a/Sources/Smithy/Node.swift b/Sources/Smithy/Node.swift new file mode 100644 index 000000000..721e168c1 --- /dev/null +++ b/Sources/Smithy/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, Hashable { + 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..0383c5f1c --- /dev/null +++ b/Sources/Smithy/Schema/Prelude.swift @@ -0,0 +1,88 @@ +// +// 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", "Document"), type: .document) + } + + public static var primitiveBooleanSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveBoolean"), type: .boolean, traits: [DefaultTrait(false)]) + } + + public static var primitiveIntegerSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveInteger"), type: .integer, traits: [DefaultTrait(0)]) + } + + public static var primitiveByteSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveByte"), type: .byte, traits: [DefaultTrait(0)]) + } + + public static var primitiveShortSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveShort"), type: .short, traits: [DefaultTrait(0)]) + } + + public static var primitiveLongSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveLong"), type: .long, traits: [DefaultTrait(0)]) + } + + public static var primitiveFloatSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveFloat"), type: .float, traits: [DefaultTrait(0.0)]) + } + + public static var primitiveDoubleSchema: Schema { + Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [DefaultTrait(0.0)]) + } +} diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift new file mode 100644 index 000000000..cdcfcb4da --- /dev/null +++ b/Sources/Smithy/Schema/Schema.swift @@ -0,0 +1,103 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// A class which describes selected, modeled information for a Smithy shape. +/// +/// Typically, the Schema contains only modeled info & properties that are relevant to +/// serialization, transport bindings, and other functions performed by the SDK. +public struct 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: TraitCollection + + /// 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 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. + /// + /// 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: TraitCollection = TraitCollection(), + members: [Schema] = [], + 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.index = index + } + + /// Checks if a schema has a trait, by trait type. + /// - Parameter type: The trait type to be checked. + /// - Returns: `true` if the schema has a trait for the passed trait, `false` otherwise. + public func hasTrait(_ type: T.Type) -> Bool { + return traits.hasTrait(T.self) + } + + /// Gets a trait from the schema. + /// - Parameter type: The trait to be retrieved. + /// - Returns: The requested trait, or `nil` if the schema doesn't have that trait. + public func getTrait(_ type: T.Type) throws -> T? { + try traits.getTrait(type) + } + + /// Returns the member for a List's element. + /// + /// Only access this property on a schema of type `.list`. + public var member: Schema { + members[0] // `member` will be the only member in a list schema + } + + /// Returns the key member for a Map's key. + /// + /// Only access this property on a schema of type `.map`. + public var key: Schema { + members[0] // `key` will be the first member in a map schema, before `value` + } + + /// Returns the value member for a Map's value. + /// + /// Only access this property on a schema of type `.map`. + public var value: Schema { + members[1] // `value` will be the second / last member in a map schema, after `key` + } +} diff --git a/Sources/Smithy/ShapeID.swift b/Sources/Smithy/ShapeID.swift new file mode 100644 index 000000000..e5a3333bc --- /dev/null +++ b/Sources/Smithy/ShapeID.swift @@ -0,0 +1,95 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Represents a Smithy shape ID. +/// +/// The id that ShapeID is created from is presumed to be properly formed, since this type will usually +/// be constructed from previously validated models. +/// +/// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id). +public struct ShapeID: Hashable, Sendable { + public let namespace: String + public let name: String + public let member: String? + + /// Creates a Shape ID for a Smithy shape. + /// + /// This initializer does no validation of length or of allowed characters in the Shape ID; + /// that is to be ensured by the caller (typically calls to this initializer will be code-generated + /// from previously validated Smithy models.) + /// - Parameters: + /// - namespace: The namespace for this shape, i.e. `smithy.api`. + /// - name: The name for this shape, i.e. `Integer`. + /// - member: The optional member name for this shape. + public init(_ namespace: String, _ name: String, _ member: String? = nil) { + self.namespace = namespace + self.name = name + self.member = member + } + + public init(_ id: String) throws { + let splitOnPound = id.split(separator: "#") + guard splitOnPound.count == 2 else { + throw ShapeIDError("id \"\(id)\" does not have a #") + } + guard let namespace = splitOnPound.first, !namespace.isEmpty else { + throw ShapeIDError("id \"\(id)\" does not have a nonempty namespace") + } + self.namespace = String(namespace) + let splitOnDollar = splitOnPound.last!.split(separator: "$") + switch splitOnDollar.count { + case 2: + self.name = String(splitOnDollar.first!) + self.member = String(splitOnDollar.last!) + case 1: + self.name = String(splitOnDollar.first!) + self.member = nil + default: + throw ShapeIDError("id \"\(id)\" has more than one $") + } + } + + public init(id: ShapeID, member: String?) { + self.namespace = id.namespace + self.name = id.name + self.member = member + } + + public var absolute: String { + return "\(namespace)#\(relative)" + } + + public var relative: String { + if let member { + return "\(name)$\(member)" + } else { + return "\(name)" + } + } +} + +extension ShapeID: Comparable { + + // This logic matches the sorting logic used by the Java-based codegen + public static func < (lhs: ShapeID, rhs: ShapeID) -> Bool { + lhs.absolute.lowercased() < rhs.absolute.lowercased() + } +} + +extension ShapeID: CustomStringConvertible { + + /// Returns the absolute Shape ID in a single, printable string. + public var description: String { absolute } +} + +public struct ShapeIDError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/Smithy/Document/ShapeType.swift b/Sources/Smithy/ShapeType.swift similarity index 71% rename from Sources/Smithy/Document/ShapeType.swift rename to Sources/Smithy/ShapeType.swift index 5b3de70a1..43a97cc5b 100644 --- a/Sources/Smithy/Document/ShapeType.swift +++ b/Sources/Smithy/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/Sources/Smithy/Trait/Trait.swift b/Sources/Smithy/Trait/Trait.swift new file mode 100644 index 000000000..92f75e34b --- /dev/null +++ b/Sources/Smithy/Trait/Trait.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// An interface for working with Smithy traits stored in a ``TraitCollection``. +/// +/// All traits have a ``Node`` associated with them, but will typically provide their properties +/// with convenient, type-safe accessors. +public protocol Trait { + static var id: ShapeID { get } + + var node: Node { get } + + init(node: Node) throws +} + +public extension Trait { + + var id: ShapeID { Self.id } +} diff --git a/Sources/Smithy/Trait/TraitCollection.swift b/Sources/Smithy/Trait/TraitCollection.swift new file mode 100644 index 000000000..392d36bd6 --- /dev/null +++ b/Sources/Smithy/Trait/TraitCollection.swift @@ -0,0 +1,91 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// A container for traits that allows for type-safe access. +public struct TraitCollection: Sendable, Hashable { + /// The "raw" traits in this collection, as a dictionary of `Node`s keyed by trait shape ID. + public var traitDict: [ShapeID: Node] + + public init() { + self.traitDict = [:] + } + + public init(traits: [ShapeID: Node]) { + self.traitDict = traits + } + + /// Whether the trait collection is empty. + public var isEmpty: Bool { + traitDict.isEmpty + } + + /// The number of traits in the collection. + public var count: Int { + traitDict.count + } + + /// Checks if a trait collection has a trait, by trait type. + /// - Parameter type: The trait type to be checked. + /// - Returns: `true` if the collection has a trait for the passed trait, `false` otherwise. + public func hasTrait(_ type: T.Type) -> Bool { + traitDict[T.id] != nil + } + + /// Checks if a trait collection has a trait, by ID. + /// - Parameter id: The trait ID to be checked. + /// - Returns: `true` if the collection has a trait for the passed Shape ID, `false` otherwise. + public func hasTrait(_ id: ShapeID) -> Bool { + traitDict[id] != nil + } + + /// Gets a trait from the collection. + /// - Parameter type: The trait to be retrieved. + /// - Returns: The requested trait, or `nil` if the collection doesn't have that trait. + public func getTrait(_ type: T.Type) throws -> T? { + guard let node = traitDict[T.id] else { return nil } + return try T(node: node) + } + + /// Adds a new trait to the collection, overwriting an existing, matching trait. + /// - Parameter trait: The trait to add to the collection. + public mutating func add(_ trait: Trait) { + traitDict[trait.id] = trait.node + } + + /// Combines two trait collections into a single collection. + /// - Parameter other: The trait collection to merge. Traits in this collection overwrite the other. + /// - Returns: The merged ``TraitCollection``. + public func adding(_ other: TraitCollection) -> TraitCollection { + let combined = self.traitDict.merging(other.traitDict) { _, new in new } + return TraitCollection(traits: combined) + } + + /// Returns a trait collection containing only this collection's traits that belong in a schema. + public var schemaTraits: TraitCollection { + let schemaTraitDict = traitDict.filter { (shapeID, _) in allSupportedTraits.contains(shapeID) } + return Self(traits: schemaTraitDict) + } +} + +/// Allows for the creation of a ``TraitCollection`` from a `[ShapeID: Node]` dictionary literal. +extension TraitCollection: ExpressibleByDictionaryLiteral { + public typealias Key = ShapeID + public typealias Value = Node + + public init(dictionaryLiteral elements: (Key, Value)...) { + self.traitDict = Dictionary(uniqueKeysWithValues: elements) + } +} + +/// Allows for the creation of a ``TraitCollection`` from a `[Trait]` array literal. +extension TraitCollection: ExpressibleByArrayLiteral { + public typealias ArrayLiteralElement = any Trait + + public init(arrayLiteral elements: ArrayLiteralElement...) { + self.init(traits: Dictionary(uniqueKeysWithValues: elements.map { ($0.id, $0.node) })) + } +} diff --git a/Sources/Smithy/Trait/TraitError.swift b/Sources/Smithy/Trait/TraitError.swift new file mode 100644 index 000000000..b615932f7 --- /dev/null +++ b/Sources/Smithy/Trait/TraitError.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Used to throw errors relating to trait-related anomalies. +public struct TraitError: Error { + public let localizedDescription: String + + public init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift b/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift new file mode 100644 index 000000000..d5bdce3ec --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AWSQueryCompatibleTrait.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsquerycompatible-trait +public struct AWSQueryCompatibleTrait: Trait { + public static var id: ShapeID { .init("aws.protocols", "awsQueryCompatible") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift b/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift new file mode 100644 index 000000000..a96ff5874 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AWSQueryErrorTrait.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsqueryerror-trait +public struct AWSQueryErrorTrait: Trait { + public static var id: ShapeID { .init("aws.protocols", "awsQueryError") } + + public let node: Node + public let code: String + public let httpResponseCode: Int + + public init(node: Node) throws { + guard case .object(let object) = node else { + throw TraitError("AWSQueryError trait does not have root object") + } + guard case .string(let code) = object["code"] else { + throw TraitError("AWSQueryError trait does not have code") + } + guard case .number(let httpResponseCode) = object["httpResponseCode"] else { + throw TraitError("AWSQueryError trait does not have httpResponseCode") + } + self.node = node + self.code = code + self.httpResponseCode = Int(httpResponseCode) + } +} diff --git a/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift b/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift new file mode 100644 index 000000000..428a6fbfb --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AddedDefaultTrait.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#addeddefault-trait +public struct AddedDefaultTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "addedDefault") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift new file mode 100644 index 000000000..d128909c9 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/AllSupportedTraits.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// The trait IDs that should be copied into schemas. Other traits are omitted for brevity. +/// +/// This list can be expanded as features are added to Smithy/SDK that use them. +public let allSupportedTraits = Set([ + + // Traits defined in Smithy + ServiceTrait.id, + AddedDefaultTrait.id, + AWSQueryCompatibleTrait.id, + AWSQueryErrorTrait.id, + ClientOptionalTrait.id, + DefaultTrait.id, + EnumValueTrait.id, + ErrorTrait.id, + InputTrait.id, + OutputTrait.id, + SensitiveTrait.id, + SparseTrait.id, + + // Synthetic traits + TargetsUnitTrait.id, +]) diff --git a/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift b/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift new file mode 100644 index 000000000..35433a99b --- /dev/null +++ b/Sources/Smithy/TraitLibrary/ClientOptionalTrait.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#clientoptional-trait +public struct ClientOptionalTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "clientOptional") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/Smithy/TraitLibrary/DefaultTrait.swift b/Sources/Smithy/TraitLibrary/DefaultTrait.swift new file mode 100644 index 000000000..5d3699c41 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/DefaultTrait.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#default-trait +public struct DefaultTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "default") } + + public let node: Node + + public init(node: Node) throws { + self.node = node + } + + public init(_ bool: Bool) { + self.node = .boolean(bool) + } + + public init(_ integer: Int) { + self.node = .number(Double(integer)) + } + + public init(_ double: Double) { + self.node = .number(double) + } +} diff --git a/Sources/Smithy/TraitLibrary/EnumValueTrait.swift b/Sources/Smithy/TraitLibrary/EnumValueTrait.swift new file mode 100644 index 000000000..df3ee666f --- /dev/null +++ b/Sources/Smithy/TraitLibrary/EnumValueTrait.swift @@ -0,0 +1,25 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#enumvalue-trait +public struct EnumValueTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "enumValue") } + + public var node: Node + + public init(node: Node) throws { + self.node = node + } + + public init(value: String) { + self.node = .string(value) + } + + public init(value: Int) { + self.node = .number(Double(value)) + } +} diff --git a/Sources/Smithy/TraitLibrary/ErrorTrait.swift b/Sources/Smithy/TraitLibrary/ErrorTrait.swift new file mode 100644 index 000000000..61cb6fb62 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/ErrorTrait.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#error-trait +public struct ErrorTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "error") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/InputTrait.swift b/Sources/Smithy/TraitLibrary/InputTrait.swift new file mode 100644 index 000000000..8763018d4 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/InputTrait.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#input-trait +public struct InputTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "input") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/Smithy/TraitLibrary/OutputTrait.swift b/Sources/Smithy/TraitLibrary/OutputTrait.swift new file mode 100644 index 000000000..ff6e91b1f --- /dev/null +++ b/Sources/Smithy/TraitLibrary/OutputTrait.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#smithy-api-output-trait +public struct OutputTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "output") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/Smithy/TraitLibrary/SensitiveTrait.swift b/Sources/Smithy/TraitLibrary/SensitiveTrait.swift new file mode 100644 index 000000000..3b311dfce --- /dev/null +++ b/Sources/Smithy/TraitLibrary/SensitiveTrait.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/documentation-traits.html#smithy-api-sensitive-trait +public struct SensitiveTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "sensitive") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/ServiceTrait.swift b/Sources/Smithy/TraitLibrary/ServiceTrait.swift new file mode 100644 index 000000000..e6f40c6bd --- /dev/null +++ b/Sources/Smithy/TraitLibrary/ServiceTrait.swift @@ -0,0 +1,25 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/aws/aws-core.html#aws-api-service-trait +public struct ServiceTrait: Trait { + public static var id: ShapeID { .init("aws.api", "service") } + + public let node: Node + public let sdkId: String + + public init(node: Node) throws { + guard case .object(let object) = node else { + throw TraitError("ServiceTrait does not have root object") + } + guard let sdkId = object["sdkId"]?.string else { + throw TraitError("ServiceTrait does not have sdkId") + } + self.node = node + self.sdkId = sdkId + } +} diff --git a/Sources/Smithy/TraitLibrary/SparseTrait.swift b/Sources/Smithy/TraitLibrary/SparseTrait.swift new file mode 100644 index 000000000..ab8b73284 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/SparseTrait.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// https://smithy.io/2.0/spec/type-refinement-traits.html#sparse-trait +public struct SparseTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "sparse") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift b/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift new file mode 100644 index 000000000..30c0cc9c3 --- /dev/null +++ b/Sources/Smithy/TraitLibrary/TargetsUnitTrait.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// A synthetic trait that is used to identify operation inputs & outputs that were either left +/// undefined or that targeted `smithy.api#Unit`. +/// +/// This trait is applied using a model transform, prior to code generation. +public struct TargetsUnitTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "targetsUnit") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init () {} +} diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift new file mode 100644 index 000000000..70f12cdd6 --- /dev/null +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -0,0 +1,86 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ArgumentParser +import Foundation +import struct SmithyCodegenCore.CodeGenerator + +@main +struct SmithyCodegenCLI: AsyncParsableCommand { + + @Argument(help: "The shape ID of the service to be code-generated. Must exist in the model file.") + var service: String + + @Argument(help: "The full or relative path to read the JSON AST model input file.") + var modelPath: String + + @Option(help: "The full or relative path to write the Schemas output file.") + var schemasPath: String? + + @Option(help: "The full or relative path to write the Serialize output file.") + var serializePath: String? + + @Option(help: "The full or relative path to write the Deserialize output file.") + var deserializePath: String? + + func run() async throws { + + let start = Date() + + let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() + + // 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)") + } + + // If --schemas-path was supplied, create the schema file URL + let schemasFileURL = resolve(path: schemasPath) + + // If --serialize-path was supplied, create the Serialize file URL + let serializeFileURL = resolve(path: serializePath) + + // If --deserialize-path was supplied, create the Deserialize file URL + let deserializeFileURL = resolve(path: deserializePath) + + // Use resolved file URLs to run code generator + try CodeGenerator( + service: service, + modelFileURL: modelFileURL, + schemasFileURL: schemasFileURL, + serializeFileURL: serializeFileURL, + deserializeFileURL: deserializeFileURL + ).run() + + let duration = Date().timeIntervalSince(start) + let secondsDuration = String( + format: "%0.2f", + locale: Locale(identifier: "en_US_POSIX"), + arguments: [duration] + ) + print("Completed generating model \(modelFileURL.lastPathComponent) in \(secondsDuration) sec") + } + + private func currentWorkingDirectoryFileURL() -> URL { + // 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(path: String?) -> URL? { + guard let path else { return nil } + return URL(fileURLWithPath: path, relativeTo: currentWorkingDirectoryFileURL()) + } +} + +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..550d7db28 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTError.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Used to raise errors while reading or processing the JSON AST file. +public struct ASTError: Error { + public let localizedDescription: String + + 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..6602103b2 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// 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: Node]? +} diff --git a/Sources/SmithyCodegenCore/AST/ASTModel.swift b/Sources/SmithyCodegenCore/AST/ASTModel.swift new file mode 100644 index 000000000..c3c13d8ee --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// 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: Node? + let shapes: [String: ASTShape] +} diff --git a/Sources/SmithyCodegenCore/AST/ASTReference.swift b/Sources/SmithyCodegenCore/AST/ASTReference.swift new file mode 100644 index 000000000..4cdce691a --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTReference.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/json-ast.html#ast-shape-reference +struct ASTReference: Decodable { + let target: String +} diff --git a/Sources/SmithyCodegenCore/AST/ASTShape.swift b/Sources/SmithyCodegenCore/AST/ASTShape.swift new file mode 100644 index 000000000..93d5ea0da --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// 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 +// Mixins are omitted because they are applied by Smithy before +// the AST is generated. +struct ASTShape: Decodable { + let type: ASTType + let traits: [String: Node]? + let member: ASTMember? + let key: ASTMember? + let value: ASTMember? + let members: [String: ASTMember]? + let version: String? + let operations: [ASTReference]? + let resources: [ASTReference]? + let errors: [ASTReference]? + let rename: [String: String]? + let identifiers: [String: ASTReference]? + let properties: [String: ASTReference]? + let create: ASTReference? + let put: ASTReference? + let read: ASTReference? + let update: ASTReference? + let delete: ASTReference? + let list: ASTReference? + let collectionOperations: [ASTReference]? + let input: ASTReference? + let output: ASTReference? +} diff --git a/Sources/SmithyCodegenCore/AST/ASTType.swift b/Sources/SmithyCodegenCore/AST/ASTType.swift new file mode 100644 index 000000000..adf1e6b52 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/ASTType.swift @@ -0,0 +1,38 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// See https://smithy.io/2.0/spec/model.html#shape-types +enum ASTType: String, Decodable { + // These cases are all the standard Smithy shape types + case blob + case boolean + case string + case timestamp + case byte + case short + case integer + case long + case float + case document + case double + case bigDecimal + case bigInteger + case `enum` + case intEnum + case list + case set + case map + case structure + case union + case member + case service + case resource + case operation + + // Special for AST, added 'apply' case + case apply +} diff --git a/Sources/SmithyCodegenCore/AST/Node+AST.swift b/Sources/SmithyCodegenCore/AST/Node+AST.swift new file mode 100644 index 000000000..77acfa221 --- /dev/null +++ b/Sources/SmithyCodegenCore/AST/Node+AST.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node + +// The Node type itself is defined at ``Smithy.Node``. That type is extended here +// with `Decodable` conformance so it can be read directly into memory from JSON. +// See https://smithy.io/2.0/spec/model.html#trait-node-values in the Smithy docs. +extension Node: Decodable { + + public init(from decoder: any Decoder) throws { + 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([Node].self) { + self = .list(array) + } 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 new file mode 100644 index 000000000..5be1e87dd --- /dev/null +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -0,0 +1,75 @@ +// +// 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 +import struct Smithy.ShapeID + +/// The wrapper for Swift-native code generation. +public struct CodeGenerator { + let service: String + let modelFileURL: URL + let schemasFileURL: URL? + let serializeFileURL: URL? + let deserializeFileURL: URL? + + /// Creates a code generator. + /// - Parameters: + /// - service: The absolute shape ID of the service to be generated. A service with this ID must exist in the model. + /// - modelFileURL: The file URL where the JSON AST model file can be accessed. + /// - schemasFileURL: The file URL to which the `Schemas.swift` source file should be written. + public init( + service: String, + modelFileURL: URL, + schemasFileURL: URL?, + serializeFileURL: URL?, + deserializeFileURL: URL? + ) throws { + self.service = service + self.modelFileURL = modelFileURL + self.schemasFileURL = schemasFileURL + self.serializeFileURL = serializeFileURL + self.deserializeFileURL = deserializeFileURL + } + + /// Executes the code generator. + /// + /// The model is loaded and processed, then Swift source files are generated and written to the specified URL(s). + public func run() throws { + // Load the AST from the model file + let modelData = try Data(contentsOf: modelFileURL) + let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) + + // Create the service's ShapeID + let serviceID = try ShapeID(service) + + // Create the model from the AST + let model = try Model(astModel: astModel) + + // Create a generation context from the model + let ctx = try GenerationContext(serviceID: serviceID, model: model) + + // If a schemas file URL was provided, generate it + if let schemasFileURL { + let schemasContents = try SchemasCodegen().generate(ctx: ctx) + try Data(schemasContents.utf8).write(to: schemasFileURL) + } + + // If a Serialize file URL was provided, generate it + if let serializeFileURL { + let serializeContents = try SerializeCodegen().generate(ctx: ctx) + try Data(serializeContents.utf8).write(to: serializeFileURL) + } + + // If a Deserialize file URL was provided, generate it + if let deserializeFileURL { + let deserializeContents = try DeserializeCodegen().generate(ctx: ctx) + try Data(deserializeContents.utf8).write(to: deserializeFileURL) + } + } +} diff --git a/Sources/SmithyCodegenCore/CodegenError.swift b/Sources/SmithyCodegenCore/CodegenError.swift new file mode 100644 index 000000000..1b11bba70 --- /dev/null +++ b/Sources/SmithyCodegenCore/CodegenError.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// An error that is thrown for general code generation problems. +public struct CodegenError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift b/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift new file mode 100644 index 000000000..625976c83 --- /dev/null +++ b/Sources/SmithyCodegenCore/Deserialize/DeserializeCodegen.swift @@ -0,0 +1,185 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ErrorTrait +import struct Smithy.SparseTrait + +package struct DeserializeCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import Foundation") + writer.write("import enum Smithy.ByteStream") + writer.write("import struct Smithy.Document") + writer.write("import enum Smithy.Prelude") + writer.write("import struct Smithy.Schema") + writer.write("import protocol SmithySerialization.DeserializableStruct") + writer.write("import typealias SmithySerialization.ReadStructConsumer") + writer.write("import protocol SmithySerialization.ShapeDeserializer") + writer.write("") + + // Get structs & unions that are part of an operation output. + // These will all get a DeserializableShape conformance rendered. + let outputStructsAndUnions = try ctx.service + .outputDescendants + .filter { $0.type == .structure || $0.type == .union } + .smithySorted() + + // Render a DeserializableStruct conformance for every struct & union. + for shape in outputStructsAndUnions { + let swiftType = try ctx.symbolProvider.swiftType(shape: shape) + let varName = shape.type == .structure ? "structure" : "union" + try writer.openBlock("extension \(swiftType): SmithySerialization.DeserializableStruct {", "}") { writer in + writer.write("") + let deserializerType = "any SmithySerialization.ShapeDeserializer" + try writer.openBlock( + "public static func deserialize(_ deserializer: \(deserializerType)) throws -> Self {", "}" + ) { writer in + let initializer = shape.type == .structure ? "()" : ".sdkUnknown(\"\")" + writer.write("var \(varName) = Self\(initializer)") + let schemaVarName = try shape.schemaVarName + writer.write("try deserializer.readStruct(\(schemaVarName), &\(varName))") + writer.write("return \(varName)") + } + writer.write("") + let consumerType = "SmithySerialization.ReadStructConsumer" + try writer.openBlock( + "public static var readConsumer: \(consumerType) {", "}") { writer in + try writer.openBlock("{ memberSchema, \(varName), deserializer in", "}") { writer in + try writer.openBlock("switch memberSchema.index {", "}") { writer in + writer.dedent() + let isNonStreaming = !shape.hasTrait(StreamingTrait.self) + let nonErrorMembers = try members(of: shape) + .filter { try isNonStreaming || !$0.target.hasTrait(ErrorTrait.self) } + for (index, member) in nonErrorMembers.enumerated() { + writer.write("case \(index):") + writer.indent() + try writeDeserializeCall( + ctx: ctx, + writer: writer, + shape: shape, + member: member, + schemaVarName: "memberSchema" + ) + writer.dedent() + } + writer.write("default: break") + writer.indent() + } + } + } + } + writer.write("") + } + writer.unwrite("\n") + return writer.contents + } + + private func writeDeserializeCall( + ctx: GenerationContext, + writer: SwiftWriter, + shape: Shape, + member: MemberShape, + schemaVarName: String + ) throws { + let target = try member.target + let propertySwiftType = try ctx.symbolProvider.swiftType(shape: target) + switch try member.target.type { + case .structure, .union: + let readMethodName = try target.deserializeMethodName + if target.type == .union && target.hasTrait(StreamingTrait.self) { + let streamType = "AsyncThrowingStream<\(propertySwiftType), any Swift.Error>" + writer.write("let value: \(streamType) = try deserializer.\(readMethodName)(\(schemaVarName))") + } else { + let initializer = target.type == .structure ? "()" : ".sdkUnknown(\"\")" + writer.write("var value = \(propertySwiftType)\(initializer)") + writer.write("try deserializer.\(readMethodName)(\(schemaVarName), &value)") + } + case .list, .set: + guard let listShape = target as? ListShape else { + throw SymbolProviderError("Shape has type .list but is not a ListShape") + } + let isSparse = listShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "readSparseList" : "readList" + try writer.openBlock( + "let value: \(propertySwiftType) = try deserializer.\(methodName)(\(schemaVarName)) { deserializer in", + "}" + ) { writer in + try writeDeserializeCall( + ctx: ctx, + writer: writer, + shape: listShape, + member: listShape.member, + schemaVarName: "\(schemaVarName).target!.member" + ) + } + case .map: + guard let mapShape = target as? MapShape else { + throw SymbolProviderError("Shape has type .map but is not a MapShape") + } + let isSparse = mapShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "readSparseMap" : "readMap" + try writer.openBlock( + "let value: \(propertySwiftType) = try deserializer.\(methodName)(\(schemaVarName)) { deserializer in", + "}" + ) { writer in + try writeDeserializeCall( + ctx: ctx, + writer: writer, + shape: mapShape, + member: mapShape.value, + schemaVarName: "\(schemaVarName).target!.value" + ) + } + default: + let methodName = try target.deserializeMethodName + writer.write("let value: \(propertySwiftType) = try deserializer.\(methodName)(\(schemaVarName))") + } + try writeAssignment(ctx: ctx, writer: writer, shape: shape, member: member) + } + + private func writeAssignment( + ctx: GenerationContext, + writer: SwiftWriter, + shape: Shape, + member: MemberShape + ) throws { + let target = try member.target + + // The assignment being written is based on the shape enclosing the member. + switch shape.type { + case .structure: + // For a structure member, write the value to the appropriate structure property, + // making the appropriate adjustment for an error. + let properties = shape.hasTrait(ErrorTrait.self) ? "properties." : "" + let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id) + writer.write("structure.\(properties)\(propertyName) = value") + case .union: + if target.hasTrait(ErrorTrait.self) && shape.hasTrait(StreamingTrait.self) { + // For an event stream error, throw it + writer.write("throw value as! Swift.Error") + } else { + // For a union member or event stream event, write the appropriate union case to the union variable + let enumCaseName = try ctx.symbolProvider.enumCaseName(shapeID: member.id) + writer.write("union = .\(enumCaseName)(value)") + } + case .list, .set, .map: + // For a collection member, return it to the caller since this is being written + // into a consumer block that returns the collection element. + writer.write("return value") + default: + throw CodegenError("Unsupported shape type \(shape.type) for rendering member deserialize") + } + } + + private func members(of shape: Shape) throws -> [MemberShape] { + guard let hasMembers = shape as? HasMembers else { return [] } + return try hasMembers.members + } +} diff --git a/Sources/SmithyCodegenCore/Deserialize/Shape+DeserializeMethodName.swift b/Sources/SmithyCodegenCore/Deserialize/Shape+DeserializeMethodName.swift new file mode 100644 index 000000000..251cff544 --- /dev/null +++ b/Sources/SmithyCodegenCore/Deserialize/Shape+DeserializeMethodName.swift @@ -0,0 +1,64 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Shape { + + var deserializeMethodName: String { + get throws { + switch type { + case .blob: + if hasTrait(StreamingTrait.self) { + return "readDataStream" + } else { + return "readBlob" + } + case .boolean: + return "readBoolean" + case .string: + return "readString" + case .enum: + return "readEnum" + case .timestamp: + return "readTimestamp" + case .byte: + return "readByte" + case .short: + return "readShort" + case .integer: + return "readInteger" + case .intEnum: + return "readIntEnum" + case .long: + return "readLong" + case .float: + return "readFloat" + case .document: + return "readDocument" + case .double: + return "readDouble" + case .bigDecimal: + return "readBigDecimal" + case .bigInteger: + return "readBigInteger" + case .list, .set: + return "readList" + case .map: + return "readMap" + case .structure, .union: + if hasTrait(StreamingTrait.self) { + return "readEventStream" + } else { + return "readStruct" + } + case .member, .service, .resource, .operation: + throw ModelError("Cannot serialize type \(type)") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift new file mode 100644 index 000000000..f873ef7b1 --- /dev/null +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +/// A type that provides the resources needed to perform Swift code generation. +public struct GenerationContext { + public let service: ServiceShape + public let model: Model + public let symbolProvider: SymbolProvider + + /// Creates a ``GenerationContext`` from a model. + /// + /// The model must contain a service with the passed service ID. + /// - Parameter serviceID: The ``ShapeID`` for the service the model should be pruned to. + /// - Parameter model: The ``Model`` to create the generation context from. + /// - Throws: ``ModelError`` if the model does not contain exactly one service. + init(serviceID: ShapeID, model: Model) throws { + + // Perform model transformations here + let finalModel = try model + .withSynthesizedInputsOutputs() + .withDeprecatedShapesRemoved() + .withUnionsTargetingUnitAdded() + .optionalizeStructMembers(serviceID: serviceID) + .prune(serviceID: serviceID) + + // Initialize using the final, processed model + self.service = try finalModel.expectServiceShape(id: serviceID) + self.model = finalModel + self.symbolProvider = SymbolProvider(service: service, model: finalModel) + } +} diff --git a/Sources/SmithyCodegenCore/HasShapeID.swift b/Sources/SmithyCodegenCore/HasShapeID.swift new file mode 100644 index 000000000..db766a4a9 --- /dev/null +++ b/Sources/SmithyCodegenCore/HasShapeID.swift @@ -0,0 +1,34 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +/// A protocol for types that are identified by a Shape ID. +/// +/// Primary purpose is to facilitate sorting, filtering, etc. +protocol HasShapeID { + var id: ShapeID { get } +} + +extension Array where Element: HasShapeID { + + /// Sorts alphabetically by shape ID to match the ordering used by Kotlin-based codegen. + /// + /// The comparator on `ShapeID` implements the actual comparison logic. + /// - Returns: An array of elements in sorted order + func smithySorted() -> Self { + sorted { $0.id < $1.id } + } +} + +extension ShapeID: HasShapeID { + + /// ShapeID itself is made to conform to ``HasShapeID``. + var id: ShapeID { + self + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift new file mode 100644 index 000000000..01c87cbcc --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -0,0 +1,212 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.EnumValueTrait +import enum Smithy.Node +import enum Smithy.Prelude +import struct Smithy.ShapeID +import struct Smithy.TraitCollection + +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 create a `Model` from. + convenience init(astModel: ASTModel) throws { + // Get all of the members from the AST model, create pairs of ShapeID & MemberShape + let idToMemberShapePairs = try astModel.shapes + .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 with the shape dictionary + self.init(version: astModel.smithy, metadata: astModel.metadata, shapes: shapes) + } + + 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 + } + + // If this shape is a string with the enum trait, add members for its trait members + if astShape.type == .string, let enumTraitNode = astShape.traits?[EnumTrait.id.absolute] { + let enumTrait = try EnumTrait(node: enumTraitNode) + let unitID = Smithy.Prelude.unitSchema.id.absolute + enumTrait.members.forEach { enumMember in + let name = enumMember.name ?? enumMember.value + let traits: [String: Node] = if enumMember.name != nil { + [EnumValueTrait.id.absolute: .string(enumMember.value)] + } else { + [:] + } + baseMembers[name] = ASTMember(target: unitID, traits: traits) + } + } + + // Map the AST members to ShapeID-to-MemberShape pairs & return the list of pairs + return try baseMembers.map { astMember in + // Create a ShapeID for this member + 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 traitDict = Dictionary(uniqueKeysWithValues: traitPairs ?? []) + let traits = TraitCollection(traits: traitDict) + + // Create a Shape ID for this member's target + let targetID = try ShapeID(astMember.value.target) + + // Create the ShapeID-to-MemberShape pair + return (memberID, MemberShape(id: memberID, traits: traits, targetID: targetID)) + } + } + + // swiftlint:disable:next function_body_length + 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 traitDict = Dictionary(uniqueKeysWithValues: idToTraitPairs) + let traits = TraitCollection(traits: traitDict) + + // Based on the AST shape type, create the appropriate Shape type. + switch astShape.type { + case .service: + let shape = ServiceShape( + id: shapeID, + traits: traits, + operationIDs: try astShape.operations?.map { try $0.id } ?? [], + resourceIDs: try astShape.resources?.map { try $0.id } ?? [], + errorIDs: try astShape.errors?.map { try $0.id } ?? [] + ) + return (shapeID, shape) + case .resource: + let shape = ResourceShape( + id: shapeID, + traits: traits, + operationIDs: try astShape.operations?.map { try $0.id } ?? [], + createID: try astShape.create?.id, + putID: try astShape.put?.id, + readID: try astShape.read?.id, + updateID: try astShape.update?.id, + deleteID: try astShape.delete?.id, + listID: try astShape.list?.id + ) + return (shapeID, shape) + case .operation: + let shape = OperationShape( + id: shapeID, + traits: traits, + inputID: try astShape.input?.id, + outputID: try astShape.output?.id, + errorIDs: try astShape.errors?.map { try $0.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) + case .string: + if traits.hasTrait(EnumTrait.self) { + // The enum trait is a holdover from Smithy 1.0 and is deprecated in favor of + // the enum shape. + // If this is a String with enum trait, convert it to a EnumShape. + let shape = EnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + } else { + let shape = StringShape(id: shapeID, traits: traits) + return (shapeID, shape) + } + case .integer: + let shape = IntegerShape(id: shapeID, traits: traits) + return (shapeID, shape) + default: + let shape = Shape( + id: shapeID, + type: try astShape.type.modelType, + traits: traits + ) + + // Return the ShapeID-to-Shape pair. + return (shapeID, shape) + } + } + + private static func memberIDs(for shapeID: ShapeID, memberShapes: [ShapeID: MemberShape]) -> [ShapeID] { + // Given all the member shapes in this model, select the ones for the passed shape ID + // and return their IDs in sorted order. + memberShapes.keys.filter { + $0.namespace == shapeID.namespace && $0.name == shapeID.name && $0.member != nil + }.sorted() + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift new file mode 100644 index 000000000..5464f6dcd --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -0,0 +1,126 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +/// An in-memory representation of a Smithy model, suitable for use in code generation. +public class Model { + + /// The Smithy version that this model conforms to. This type supports `1.0` and `2.0`. + public let version: String + + /// The model metadata. + public let metadata: Node? + + /// All shapes in the model, keyed by their absolute ShapeID. + public let shapes: [ShapeID: Shape] + + /// An array of all shapes in the model, in Smithy-sorted order. + public let allShapesSorted: [Shape] + + /// Creates a new model. + /// + /// When a ``Model`` is created from another model, the shapes are updated to point to the new model + /// for reference resolution. The old model should no longer be used once a new model is created from it. + /// - Parameters: + /// - version: The Smithy version that the model conforms to. + /// - metadata: The model metadata. + /// - shapes: A dictionary of all shapes in the model, keyed by their absolute Shape IDs. + init(version: String, metadata: Node?, shapes: [ShapeID: Shape]) { + self.version = version + self.metadata = metadata + self.shapes = shapes + + // Sort the shapes in the dictionary & store them in an array. + self.allShapesSorted = Array(shapes.values).smithySorted() + + // self is now fully initialized. + // Set the model on each shape to self. + shapes.values.forEach { $0.model = self } + } + + /// Returns the shape for the passed ID. Throws if the shape is not present. + /// - Parameter id: The ShapeID for the shape to be retrieved. + /// - Returns: The retrieved shape. Throws if the shape is not found. + func expectShape(id: ShapeID) throws -> Shape { + guard let shape = shapes[id] else { + throw ModelError("ShapeID \(id) was expected in model but not found") + } + return shape + } + + /// Returns the service for the passed ID. Throws if the shape is not present or not a ``ServiceShape``. + /// - Parameter id: The ShapeID for the service to be retrieved. + /// - Returns: The retrieved shape. Throws if the shape is not found or not a ``ServiceShape``. + func expectServiceShape(id: ShapeID) throws -> ServiceShape { + guard let shape = try expectShape(id: id) as? ServiceShape else { + throw ModelError("ShapeID \(id) is not a ServiceShape") + } + return shape + } + + /// Returns the resource for the passed ID. Throws if the shape is not present or not a ``ResourceShape``. + /// - Parameter id: The ShapeID for the resource to be retrieved. + /// - Returns: The retrieved resource. Throws if the shape is not found or not a ``ResourceShape``. + func expectResourceShape(id: ShapeID) throws -> ResourceShape { + guard let shape = try expectShape(id: id) as? ResourceShape else { + throw ModelError("ShapeID \(id) is not a ResourceShape") + } + return shape + } + + /// Returns the operation for the passed ID. Throws if the shape is not present or not a ``OperationShape``. + /// - Parameter id: The ShapeID for the operation to be retrieved. + /// - Returns: The retrieved operation. Throws if the shape is not found or not a ``OperationShape``. + func expectOperationShape(id: ShapeID) throws -> OperationShape { + guard let shape = try expectShape(id: id) as? OperationShape else { + throw ModelError("ShapeID \(id) is not a OperationShape") + } + return shape + } + + /// Returns the structure for the passed ID. Throws if the shape is not present or not a ``StructureShape``. + /// - Parameter id: The ShapeID for the structure to be retrieved. + /// - Returns: The retrieved structure. Throws if the shape is not found or not a ``StructureShape``. + func expectStructureShape(id: ShapeID) throws -> StructureShape { + guard let shape = try expectShape(id: id) as? StructureShape else { + throw ModelError("ShapeID \(id) is not a StructureShape") + } + return shape + } + + /// Returns the list for the passed ID. Throws if the shape is not present or not a ``ListShape``. + /// - Parameter id: The ShapeID for the list to be retrieved. + /// - Returns: The retrieved list. Throws if the shape is not found or not a ``ListShape``. + func expectListShape(id: ShapeID) throws -> ListShape { + guard let shape = try expectShape(id: id) as? ListShape else { + throw ModelError("ShapeID \(id) is not a ListShape") + } + return shape + } + + /// Returns the map for the passed ID. Throws if the shape is not present or not a ``MapShape``. + /// - Parameter id: The ShapeID for the map to be retrieved. + /// - Returns: The retrieved map. Throws if the shape is not found or not a ``MapShape``. + func expectMapShape(id: ShapeID) throws -> MapShape { + guard let shape = try expectShape(id: id) as? MapShape else { + throw ModelError("ShapeID \(id) is not a MapShape") + } + return shape + } + + /// Returns the member for the passed ID. Throws if the shape is not present or not a ``MemberShape``. + /// - Parameter id: The ShapeID for the member to be retrieved. + /// - Returns: The retrieved member. Throws if the shape is not found or not a ``MemberShape``. + func expectMemberShape(id: ShapeID) throws -> MemberShape { + guard let shape = try expectShape(id: id) as? MemberShape else { + throw ModelError("ShapeID \(id) is not a MemberShape") + } + return shape + } +} diff --git a/Sources/SmithyCodegenCore/Model/ModelError.swift b/Sources/SmithyCodegenCore/Model/ModelError.swift new file mode 100644 index 000000000..b0b76c3ea --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ModelError.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Used to throw errors related to a Smithy model. +public struct ModelError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift new file mode 100644 index 000000000..d860cf7a0 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension ASTReference { + + /// Convenience accessor to create a ``ShapeID`` from an ``ASTReference``. + var id: ShapeID { + get throws { + try ShapeID(target) + } + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift new file mode 100644 index 000000000..53e664734 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -0,0 +1,69 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.ShapeType + +extension ASTType { + + /// Convenience method to convert ``ASTType`` to a ``Smithy.ShapeType``. + var modelType: ShapeType { + get throws { + switch self { + case .blob: + return .blob + case .boolean: + return .boolean + case .string: + return .string + case .timestamp: + return .timestamp + case .byte: + return .byte + case .short: + return .short + case .integer: + return .integer + case .long: + return .long + case .float: + return .float + case .document: + return .document + case .double: + return .double + case .bigDecimal: + return .bigDecimal + case .bigInteger: + return .bigInteger + case .`enum`: + return .`enum` + case .intEnum: + return .intEnum + case .list: + return .list + case .set: + return .set + case .map: + return .map + case .structure: + return .structure + case .union: + return .union + case .member: + return .member + case .service: + return .service + case .resource: + return .resource + case .operation: + return .operation + case .apply: + throw ModelError("\"apply\" AST shapes not implemented") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift new file mode 100644 index 000000000..a496a594b --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Box.swift @@ -0,0 +1,55 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ClientOptionalTrait +import struct Smithy.ShapeID + +extension Model { + + /// Adds the `clientOptional` trait to all structure members in selected services. + /// Reproduces the transform in [BoxServices.kt](https://github.com/awslabs/aws-sdk-swift/blob/main/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/customization/BoxServices.kt). + /// - Parameter serviceID: The ShapeID for the service that is being code generated. + /// - Returns: The transformed model. + func optionalizeStructMembers(serviceID: ShapeID) throws -> Model { + guard affectedServices.contains(serviceID.absolute) else { return self } + + let newShapes = try self.shapes.mapValues { shape in + guard let member = shape as? MemberShape else { return shape } + guard try member.container.type == .structure else { return shape } + return MemberShape( + id: member.id, + traits: member.traits.adding([ClientOptionalTrait()]), + targetID: member.targetID + ) + } + return Model(version: self.version, metadata: self.metadata, shapes: newShapes) + } +} + +private let affectedServices = [ + "com.amazonaws.ec2#AmazonEC2", + "com.amazonaws.nimble#nimble", + "com.amazonaws.amplifybackend#AmplifyBackend", + "com.amazonaws.apigatewaymanagementapi#ApiGatewayManagementApi", + "com.amazonaws.apigatewayv2#ApiGatewayV2", + "com.amazonaws.dataexchange#DataExchange", + "com.amazonaws.greengrass#Greengrass", + "com.amazonaws.iot1clickprojects#AWSIoT1ClickProjects", + "com.amazonaws.kafka#Kafka", + "com.amazonaws.macie2#Macie2", + "com.amazonaws.mediaconnect#MediaConnect", + "com.amazonaws.mediaconvert#MediaConvert", + "com.amazonaws.medialive#MediaLive", + "com.amazonaws.mediapackage#MediaPackage", + "com.amazonaws.mediapackagevod#MediaPackageVod", + "com.amazonaws.mediatailor#MediaTailor", + "com.amazonaws.pinpoint#Pinpoint", + "com.amazonaws.pinpointsmsvoice#PinpointSMSVoice", + "com.amazonaws.serverlessapplicationrepository#ServerlessApplicationRepository", + "com.amazonaws.mq#mq", + "com.amazonaws.schemas#schemas", +] diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift new file mode 100644 index 000000000..4a46c17ea --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Deprecated.swift @@ -0,0 +1,131 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class Foundation.DateFormatter +import struct Foundation.Locale + +extension Model { + + /// Removes all shapes with the `deprecated` trait and a `since` date after `2024-09-17`, the `aws-sdk-swift` + /// GA date. + /// - Returns: The transformed model. + func withDeprecatedShapesRemoved() throws -> Model { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd" + + // The "cutoff date". Shapes deprecated before this date will be removed from the model. + let cutoff = formatter.date(from: "2024-09-17")! + + // Filter the deprecated shapes from the model. + let nonDeprecatedShapes = try shapes.filter { (_, shape) in + + // Keep this shape if it doesn't have a DeprecatedTrait with a `since` value. + guard let since = try shape.getTrait(DeprecatedTrait.self)?.since else { return true } + + // Keep this shape if the `since` value doesn't parse to a yyyy-MM-dd date. + guard let sinceDate = formatter.date(from: since) else { return true } + + // Compare dates, keep the shape if it was deprecated before the cutoff. + return sinceDate > cutoff + } + + var trimmedShapes = nonDeprecatedShapes + var previousTrimmedShapesCount = 0 + + // Now remove any members, lists, and maps that refer to deprecated shapes. + // We repeat this until no additional shapes are removed to ensure that nested + // references to deprecated shapes don't result in an inconsistent model. + repeat { + previousTrimmedShapesCount = trimmedShapes.count + let newTrimmedShapes = try trimmedShapes.filter { (_, shape) in + switch shape { + case let listShape as ListShape: + let id = try listShape.member.targetID + guard id.namespace != "smithy.api" else { return true } + let found = trimmedShapes[id] != nil + return found + case let mapShape as MapShape: + let id = try mapShape.value.targetID + guard id.namespace != "smithy.api" else { return true } + let found = trimmedShapes[id] != nil + return found + case let memberShape as MemberShape: + let id = memberShape.targetID + guard id.namespace != "smithy.api" else { return true } + let found = trimmedShapes[id] != nil + return found + default: + return true + } + } + trimmedShapes = newTrimmedShapes + } while trimmedShapes.count != previousTrimmedShapesCount + + // Finally, go through all the shapes and remove references to removed shapes. + let finalShapes = trimmedShapes.mapValues { shape -> Shape in + switch shape { + case let serviceShape as ServiceShape: + let operationIDs = serviceShape.operationIDs.filter { trimmedShapes[$0] != nil } + let resourceIDs = serviceShape.resourceIDs.filter { trimmedShapes[$0] != nil } + let errorIDs = serviceShape.errorIDs.filter { trimmedShapes[$0] != nil } + return ServiceShape( + id: serviceShape.id, + traits: serviceShape.traits, + operationIDs: operationIDs, + resourceIDs: resourceIDs, + errorIDs: errorIDs + ) + case let resourceShape as ResourceShape: + let operationIDs = resourceShape.operationIDs.filter { trimmedShapes[$0] != nil } + let createID = resourceShape.createID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let putID = resourceShape.putID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let readID = resourceShape.readID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let updateID = resourceShape.updateID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let deleteID = resourceShape.deleteID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + let listID = resourceShape.listID.map { trimmedShapes[$0] != nil ? $0 : nil } ?? nil + return ResourceShape( + id: resourceShape.id, + traits: resourceShape.traits, + operationIDs: operationIDs, + createID: createID, + putID: putID, + readID: readID, + updateID: updateID, + deleteID: deleteID, + listID: listID + ) + case let operationShape as OperationShape: + let errorIDs = operationShape.errorIDs.filter { trimmedShapes[$0] != nil } + return OperationShape( + id: operationShape.id, + traits: operationShape.traits, + inputID: operationShape.inputID, + outputID: operationShape.outputID, + errorIDs: errorIDs + ) + case let structureShape as StructureShape: + let memberIDs = structureShape.memberIDs.filter { trimmedShapes[$0] != nil } + return StructureShape(id: structureShape.id, traits: structureShape.traits, memberIDs: memberIDs) + case let unionShape as UnionShape: + let memberIDs = unionShape.memberIDs.filter { trimmedShapes[$0] != nil } + return UnionShape(id: unionShape.id, traits: unionShape.traits, memberIDs: memberIDs) + case let enumShape as EnumShape: + let memberIDs = enumShape.memberIDs.filter { trimmedShapes[$0] != nil } + return EnumShape(id: enumShape.id, traits: enumShape.traits, memberIDs: memberIDs) + case let intEnumShape as IntEnumShape: + let memberIDs = intEnumShape.memberIDs.filter { trimmedShapes[$0] != nil } + return IntEnumShape(id: intEnumShape.id, traits: intEnumShape.traits, memberIDs: memberIDs) + default: + return shape + } + } + + // Create the transformed model, and return it to the caller. + return Model(version: self.version, metadata: self.metadata, shapes: finalShapes) + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift new file mode 100644 index 000000000..35858609d --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+InputOutput.swift @@ -0,0 +1,98 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import enum Smithy.Prelude +import struct Smithy.ShapeID +import struct Smithy.TargetsUnitTrait +import struct Smithy.TraitCollection + +extension Model { + + /// Creates an empty structure in place of operation inputs & outputs that target `smithy.api#Unit` + /// or that have no target. Applies the `TargetsUnitTrait` to these synthesized structures as well. + /// - Returns: The transformed model. + func withSynthesizedInputsOutputs() throws -> Model { + + // Get the operations in the model + let operations = shapes.values + .filter { $0.type == .operation } + .compactMap { $0 as? OperationShape } + + // Make a copy of this model's shapes to modify + var newShapes = shapes + + for operation in operations { + // Make new "synthetic" ShapeIDs for this operation's input & output + let newInputShapeID = ShapeID("swift.synthetic", "\(operation.id.name)Input") + let newOutputShapeID = ShapeID("swift.synthetic", "\(operation.id.name)Output") + + // Get the input & output structures for this operation. Substitute an empty + // structure if ID is omitted or targets Unit + let inputShape = if operation.inputID == Prelude.unitSchema.id { + StructureShape(id: Prelude.unitSchema.id, traits: [TargetsUnitTrait()], memberIDs: []) + } else { + try expectStructureShape(id: operation.inputID) + } + let outputShape = if operation.outputID == Prelude.unitSchema.id { + StructureShape(id: Prelude.unitSchema.id, traits: [TargetsUnitTrait()], memberIDs: []) + } else { + try expectStructureShape(id: operation.outputID) + } + + // Make new input and output shapes, plus their members, with the new ID + // Add UsedAsInput and UsedAsOutput traits to the input/output structures + // These traits allow us to identify inputs/outputs by trait, but allow us to + // leave the Smithy input & output traits as set on the original model. + let newInput = newStruct(newID: newInputShapeID, newTraits: [UsedAsInputTrait()], original: inputShape) + let newInputShapeMembers = try renamedMembers(newID: newInputShapeID, original: inputShape) + let newOutput = newStruct(newID: newOutputShapeID, newTraits: [UsedAsOutputTrait()], original: outputShape) + let newOutputShapeMembers = try renamedMembers(newID: newOutputShapeID, original: outputShape) + + // Add the new input & output and their members to the new shape dictionary. + // The originals will remain and will be pruned later if they are left unreferenced. + newShapes[newInput.id] = newInput + newInputShapeMembers.forEach { newShapes[$0.id] = $0 } + newShapes[newOutput.id] = newOutput + newOutputShapeMembers.forEach { newShapes[$0.id] = $0 } + + // Make a new operation with the new input & output IDs + let newOperation = OperationShape( + id: operation.id, + traits: operation.traits, + inputID: newInputShapeID, + outputID: newOutputShapeID, + errorIDs: operation.errorIDs + ) + + // Add the new operation to the new shapes. It will replace the original + // since the new operation has the same ID. + newShapes[newOperation.id] = newOperation + } + // Return the new model with the updated shapes. + return Model(version: version, metadata: metadata, shapes: newShapes) + } + + private func newStruct(newID: ShapeID, newTraits: TraitCollection, original: StructureShape) -> StructureShape { + StructureShape( + id: newID, + traits: original.traits.adding(newTraits), + memberIDs: original.memberIDs.map { .init(id: newID, member: $0.member) } + ) + } + + private func renamedMembers(newID: ShapeID, original: StructureShape) throws -> [MemberShape] { + let originalMembers = try original.memberIDs.map { try expectMemberShape(id: $0) } + return originalMembers.map { member in + MemberShape( + id: .init(id: newID, member: member.id.member), + traits: member.traits, + targetID: member.targetID + ) + } + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift new file mode 100644 index 000000000..c14cd7b5b --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+PruneToService.swift @@ -0,0 +1,31 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Model { + + /// Filters out all shapes except for the identified service and its direct & indirect descendants. + /// + /// This eliminates any shapes in the model that are not needed for the service being generated. + /// - Parameter serviceID: The ShapeID for the service that is being generated. + /// - Returns: The transformed model. + func prune(serviceID: ShapeID) throws -> Model { + + // Get the service + let service = try expectServiceShape(id: serviceID) + + // Create a set with the service and its descendants + let shapesForService = try Set([service]).union(service.descendants) + + // Create a dictionary from the set, keyed by ShapeID + let shapeDict = Dictionary(uniqueKeysWithValues: shapesForService.map { ($0.id, $0) }) + + // Create and return the transformed model + return Model(version: self.version, metadata: self.metadata, shapes: shapeDict) + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift b/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift new file mode 100644 index 000000000..2dec0773f --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/Model+Union.swift @@ -0,0 +1,45 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Prelude +import struct Smithy.ShapeID + +extension Model { + + /// smithy-swift mistakenly creates a structure for an enum case with an associated value for a union + /// member that targets `smithy.api#Unit`. To ensure that these structures get SerializableStruct / + /// DeserializableStruct conformance, we replace `smithy.api#Unit` with a structure named `Unit` in the + /// union's namespace. + /// - Returns: The transformed model. + func withUnionsTargetingUnitAdded() throws -> Model { + var unitSubstitute: Shape? + + var newShapes = try shapes.mapValues { shape in + guard let member = shape as? MemberShape else { return shape } + guard try member.container.type == .union else { return member } + guard member.targetID == Smithy.Prelude.unitSchema.id else { return member } + let unitSubstituteID = ShapeID(member.id.namespace, "Unit") + unitSubstitute = unitSubstitute ?? StructureShape( + id: unitSubstituteID, + traits: [:], + memberIDs: [] + ) + let newMember = MemberShape( + id: member.id, + traits: member.traits, + targetID: unitSubstituteID + ) + return newMember + } + + if let unitSubstitute, newShapes[unitSubstitute.id] == nil { + newShapes[unitSubstitute.id] = unitSubstitute + } + + return Model(version: self.version, metadata: self.metadata, shapes: newShapes) + } +} diff --git a/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift b/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift new file mode 100644 index 000000000..0812ddda3 --- /dev/null +++ b/Sources/SmithyCodegenCore/NullableIndex/NullableIndex.swift @@ -0,0 +1,76 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.AddedDefaultTrait +import struct Smithy.ClientOptionalTrait +import struct Smithy.DefaultTrait +import struct Smithy.InputTrait +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.SparseTrait + +struct NullableIndex { + + /// Determines whether a structure member should be rendered as non-optional. + /// - Parameter memberShape: The member for which optionality is being determined + /// - Returns: `true` if the member should be non-optional, `false` otherwise + func isNonOptional(_ memberShape: MemberShape) throws -> Bool { + let container = try memberShape.container + let target = try memberShape.target + + // Note that these are the current rules in use by smithy-swift. They are not Smithy 2.0 "correct". + + // If the container is a list/set, member is nonoptional unless sparse trait is applied + if [ShapeType.list, .set].contains(container.type), memberShape.id.member == "member" { + return !container.hasTrait(SparseTrait.self) + } + + // If the container is a map, value is nonoptional unless sparse trait is applied + if container.type == .map { + if memberShape.id.member == "value" { + return !container.hasTrait(SparseTrait.self) + } else { + // key is always non-optional + return true + } + } + + // If the containing shape has the input trait, it's definitely optional + if container.hasTrait(InputTrait.self) { + return false + } + + // If the member has the clientOptional trait, it's definitely optional + if memberShape.hasTrait(ClientOptionalTrait.self) { + return false + } + + // If the member has the addedDefault trait, it's definitely optional + if memberShape.hasTrait(AddedDefaultTrait.self) { + return false + } + + // Only number & Boolean types are allowed to be non-optional + let allowedTypes = + [ShapeType.boolean, .bigDecimal, .bigInteger, .byte, .double, .float, .intEnum, .integer, .long, .short] + guard allowedTypes.contains(target.type) else { return false } + + // Check if there is a default trait with a zero/false value. If so, member is non-optional. + let memberDefaultTrait = try memberShape.getTrait(DefaultTrait.self) + let targetDefaultTrait = try target.getTrait(DefaultTrait.self) + guard let defaultNode = (memberDefaultTrait ?? targetDefaultTrait)?.node else { + return false + } + if target.type == .boolean, let bool = defaultNode.boolean, !bool { + return true + } else if let number = defaultNode.number, number == 0.0 { + return true + } else { + return false + } + } +} diff --git a/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt b/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt new file mode 100644 index 000000000..8f50fc772 --- /dev/null +++ b/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by SmithyCodegenCLI. DO NOT EDIT! diff --git a/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift new file mode 100644 index 000000000..976096026 --- /dev/null +++ b/Sources/SmithyCodegenCore/Schemas/SchemasCodegen.swift @@ -0,0 +1,134 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import let Smithy.allSupportedTraits +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A generator for the `Schemas.swift` +package struct SchemasCodegen { + + package init() {} + + /// Generates the `Schemas.swift` file for a modeled service. + /// - Parameter ctx: The generation context to be used for codegen. + /// - Returns: The contents of the `Schemas.swift` source file. + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import struct Smithy.Schema") + writer.write("import enum Smithy.Prelude") + writer.write("") + + // Get all operations, sorted + let sortedOperationShapes = ctx.model.allShapesSorted + .filter { $0.type == .operation } + + // Get the rest of the shapes, sorted + let sortedModelShapes: [Shape] = ctx.model.allShapesSorted + .filter { ![ShapeType.service, .resource, .operation, .member].contains($0.type) } + .filter { $0.id.namespace != "smithy.api" } + + // Combine shapes in order: service, operations sorted, models sorted + let allShapes = [ctx.service] + sortedOperationShapes + sortedModelShapes + + for shape in allShapes { + + // Render an internal-scoped, computed var in the global namespace for this schema. + try writer.openBlock("var \(shape.schemaVarName): Smithy.Schema {", "}") { writer in + try writeSchema(writer: writer, shape: shape) + writer.unwrite(",") + } + writer.write("") + + // If a schema has a member that targets the schema itself, we avoid a compile warning for + // self-reference by generating a duplicate schema var that references this schema, and we + // will target the duplicate instead. + // + // This happens ~20 times in AWS models so it is not so frequent that the extra var will bloat + // service clients. + if let hm = shape as? HasMembers, try hm.members.contains(where: { $0.targetID == shape.id }) { + try writer.openBlock("var dup_of_\(shape.schemaVarName): Smithy.Schema {", "}") { writer in + try writer.write(shape.schemaVarName) + } + writer.write("") + } + } + writer.unwrite("\n") + return writer.contents + } + + private func writeSchema(writer: SwiftWriter, shape: Shape, index: Int? = nil) throws { + + // Open the initializer + try writer.openBlock(".init(", "),") { writer in + + // Write the id: and type: params. All schemas will have this + writer.write("id: \(shape.id.rendered),") + writer.write("type: .\(shape.type),") + + // Get the trait IDs for traits that are allow-listed for the schema & sort + let traitIDs = Array(shape.traits.schemaTraits.traitDict.keys).smithySorted() + + // If there are any traits, write the traits: param + if !traitIDs.isEmpty { + writer.openBlock("traits: [", "],") { writer in + for traitID in traitIDs { + let trait = shape.traits.traitDict[traitID]! + writer.write("\(traitID.rendered): \(trait.rendered),") + } + } + } + + // Get the members for this shape + let members = try (shape as? HasMembers)?.members ?? [] + + // If there are any members, write the members param + if !members.isEmpty { + try writer.openBlock("members: [", "],") { writer in + for (index, member) in members.enumerated() { + // Make a recursive call to this method to render the member + try writeSchema(writer: writer, shape: member, index: index) + } + } + } + + // If this shape is a member, write the target: param + if let member = shape as? MemberShape { + let target = try member.target + + // If this schema's target is the same as itself, target the duplicate + // (see above) to avoid a self-reference compile warning. + let prefix = target.id == member.containerID ? "dup_of_" : "" + writer.write(try "target: \(prefix)\(target.schemaVarName),") + } + + // Write the index: param if one was passed. Only members will have an index. + if let index { + writer.write("index: \(index),") + } + + // Get rid of the trailing comma since Swift 5.x will fail to compile on a + // method param trailing comma. + writer.unwrite(",") + } + } +} + +extension ShapeID { + + /// Change the Shape ID into a rendered ShapeID initializer call. + var rendered: String { + let namespaceLiteral = namespace.literal + let nameLiteral = name.literal + if let member { + let memberLiteral = member.literal + return ".init(\(namespaceLiteral), \(nameLiteral), \(memberLiteral))" + } else { + return ".init(\(namespaceLiteral), \(nameLiteral))" + } + } +} diff --git a/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift new file mode 100644 index 000000000..9fcfeb628 --- /dev/null +++ b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift @@ -0,0 +1,61 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Shape { + + var schemaVarName: String { + get throws { + if id.namespace == "smithy.api" { + try id.preludeSchemaVarName + } else { + try id.schemaVarName + } + } + } +} + +private extension ShapeID { + + var preludeSchemaVarName: String { + get throws { + let propertyName = switch name { + case "Unit": "unitSchema" + case "String": "stringSchema" + case "Blob": "blobSchema" + case "Integer": "integerSchema" + case "Timestamp": "timestampSchema" + case "Boolean": "booleanSchema" + case "Float": "floatSchema" + case "Double": "doubleSchema" + case "Long": "longSchema" + case "Short": "shortSchema" + case "Byte": "byteSchema" + case "PrimitiveInteger": "primitiveIntegerSchema" + case "PrimitiveBoolean": "primitiveBooleanSchema" + case "PrimitiveFloat": "primitiveFloatSchema" + case "PrimitiveDouble": "primitiveDoubleSchema" + case "PrimitiveLong": "primitiveLongSchema" + case "PrimitiveShort": "primitiveShortSchema" + case "PrimitiveByte": "primitiveByteSchema" + case "Document": "documentSchema" + default: throw ModelError("Unhandled prelude type converted to schemaVar: \"\(name)\"") + } + return "Smithy.Prelude.\(propertyName)" + } + } + + var schemaVarName: String { + get throws { + guard member == nil else { throw ModelError("Assigning member schema to a var") } + let namespacePortion = namespace.replacingOccurrences(of: ".", with: "_") + let namePortion = name + return "schema__\(namespacePortion)__\(namePortion)" + } + } +} diff --git a/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift b/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift new file mode 100644 index 000000000..374ddf960 --- /dev/null +++ b/Sources/SmithyCodegenCore/Serialize/SerializeCodegen.swift @@ -0,0 +1,134 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ErrorTrait +import struct Smithy.SparseTrait + +package struct SerializeCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import enum Smithy.Prelude") + writer.write("import struct Smithy.Schema") + writer.write("import protocol SmithySerialization.SerializableStruct") + writer.write("import protocol SmithySerialization.ShapeSerializer") + writer.write("import typealias SmithySerialization.WriteStructConsumer") + writer.write("") + + let inputStructsAndUnions = try ctx.service.inputDescendants + .filter { $0.type == .structure || $0.type == .union } + .smithySorted() + + for shape in inputStructsAndUnions { + let swiftType = try ctx.symbolProvider.swiftType(shape: shape) + let varName = shape.type == .structure ? "structure" : "union" + try writer.openBlock("extension \(swiftType): SmithySerialization.SerializableStruct {", "}") { writer in + writer.write("") + try writer.openBlock( + "public func serialize(_ serializer: any SmithySerialization.ShapeSerializer) throws {", + "}" + ) { writer in + let schemaVarName = try shape.schemaVarName + writer.write("try serializer.writeStruct(\(schemaVarName), self)") + } + writer.write("") + try writer.openBlock( + "public static var writeConsumer: SmithySerialization.WriteStructConsumer {", "}" + ) { writer in + try writer.openBlock("{ memberSchema, \(varName), serializer in", "}") { writer in + writer.write("switch memberSchema.index {") + for (index, member) in try members(of: shape).enumerated() { + writer.write("case \(index):") + writer.indent() + if shape.type == .structure { + let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id) + let properties = shape.hasTrait(ErrorTrait.self) ? "properties." : "" + if try NullableIndex().isNonOptional(member) { + writer.write("let value = \(varName).\(properties)\(propertyName)") + } else { + writer.write( + "guard let value = \(varName).\(properties)\(propertyName) else { break }" + ) + } + try writeSerializeCall( + writer: writer, shape: shape, member: member, schemaVarName: "memberSchema" + ) + } else { // shape is a union + let enumCaseName = try ctx.symbolProvider.enumCaseName(shapeID: member.id) + writer.write("guard case .\(enumCaseName)(let value) = \(varName) else { break }") + try writeSerializeCall( + writer: writer, shape: shape, member: member, schemaVarName: "memberSchema" + ) + } + writer.dedent() + } + writer.write("default: break") + writer.write("}") + } + } + } + writer.write("") + } + writer.unwrite("\n") + return writer.contents + } + + private func writeSerializeCall( + writer: SwiftWriter, + shape: Shape, + member: MemberShape, + schemaVarName: String + ) throws { + let target = try member.target + switch target.type { + case .list, .set: + guard let listShape = target as? ListShape else { + throw ModelError("Shape \(target.id) is type .\(target.type) but not a ListShape") + } + let isSparse = listShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "writeSparseList" : "writeList" + try writer.openBlock( + "try serializer.\(methodName)(\(schemaVarName), value) { value, serializer in", + "}" + ) { writer in + try writeSerializeCall( + writer: writer, + shape: listShape, + member: listShape.member, + schemaVarName: "\(schemaVarName).target!.member" + ) + } + case .map: + guard let mapShape = target as? MapShape else { + throw ModelError("Shape \(target.id) is type .map but not a MapShape") + } + let isSparse = mapShape.hasTrait(SparseTrait.self) + let methodName = isSparse ? "writeSparseMap" : "writeMap" + try writer.openBlock( + "try serializer.\(methodName)(\(schemaVarName), value) { value, serializer in", + "}" + ) { writer in + try writeSerializeCall( + writer: writer, + shape: mapShape, + member: mapShape.value, + schemaVarName: "\(schemaVarName).target!.value" + ) + } + default: + let methodName = try target.serializeMethodName + writer.write("try serializer.\(methodName)(\(schemaVarName), value)") + } + } + + private func members(of shape: Shape) throws -> [MemberShape] { + guard let hasMembers = shape as? HasMembers else { return [] } + return try hasMembers.members + } +} diff --git a/Sources/SmithyCodegenCore/Serialize/Shape+SerializeMethodName.swift b/Sources/SmithyCodegenCore/Serialize/Shape+SerializeMethodName.swift new file mode 100644 index 000000000..651a9c325 --- /dev/null +++ b/Sources/SmithyCodegenCore/Serialize/Shape+SerializeMethodName.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension Shape { + + var serializeMethodName: String { + get throws { + switch type { + case .blob: + if hasTrait(StreamingTrait.self) { + return "writeDataStream" + } else { + return "writeBlob" + } + case .boolean: + return "writeBoolean" + case .string: + return "writeString" + case .enum: + return "writeEnum" + case .timestamp: + return "writeTimestamp" + case .byte: + return "writeByte" + case .short: + return "writeShort" + case .integer: + return "writeInteger" + case .intEnum: + return "writeIntEnum" + case .long: + return "writeLong" + case .float: + return "writeFloat" + case .document: + return "writeDocument" + case .double: + return "writeDouble" + case .bigDecimal: + return "writeBigDecimal" + case .bigInteger: + return "writeBigInteger" + case .list, .set: + return "writeList" + case .map: + return "writeMap" + case .structure, .union: + if hasTrait(StreamingTrait.self) { + return "writeEventStream" + } else { + return "writeStruct" + } + case .member, .service, .resource, .operation: + throw ModelError("Cannot serialize type \(type)") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift new file mode 100644 index 000000000..6e26906b5 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/EnumShape.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 +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy enums. +public class EnumShape: Shape, HasMembers { + + public let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .enum, traits: traits) + } + + public var members: [MemberShape] { + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/HasMembers.swift b/Sources/SmithyCodegenCore/Shape/HasMembers.swift new file mode 100644 index 000000000..96f90272a --- /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 throws } +} diff --git a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift new file mode 100644 index 000000000..db330c18f --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift @@ -0,0 +1,31 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy intEnums. +public class IntEnumShape: Shape, HasMembers { + public let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .intEnum, traits: traits) + } + + public var members: [MemberShape] { + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/IntegerShape.swift b/Sources/SmithyCodegenCore/Shape/IntegerShape.swift new file mode 100644 index 000000000..15c7031dc --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/IntegerShape.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy integers. +class IntegerShape: Shape { + + public init(id: ShapeID, traits: TraitCollection) { + super.init(id: id, type: .integer, traits: traits) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ListShape.swift b/Sources/SmithyCodegenCore/Shape/ListShape.swift new file mode 100644 index 000000000..680f7a7a7 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ListShape.swift @@ -0,0 +1,42 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy lists. +public class ListShape: Shape, HasMembers { + public let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .list, traits: traits) + } + + public var member: MemberShape { + get throws { + // A list will always have one member, and it will always be "member". + let memberID = memberIDs[0] + guard memberID.member == "member" else { + throw ModelError("ListShape does not have expected \"member\" member") + } + return try model.expectMemberShape(id: memberID) + } + } + + public var members: [MemberShape] { + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/MapShape.swift b/Sources/SmithyCodegenCore/Shape/MapShape.swift new file mode 100644 index 000000000..304c65d88 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/MapShape.swift @@ -0,0 +1,53 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy maps. +public class MapShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .map, traits: traits) + } + + public var key: MemberShape { + get throws { + // A map will always have two members, and the first will always be "key". + let keyID = memberIDs[0] + guard keyID.member == "key" else { + throw ModelError("MapShape does not have expected Key member") + } + return try model.expectMemberShape(id: keyID) + } + } + + public var value: MemberShape { + get throws { + // A map will always have two members, and the second will always be "value". + let valueID = memberIDs[1] + guard valueID.member == "value" else { + throw ModelError("MapShape does not have expected Value member") + } + return try model.expectMemberShape(id: valueID) + } + } + + public var members: [MemberShape] { + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/MemberShape.swift b/Sources/SmithyCodegenCore/Shape/MemberShape.swift new file mode 100644 index 000000000..ab715f70b --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/MemberShape.swift @@ -0,0 +1,42 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy members. +public class MemberShape: Shape { + let targetID: ShapeID + + init(id: ShapeID, traits: TraitCollection, targetID: ShapeID) { + self.targetID = targetID + super.init(id: id, type: .member, traits: traits) + } + + public var container: Shape { + get throws { + return try model.expectShape(id: containerID) + } + } + + public var containerID: ShapeID { .init(id: id, member: nil) } + + public var target: Shape { + get throws { + guard let target = model.shapes[targetID] ?? Shape.prelude[targetID] else { + throw ModelError("Member \(id): target \(targetID) does not exist") + } + return target + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + guard targetID.namespace != "smithy.api" else { return [] } + return [try target] + } +} diff --git a/Sources/SmithyCodegenCore/Shape/OperationShape.swift b/Sources/SmithyCodegenCore/Shape/OperationShape.swift new file mode 100644 index 000000000..1531c0623 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/OperationShape.swift @@ -0,0 +1,51 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import enum Smithy.Prelude +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy operations. +public class OperationShape: Shape { + let inputID: ShapeID + let outputID: ShapeID + let errorIDs: [ShapeID] + + public init(id: ShapeID, traits: TraitCollection, inputID: ShapeID?, outputID: ShapeID?, errorIDs: [ShapeID]) { + self.inputID = inputID ?? Prelude.unitSchema.id + self.outputID = outputID ?? Prelude.unitSchema.id + self.errorIDs = errorIDs + super.init(id: id, type: .operation, traits: traits) + } + + public var input: StructureShape { + get throws { + try model.expectStructureShape(id: inputID) + } + } + + public var output: StructureShape { + get throws { + try model.expectStructureShape(id: outputID) + } + } + + public var errors: [StructureShape] { + get throws { + try errorIDs.map { try model.expectStructureShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + let inputOrNone = try includeInput ? [input] : [] + let outputOrNone = try includeOutput ? [output] : [] + let errorsOrNone = try includeOutput ? errorIDs.map { try model.expectShape(id: $0) } : [] + return Set(inputOrNone + outputOrNone + errorsOrNone) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ResourceShape.swift b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift new file mode 100644 index 000000000..8c805f170 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ResourceShape.swift @@ -0,0 +1,47 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy resources. +public class ResourceShape: Shape { + let operationIDs: [ShapeID] + let createID: ShapeID? + let putID: ShapeID? + let readID: ShapeID? + let updateID: ShapeID? + let deleteID: ShapeID? + let listID: ShapeID? + + init( + id: ShapeID, + traits: TraitCollection, + operationIDs: [ShapeID], + createID: ShapeID?, + putID: ShapeID?, + readID: ShapeID?, + updateID: ShapeID?, + deleteID: ShapeID?, + listID: ShapeID? + ) { + self.operationIDs = operationIDs + self.createID = createID + self.putID = putID + self.readID = readID + self.updateID = updateID + self.deleteID = deleteID + self.listID = listID + super.init(id: id, type: .resource, traits: traits) + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + let allOps = [createID, putID, readID, updateID, deleteID, listID].compactMap { $0 } + operationIDs + return try Set(allOps.map { try model.expectShape(id: $0) }) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift new file mode 100644 index 000000000..7a6b4fdc5 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift @@ -0,0 +1,81 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ServiceTrait +import struct Smithy.ShapeID +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy services. +public class ServiceShape: Shape { + let operationIDs: [ShapeID] + let resourceIDs: [ShapeID] + let errorIDs: [ShapeID] + + public init( + id: ShapeID, + traits: TraitCollection, + operationIDs: [ShapeID], + resourceIDs: [ShapeID], + errorIDs: [ShapeID] + ) { + self.operationIDs = operationIDs + self.resourceIDs = resourceIDs + self.errorIDs = errorIDs + super.init(id: id, type: .service, traits: traits) + } + + public var operations: [OperationShape] { + get throws { + try operationIDs.map { try model.expectOperationShape(id: $0) } + } + } + + public var resources: [ResourceShape] { + get throws { + try resourceIDs.map { try model.expectResourceShape(id: $0) } + } + } + + public var errors: [StructureShape] { + get throws { + try errorIDs.map { try model.expectStructureShape(id: $0) } + } + } + + public var sdkId: String { + get throws { + try getTrait(ServiceTrait.self)?.sdkId ?? id.name + } + } + + public var sdkIdStrippingService: String { + get throws { + var sdkIdStrippingService = try sdkId + let unwantedSuffix = " Service" + if sdkIdStrippingService.hasSuffix(unwantedSuffix) { + sdkIdStrippingService.removeLast(unwantedSuffix.count) + } + return sdkIdStrippingService + } + } + + public var clientBaseName: String { + get throws { + try sdkIdStrippingService.toUpperCamelCase() + } + + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + if includeOutput { + try Set(errors + operations + resources) + } else { + try Set(operations + resources) + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift new file mode 100644 index 000000000..9d5635159 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift @@ -0,0 +1,128 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Prelude +import struct Smithy.ShapeID + +extension Shape { + + static var prelude: [ShapeID: Shape] {[ + unit.id: unit, + boolean.id: boolean, + string.id: string, + integer.id: integer, + blob.id: blob, + timestamp.id: timestamp, + byte.id: byte, + short.id: short, + long.id: long, + float.id: float, + double.id: double, + document.id: document, + primitiveBoolean.id: primitiveBoolean, + primitiveInteger.id: primitiveInteger, + primitiveByte.id: primitiveByte, + primitiveLong.id: primitiveLong, + primitiveFloat.id: primitiveFloat, + primitiveDouble.id: primitiveDouble, + ]} + + static var unit: Shape { + let schema = Smithy.Prelude.unitSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var boolean: Shape { + let schema = Smithy.Prelude.booleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var string: Shape { + let schema = Smithy.Prelude.stringSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var integer: Shape { + let schema = Smithy.Prelude.integerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var blob: Shape { + let schema = Smithy.Prelude.blobSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var timestamp: Shape { + let schema = Smithy.Prelude.timestampSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var byte: Shape { + let schema = Smithy.Prelude.byteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var short: Shape { + let schema = Smithy.Prelude.shortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var long: Shape { + let schema = Smithy.Prelude.longSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var float: Shape { + let schema = Smithy.Prelude.floatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var double: Shape { + let schema = Smithy.Prelude.doubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var document: Shape { + let schema = Smithy.Prelude.documentSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveBoolean: Shape { + let schema = Smithy.Prelude.primitiveBooleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveInteger: Shape { + let schema = Smithy.Prelude.primitiveIntegerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveByte: Shape { + let schema = Smithy.Prelude.primitiveByteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveShort: Shape { + let schema = Smithy.Prelude.primitiveShortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveLong: Shape { + let schema = Smithy.Prelude.primitiveLongSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveFloat: Shape { + let schema = Smithy.Prelude.primitiveFloatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveDouble: Shape { + let schema = Smithy.Prelude.primitiveDoubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/Shape.swift b/Sources/SmithyCodegenCore/Shape/Shape.swift new file mode 100644 index 000000000..5f661ccf2 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -0,0 +1,111 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import protocol Smithy.Trait +import struct Smithy.TraitCollection + +public class Shape: HasShapeID { + public let id: ShapeID + public let type: ShapeType + public let traits: TraitCollection + weak var model: Model! + + public init(id: ShapeID, type: ShapeType, traits: TraitCollection) { + self.id = id + self.type = type + self.traits = traits + } + + public func hasTrait(_ type: T.Type) -> Bool { + traits.hasTrait(type) + } + + public func getTrait(_ type: T.Type) throws -> T? { + try traits.getTrait(type) + } + + public func adding(traits newTraits: TraitCollection) -> Shape { + let combinedTraits = traits.adding(newTraits) + let new = Shape(id: id, type: type, traits: combinedTraits) + new.model = model + return new + } + + public var descendants: Set { + get throws { + var c = Set() + try descendants(descendants: &c, includeInput: true, includeOutput: true) + return c + } + } + + public var inputDescendants: Set { + get throws { + var c = Set() + try descendants(descendants: &c, includeInput: true, includeOutput: false) + return c + } + } + + public var outputDescendants: Set { + get throws { + var c = Set() + try descendants(descendants: &c, includeInput: false, includeOutput: true) + return c + } + } + + private func descendants(descendants: inout Set, includeInput: Bool, includeOutput: Bool) throws { + for shape in try immediateDescendants(includeInput: includeInput, includeOutput: includeOutput) { + if descendants.contains(shape) { continue } + descendants.insert(shape) + try shape.descendants( + descendants: &descendants, + includeInput: includeInput, + includeOutput: includeOutput + ) + } + } + + /// Returns shapes that this shape directly refers to. + /// + /// Used to build a tree of shapes for code generation purposes. + /// - Parameters: + /// - includeInput: Whether to include shapes that are associated with input + /// - includeOutput: Whether to include shapes that are associated with input + /// - Returns: A set of shapes that this shape refers to. + func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + [] // none by default. Must be overridden by Shape subclasses that refer to descendant shape types. + } +} + +// Shapes should always be unique to a model by their Shape ID, so Hashable, Equatable, +// and Comparable conformances are based on the Shape ID alone. + +extension Shape: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Shape: Equatable { + + public static func == (lhs: Shape, rhs: Shape) -> Bool { + lhs.id == rhs.id + } +} + +extension Shape: Comparable { + + public static func < (lhs: Shape, rhs: Shape) -> Bool { + lhs.id < rhs.id + } +} diff --git a/Sources/SmithyCodegenCore/Shape/StringShape.swift b/Sources/SmithyCodegenCore/Shape/StringShape.swift new file mode 100644 index 000000000..1d79dfd47 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/StringShape.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy strings. +class StringShape: Shape { + + public init(id: ShapeID, traits: TraitCollection) { + super.init(id: id, type: .string, traits: traits) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/StructureShape.swift b/Sources/SmithyCodegenCore/Shape/StructureShape.swift new file mode 100644 index 000000000..6c5c5a705 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/StructureShape.swift @@ -0,0 +1,31 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy structures. +public class StructureShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .structure, traits: traits) + } + + public var members: [MemberShape] { + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/UnionShape.swift b/Sources/SmithyCodegenCore/Shape/UnionShape.swift new file mode 100644 index 000000000..1d141b3d3 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/UnionShape.swift @@ -0,0 +1,31 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import struct Smithy.TraitCollection + +/// A ``Shape`` subclass specialized for Smithy unions. +public class UnionShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: TraitCollection, memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .union, traits: traits) + } + + public var members: [MemberShape] { + get throws { + try memberIDs.map { try model.expectMemberShape(id: $0) } + } + } + + override func immediateDescendants(includeInput: Bool, includeOutput: Bool) throws -> Set { + try Set(members) + } +} diff --git a/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift b/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift new file mode 100644 index 000000000..56c153ee4 --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node + +extension Node { + + /// Returns the node, rendered into a Swift literal for use in generated code. + /// + /// The node is rendered with some interstitial whitespace but single-line. + var rendered: String { + switch self { + case .object(let object): + guard !object.isEmpty else { return "[:]" } + return "[" + object.map { "\($0.key.literal): \($0.value.rendered)" }.joined(separator: ",") + "]" + case .list(let list): + return "[" + list.map { $0.rendered }.joined(separator: ", ") + "]" + case .string(let string): + return string.literal + case .number(let number): + return "\(number)" + case .boolean(let bool): + return "\(bool)" + case .null: + return "nil" + } + } +} diff --git a/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift new file mode 100644 index 000000000..aa4b22d5f --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift @@ -0,0 +1,107 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Locale +import struct Foundation.NSRange +import class Foundation.NSRegularExpression + +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)\"" + } + + /// Surrounds the string in backticks. + /// Used with Swift keywords that are used as identifiers. + var inBackticks: String { + "`\(self)`" + } + + /// Capitalizes the first letter of a string. + var capitalized: String { + let firstChar = self.first?.uppercased() ?? "" + return "\(firstChar)\(self.dropFirst())" + } + + /// Converts the string to lower camel case. + /// + /// Follows the exact logic used in the Kotlin code generator. + /// - Returns: The string, in lower camel case. + func toLowerCamelCase() -> String { + let words = splitOnWordBoundaries() // Split into words + let firstWord = words.first!.lowercased() // make first word lowercase + return firstWord + words.dropFirst().joined() // join lowercased first word to remainder + } + + /// Converts the string to upper camel case. + /// + /// Follows the exact logic used in the Kotlin code generator. + /// - Returns: The string, in upper camel case. + func toUpperCamelCase() -> String { + let words = splitOnWordBoundaries() // Split into words + let firstLetter = words.first!.first!.uppercased() // make first letter uppercase + return firstLetter + words.joined().dropFirst() // join uppercased first letter to remainder + } + + private func splitOnWordBoundaries() -> [String] { + // TODO: when nonsupporting platforms are dropped, convert this to Swift-native regex + // adapted from Java v2 SDK CodegenNamingUtils.splitOnWordBoundaries + var result = self + + // all non-alphanumeric characters: "acm-success"-> "acm success" + result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" + result = underscoreRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " _ ") + + // if a number has a standalone v or V in front of it, separate it out + result = smallVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 v$2") + result = largeVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 V$2") + + // add a space between camelCased words + result = camelCaseSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // add a space after acronyms + result = acronymSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // add space after a number in the middle of a word + result = spaceAfterNumberRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // remove extra spaces - multiple consecutive ones or those and the beginning/end of words + result = removeExtraSpaceRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + .trimmingCharacters(in: .whitespaces) + + return result.components(separatedBy: " ") + } + + private var range: NSRange { + NSRange(location: 0, length: count) + } +} + +// Regexes used in splitOnWordBoundaries() above. +// force_try linter rule is disabled since these are just created from static strings. +// swiftlint:disable force_try +private let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") +private let underscoreRegex = try! NSRegularExpression(pattern: "_") +private let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") +private let largeVRegex = try! NSRegularExpression(pattern: "([^A-Z]{2,})V([0-9]+)") +private let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") +private let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") +private let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") +private let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") +// swiftlint:enable force_try diff --git a/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift new file mode 100644 index 000000000..4d0139a3c --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift @@ -0,0 +1,94 @@ +// +// 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 + +/// A type used to write structured source code text. +/// +/// Based heavily on the Kotlin-based code generator's `SwiftWriter` type. +class SwiftWriter { + + private var lines: [String] + + var indentLevel = 0 + + // One indent/dedent will move indentation by this number of spaces. + let indentStep = 4 + + /// Creates a new ``SwiftWriter``. + /// - Parameter includeHeader: Whether to include the standard header at the top of the generated source content. + /// The header contents are in a resource named `DefaultSwiftHeader.txt`. + init(includeHeader: Bool = true) { + if includeHeader { + let defaultHeaderFileURL = Bundle.module.url(forResource: "DefaultSwiftHeader", withExtension: "txt")! + // swiftlint:disable:next force_try + let defaultHeader = try! String(data: Data(contentsOf: defaultHeaderFileURL), encoding: .utf8)! + self.lines = defaultHeader.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) } + } else { + self.lines = [] + } + } + + /// Indents the writer by one additional step + func indent() { + indentLevel += indentStep + } + + /// Dedents the writer by one step + func dedent() { + indentLevel -= indentStep + } + + /// Writes a line of text to the source + /// - Parameter line: The text to be written. + func write(_ line: String) { + if line.isEmpty { + // Don't write whitespace to an empty line + lines.append("") + } else { + // Write whitespace for the indent level, then the line content + lines.append(String(repeating: " ", count: indentLevel) + line) + } + } + + /// Removes previously written text. + /// + /// If the unwritten text matches the end of the last written line, then that text will be removed from that line. + /// + /// If the unwritten text is `\n`, then the entire previous line will be removed only if it is an empty line. + /// Otherwise, unwriting `\n` has no effect. + /// - Parameter text: The text to be removed from the last written text. + func unwrite(_ text: String) { + guard let lastIndex = lines.indices.last else { return } + if text == "\n" && lines[lastIndex] == "" { + _ = lines.removeLast() + } else if lines[lastIndex].hasSuffix(text) { + lines[lastIndex].removeLast(text.count) + } + } + + /// Writes a "block" of text with opening text, closing text, and an indented body between. + /// - Parameters: + /// - openWith: The text to open the block + /// - closeWith: The text to close the block + /// - contents: A closure that accepts a SwiftWriter as a param, and writes the indented body of the block. + func openBlock(_ openWith: String, _ closeWith: String, contents: (SwiftWriter) throws -> Void) rethrows { + write(openWith) + indent() + try contents(self) + dedent() + write(closeWith) + } + + /// Returns the entire source contents of the writer, from the header (if any) to the last line written, + /// with individual lines joined by newlines, suitable for writing to a Swift source file. + var contents: String { + return lines.joined(separator: "\n").appending("\n") + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift b/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift new file mode 100644 index 000000000..b4a25a5a9 --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/String+ReservedWords.swift @@ -0,0 +1,125 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension String { + + /// Modifies Swift reserved words that are used in Smithy models so that they can be safely used as identifiers in rendered Swift. + /// + /// Taken from `ReservedWords.kt`: + /// https://github.com/smithy-lang/smithy-swift/blob/main/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/lang/ReservedWords.kt + var escapingReservedWords: String { + if self == "Protocol" || self == "Type" { + // Swift metatypes + "Model\(self)" + } else if reservedWords.contains(self) { + // Surround reserved words in backticks to force compiler + // to treat them as identifiers + self.inBackticks + } else { + self + } + } +} + +private let reservedWords = [ + "Any", + "#available", + "associatedtype", + "associativity", + "as", + "break", + "case", + "catch", + "class", + "#colorLiteral", + "#column", + "continue", + "convenience", + "deinit", + "default", + "defer", + "didSet", + "do", + "dynamic", + "enum", + "extension", + "else", + "#else", + "#elseif", + "#endif", + "#error", + "fallthrough", + "false", + "#file", + "#fileLiteral", + "fileprivate", + "final", + "for", + "func", + "#function", + "get", + "guard", + "indirect", + "infix", + "if", + "#if", + "#imageLiteral", + "in", + "is", + "import", + "init", + "inout", + "internal", + "lazy", + "left", + "let", + "#line", + "mutating", + "none", + "nonmutating", + "nil", + "open", + "operator", + "optional", + "override", + "package", + "postfix", + "prefix", + "private", + "protocol", + "Protocol", + "public", + "repeat", + "rethrows", + "return", + "required", + "right", + "#selector", + "self", + "Self", + "set", + "#sourceLocation", + "super", + "static", + "struct", + "subscript", + "switch", + "this", + "throw", + "throws", + "true", + "try", + "Type", + "typealias", + "unowned", + "var", + "#warning", + "weak", + "willSet", + "where", + "while", +] diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift new file mode 100644 index 000000000..804d1efd2 --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -0,0 +1,123 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Locale +import struct Foundation.NSRange +import class Foundation.NSRegularExpression +import struct Smithy.ErrorTrait +import struct Smithy.ServiceTrait +import struct Smithy.ShapeID + +public struct SymbolProvider { + let service: ServiceShape + let model: Model + + init(service: ServiceShape, model: Model) { + self.service = service + self.model = model + } + + var serviceName: String { + get throws { + return try service.sdkIdStrippingService + .replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "Service", with: "") + } + } + + public func swiftType(shape: Shape) throws -> String { + switch shape.type { + case .structure, .union, .enum, .intEnum: + let base = shape.id.name + if shape.isTopLevel { + return base.capitalized.escapingReservedWords + } else if shape.type == .intEnum { + // The NestedShapeTransformer in main codegen inadvertently excludes intEnum + // so it is not namespaced here. All other shape types are in the namespace. + return base.capitalized.escapingReservedWords + } else { + return try "\(modelNamespace).\(base.capitalized.escapingReservedWords)" + } + case .list, .set: + guard let listShape = shape as? ListShape else { + throw SymbolProviderError("Shape has type .list but is not a ListShape") + } + let elementType = try swiftType(shape: listShape.member.target) + let opt = try NullableIndex().isNonOptional(listShape.member) ? "" : "?" + return "[\(elementType)\(opt)]" + case .map: + guard let mapShape = shape as? MapShape else { + throw SymbolProviderError("Shape has type .map but is not a MapShape") + } + let valueType = try swiftType(shape: mapShape.value.target) + let opt = try NullableIndex().isNonOptional(mapShape.value) ? "" : "?" + return "[Swift.String: \(valueType)\(opt)]" + case .string: + return "Swift.String" + case .boolean: + return "Swift.Bool" + case .byte: + return "Swift.Int8" + case .short: + return "Swift.Int16" + case .integer, .long: + return "Swift.Int" + case .bigInteger: + return "Swift.Int64" + case .float: + return "Swift.Float" + case .double, .bigDecimal: + return "Swift.Double" + case .blob: + if shape.hasTrait(StreamingTrait.self) { + return "Smithy.ByteStream" + } else { + return "Foundation.Data" + } + case .timestamp: + return "Foundation.Date" + case .document: + return "Smithy.Document" + case .service: + // Returns the type name for the client + guard let serviceShape = shape as? ServiceShape else { + throw SymbolProviderError("Shape has type .service but is not a ServiceShape") + } + return try "\(serviceShape.clientBaseName)Client" + case .member, .operation, .resource: + throw SymbolProviderError("Cannot provide Swift symbol for shape type \(shape.type)") + } + } + + static let locale = Locale(identifier: "en_US_POSIX") + + public func operationMethodName(operation: OperationShape) throws -> String { + return operation.id.name.toLowerCamelCase().escapingReservedWords + } + + public func propertyName(shapeID: ShapeID) throws -> String { + guard let member = shapeID.member else { throw SymbolProviderError("Shape ID has no member name") } + return member.toLowerCamelCase().escapingReservedWords + } + + public func enumCaseName(shapeID: ShapeID) throws -> String { + try propertyName(shapeID: shapeID).lowercased().escapingReservedWords + } + + private var modelNamespace: String { + get throws { + try swiftType(shape: service).appending("Types") + } + } +} + +private extension Shape { + + var isTopLevel: Bool { + hasTrait(UsedAsInputTrait.self) || hasTrait(UsedAsOutputTrait.self) || hasTrait(ErrorTrait.self) + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift new file mode 100644 index 000000000..2d17de6bb --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SymbolProviderError: Error { + let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift new file mode 100644 index 000000000..1f46a74d4 --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/DeprecatedTrait.swift @@ -0,0 +1,27 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait +import struct Smithy.TraitError + +/// See https://smithy.io/2.0/spec/documentation-traits.html#deprecated-trait +public struct DeprecatedTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "deprecated") } + + public let node: Node + public let since: String? + + public init(node: Node) throws { + guard case .object(let object) = node else { + throw TraitError("DeprecatedTrait does not have root object") + } + self.since = object["since"]?.string + self.node = node + } +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift new file mode 100644 index 000000000..9e845f0cb --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/EnumTrait.swift @@ -0,0 +1,47 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait +import struct Smithy.TraitError + +/// See https://smithy.io/2.0/spec/constraint-traits.html#smithy-api-enum-trait +/// +/// This trait is deprecated, and strings tagged with it are converted to `EnumShape` as part of loading the +/// model from AST. Therefore, it is not renered to schemas or used at runtime. +public struct EnumTrait: Trait { + + public struct EnumMember { + public let value: String + public let name: String? + } + public static var id: ShapeID { .init("smithy.api", "enum") } + + public let node: Node + public let members: [EnumMember] + + public init(node: Node) throws { + guard let list = node.list else { + throw TraitError("EnumTrait root node is not .list") + } + let members = try list.map { element in + guard let member = element.object else { + throw TraitError("EnumTrait member is not .object") + } + guard let valueNode = member["value"] else { + throw TraitError("EnumTrait member does not have value field") + } + guard let value = valueNode.string else { + throw TraitError("EnumTrait member does not have string for value") + } + return EnumMember(value: value, name: member["name"]?.string) + } + self.node = node + self.members = members + } +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift new file mode 100644 index 000000000..ddf971b47 --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/StreamingTrait.swift @@ -0,0 +1,19 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +/// See https://smithy.io/2.0/spec/streaming.html#smithy-api-streaming-trait +public struct StreamingTrait: Trait { + public static var id: ShapeID { .init("smithy.api", "streaming") } + + public var node: Node { [:] } + + public init(node: Node) throws {} +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift new file mode 100644 index 000000000..5e1f2d2dc --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsInputTrait.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +public struct UsedAsInputTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "usedAsInput") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift new file mode 100644 index 000000000..e9772600a --- /dev/null +++ b/Sources/SmithyCodegenCore/TraitLibrary/UsedAsOutputTrait.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import protocol Smithy.Trait + +public struct UsedAsOutputTrait: Trait { + public static var id: ShapeID { .init("swift.synthetic", "usedAsOutput") } + + public var node: Node { [:] } + + public init(node: Node) throws {} + + public init() {} +} diff --git a/Sources/SmithySerialization/Deserialization/DeserializableShape.swift b/Sources/SmithySerialization/Deserialization/DeserializableShape.swift new file mode 100644 index 000000000..6ec63dca7 --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/DeserializableShape.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol DeserializableShape: Sendable { + static func deserialize(_ deserializer: ShapeDeserializer) throws -> Self +} diff --git a/Sources/SmithySerialization/Deserialization/DeserializableStruct.swift b/Sources/SmithySerialization/Deserialization/DeserializableStruct.swift new file mode 100644 index 000000000..e6e01ed90 --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/DeserializableStruct.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol DeserializableStruct: DeserializableShape { + static var readConsumer: ReadStructConsumer { get } +} diff --git a/Sources/SmithySerialization/Deserialization/ReadStructConsumer.swift b/Sources/SmithySerialization/Deserialization/ReadStructConsumer.swift new file mode 100644 index 000000000..70acbc1fe --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/ReadStructConsumer.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public typealias ReadStructConsumer = (Schema, inout T, any ShapeDeserializer) throws -> Void diff --git a/Sources/SmithySerialization/Deserialization/ReadValueConsumer.swift b/Sources/SmithySerialization/Deserialization/ReadValueConsumer.swift new file mode 100644 index 000000000..148d9e783 --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/ReadValueConsumer.swift @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias ReadValueConsumer = (any ShapeDeserializer) throws -> V diff --git a/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift b/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift new file mode 100644 index 000000000..099f9d9e9 --- /dev/null +++ b/Sources/SmithySerialization/Deserialization/ShapeDeserializer.swift @@ -0,0 +1,82 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import enum Smithy.ByteStream +import struct Smithy.Document +import struct Smithy.Schema +import protocol Smithy.SmithyDocument + +public protocol ShapeDeserializer { + func readStruct(_ schema: Schema, _ value: inout T) throws + func readList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E] + func readMap(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [String: V] + func readBoolean(_ schema: Schema) throws -> Bool + func readBlob(_ schema: Schema) throws -> Data + func readByte(_ schema: Schema) throws -> Int8 + func readShort(_ schema: Schema) throws -> Int16 + func readInteger(_ schema: Schema) throws -> Int + func readLong(_ schema: Schema) throws -> Int + func readFloat(_ schema: Schema) throws -> Float + func readDouble(_ schema: Schema) throws -> Double + func readBigInteger(_ schema: Schema) throws -> Int64 + func readBigDecimal(_ schema: Schema) throws -> Double + func readString(_ schema: Schema) throws -> String + func readDocument(_ schema: Schema) throws -> Smithy.Document + func readTimestamp(_ schema: Schema) throws -> Date + func readNull(_ schema: Schema) throws -> T? + func readDataStream(_ schema: Schema) throws -> ByteStream + func readEventStream(_ schema: Schema) throws -> AsyncThrowingStream + func isNull() throws -> Bool + var containerSize: Int { get } +} + +public extension ShapeDeserializer { + + func readEnum(_ schema: Schema) throws -> T where T.RawValue == String { + // Force-unwrap is safe here because generated enums & intEnums never return nil from init(rawValue:) + try T(rawValue: readString(schema))! + } + + func readIntEnum(_ schema: Schema) throws -> T where T.RawValue == Int { + // Force-unwrap is safe here because generated enums & intEnums never return nil from init(rawValue:) + try T(rawValue: readInteger(schema))! + } + + func readSparseList(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [E?] { + try readList(schema) { deserializer in + if try deserializer.isNull() { + return try deserializer.readNull(schema.resolveTarget.member) + } else { + return try consumer(deserializer) + } + } + } + + func readSparseMap(_ schema: Schema, _ consumer: ReadValueConsumer) throws -> [String: V?] { + try readMap(schema) { deserializer in + if try deserializer.isNull() { + return try deserializer.readNull(schema.resolveTarget.value) + } else { + return try consumer(deserializer) + } + } + } + + func readDataStream(_ schema: Schema) throws -> ByteStream { + // by default, do nothing + return ByteStream.data(nil) + } + + func readEventStream(_ schema: Schema) throws -> AsyncThrowingStream { + // by default, do nothing + return AsyncThrowingStream { continuation in + continuation.finish() + } + } +} diff --git a/Sources/SmithySerialization/Serialization/SerializableShape.swift b/Sources/SmithySerialization/Serialization/SerializableShape.swift new file mode 100644 index 000000000..34f7d051b --- /dev/null +++ b/Sources/SmithySerialization/Serialization/SerializableShape.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public protocol SerializableShape { + func serialize(_ serializer: any ShapeSerializer) throws +} diff --git a/Sources/SmithySerialization/Serialization/SerializableStruct.swift b/Sources/SmithySerialization/Serialization/SerializableStruct.swift new file mode 100644 index 000000000..b0d5e4169 --- /dev/null +++ b/Sources/SmithySerialization/Serialization/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 { + static var writeConsumer: WriteStructConsumer { get } +} diff --git a/Sources/SmithySerialization/Serialization/ShapeSerializer.swift b/Sources/SmithySerialization/Serialization/ShapeSerializer.swift new file mode 100644 index 000000000..b8e91ed12 --- /dev/null +++ b/Sources/SmithySerialization/Serialization/ShapeSerializer.swift @@ -0,0 +1,157 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import enum Smithy.ByteStream +import struct Smithy.Schema +import protocol Smithy.SmithyDocument + +public protocol ShapeSerializer { + func writeStruct(_ schema: Schema, _ value: S) throws + func writeList(_ schema: Schema, _ value: [E], _ consumer: WriteValueConsumer) throws + func writeMap(_ schema: Schema, _ value: [String: V], _ consumer: WriteValueConsumer) throws + func writeBoolean(_ schema: Schema, _ value: Bool) throws + func writeByte(_ schema: Schema, _ value: Int8) throws + func writeShort(_ schema: Schema, _ value: Int16) throws + func writeInteger(_ schema: Schema, _ value: Int) throws + func writeLong(_ schema: Schema, _ value: Int) throws + func writeFloat(_ schema: Schema, _ value: Float) throws + func writeDouble(_ schema: Schema, _ value: Double) throws + func writeBigInteger(_ schema: Schema, _ value: Int64) throws + func writeBigDecimal(_ schema: Schema, _ value: Double) throws + func writeString(_ schema: Schema, _ value: String) throws + func writeBlob(_ schema: Schema, _ value: Data) throws + func writeTimestamp(_ schema: Schema, _ value: Date) throws + func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws + func writeNull(_ schema: Schema) throws + func writeDataStream(_ schema: Schema, _ value: ByteStream) throws + func writeEventStream(_ schema: Schema, _ value: AsyncThrowingStream) throws + + var data: Data { get } +} + +public extension ShapeSerializer { + + /// Writes a Smithy enum. + /// - Parameters: + /// - schema: The schema for the Smithy Enum. + /// - value: The enum value to be written. + func writeEnum(_ schema: Schema, _ value: T) throws where T.RawValue == String { + try writeString(schema, value.rawValue) + } + + /// Writes an Smithy IntEnum. + /// - Parameters: + /// - schema: The schema for the Smithy IntEnum. + /// - value: The enum value to be written. + func writeIntEnum(_ schema: Schema, _ value: T) throws where T.RawValue == Int { + try writeInteger(schema, value.rawValue) + } + + /// Write a sparse list. + /// + /// Generated code will call this method when the list has the sparse trait. + /// - Parameters: + /// - schema: The member schema targeting the list. + /// - value: The sparse list to be written. + /// - consumer: The `WriteValueConsumer` for the non-optional element type. + func writeSparseList(_ schema: Schema, _ value: [E?], _ consumer: WriteValueConsumer) throws { + try writeList(schema, value) { element, serializer in + if let element { + try consumer(element, serializer) + } else { + try serializer.writeNull(schema.resolveTarget.member) + } + } + } + + /// Write a sparse map. + /// + /// Generated code will call this method when the map has the sparse trait. + /// - Parameters: + /// - schema: The member schema targeting the map. + /// - value: The sparse map to be written. + /// - consumer: The `WriteValueConsumer` for the non-optional element type. + func writeSparseMap(_ schema: Schema, _ value: [String: V?], _ consumer: WriteValueConsumer) throws { + try writeMap(schema, value) { element, serializer in + if let element { + try consumer(element, serializer) + } else { + try serializer.writeNull(schema.resolveTarget.value) + } + } + } + + /// Write a Smithy document. + /// + /// Based on the type of the document, the contents are written using the appropriate `write...` method for its contents. + /// - Parameters: + /// - schema: The schema for the document + /// - value: The document to be written + func writeDocument(_ schema: Schema, _ value: any SmithyDocument) throws { + switch value.type { + case .blob: + try writeBlob(schema, value.asBlob()) + case .boolean: + try writeBoolean(schema, value.asBoolean()) + case .string: + try writeString(schema, value.asString()) + case .timestamp: + try writeTimestamp(schema, value.asTimestamp()) + case .byte: + try writeByte(schema, value.asByte()) + case .short: + try writeShort(schema, value.asShort()) + case .integer: + try writeInteger(schema, value.asInteger()) + case .long: + try writeLong(schema, Int(value.asLong())) + case .float: + try writeFloat(schema, value.asFloat()) + case .double: + try writeDouble(schema, value.asDouble()) + case .bigDecimal: + try writeBigDecimal(schema, value.asBigDecimal()) + case .bigInteger: + try writeBigInteger(schema, value.asBigInteger()) + case .list, .set: + try writeList(schema, value.asList()) { element, serializer in + try serializer.writeDocument(schema.member, element) + } + case .map: + try writeMap(schema, value.asStringMap()) { value, serializer in + try serializer.writeDocument(schema.value, value) + } + case .document, .enum, .intEnum, .structure, .union, .member, .service, .resource, .operation: + throw SerializerError("Unsupported or invalid document type: \(value.type)") + } + } + + func writeDataStream(_ schema: Schema, _ value: ByteStream) throws { + // by default, do nothing + } + + func writeEventStream(_ schema: Schema, _ value: AsyncThrowingStream) throws { + // by default, do nothing + } +} + +extension Schema { + + var resolveMember: Schema? { + type == .member ? self : nil + } + + var resolveTarget: Schema { + if let target { + return target + } else { + return self + } + } +} diff --git a/Sources/SmithySerialization/Serialization/WriteStructConsumer.swift b/Sources/SmithySerialization/Serialization/WriteStructConsumer.swift new file mode 100644 index 000000000..07f51b890 --- /dev/null +++ b/Sources/SmithySerialization/Serialization/WriteStructConsumer.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Schema + +public typealias WriteStructConsumer = (Schema, T, any ShapeSerializer) throws -> Void diff --git a/Sources/SmithySerialization/Serialization/WriteValueConsumer.swift b/Sources/SmithySerialization/Serialization/WriteValueConsumer.swift new file mode 100644 index 000000000..a8049f70e --- /dev/null +++ b/Sources/SmithySerialization/Serialization/WriteValueConsumer.swift @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias WriteValueConsumer = (Element, any ShapeSerializer) throws -> Void diff --git a/Sources/SmithySerialization/SerializerError.swift b/Sources/SmithySerialization/SerializerError.swift new file mode 100644 index 000000000..166595af7 --- /dev/null +++ b/Sources/SmithySerialization/SerializerError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SerializerError: Error { + public let localizedDescription: String + + public init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Tests/SmithyCodegenCoreTests/Resources/smithy-rpcv2-cbor.json b/Tests/SmithyCodegenCoreTests/Resources/smithy-rpcv2-cbor.json new file mode 100644 index 000000000..6737c16ea --- /dev/null +++ b/Tests/SmithyCodegenCoreTests/Resources/smithy-rpcv2-cbor.json @@ -0,0 +1,3622 @@ +{ + "smithy": "2.0", + "metadata": { + "suppressions": [ + { + "id": "HttpMethodSemantics", + "namespace": "com.amazonaws.glacier" + }, + { + "id": "HttpMethodSemantics", + "namespace": "com.amazonaws.s3" + }, + { + "id": "EnumTrait", + "namespace": "com.amazonaws.s3" + }, + { + "id": "UnreferencedShape", + "namespace": "aws.protocoltests.config", + "reason": "These shapes are intended to be used to validate vendorParams in\nprotocol tests, so they naturally will not be connected to a service.\n" + }, + { + "id": "DeprecatedTrait", + "namespace": "*", + "reason": "Some of the AWS protocols make use of deprecated traits, and some are\nthemselves deprecated traits. As this package is intended to test those\nprotocols, the warnings should be suppressed." + } + ], + "validators": [ + { + "configuration": { + "selector": "operation :not(< service)" + }, + "id": "UnboundTestOperation", + "message": "This operation in the AWS protocol tests is not bound to a service.", + "name": "EmitEachSelector", + "namespaces": [ + "aws.protocoltests.json10", + "aws.protocoltests.json", + "aws.protocoltests.query", + "aws.protocoltests.ec2", + "aws.protocoltests.restjson.validation", + "aws.protocoltests.restjson", + "aws.protocoltests.restxml", + "aws.protocoltests.restxml.xmlns", + "com.amazonaws.apigateway", + "com.amazonaws.glacier", + "com.amazonaws.machinelearning", + "com.amazonaws.s3" + ], + "severity": "WARNING" + }, + { + "configuration": { + "selector": "operation :not(< service)" + }, + "id": "UnboundTestOperation", + "message": "This operation in the Smithy protocol tests is not bound to a service.", + "name": "EmitEachSelector", + "namespaces": [ + "smithy.protocoltests.rpcv2Cbor" + ], + "severity": "WARNING" + } + ] + }, + "shapes": { + "smithy.framework#ValidationException": { + "type": "structure", + "members": { + "message": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A summary of the validation failure.", + "smithy.api#required": {} + } + }, + "fieldList": { + "target": "smithy.framework#ValidationExceptionFieldList", + "traits": { + "smithy.api#documentation": "A list of specific failures encountered while validating the input.\nA member can appear in this list more than once if it failed to satisfy multiple constraints." + } + } + }, + "traits": { + "smithy.api#documentation": "A standard error for input validation failures.\nThis should be thrown by services when a member of the input structure\nfalls outside of the modeled or documented constraints.", + "smithy.api#error": "client" + } + }, + "smithy.framework#ValidationExceptionField": { + "type": "structure", + "members": { + "path": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A JSONPointer expression to the structure member whose value failed to satisfy the modeled constraints.", + "smithy.api#required": {} + } + }, + "message": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A detailed description of the validation failure.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "Describes one specific validation failure for an input member." + } + }, + "smithy.framework#ValidationExceptionFieldList": { + "type": "list", + "member": { + "target": "smithy.framework#ValidationExceptionField" + } + }, + "smithy.protocols#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#documentation": "A list of String shapes.", + "smithy.api#private": {} + } + }, + "smithy.protocols#rpcv2Cbor": { + "type": "structure", + "members": { + "http": { + "target": "smithy.protocols#StringList", + "traits": { + "smithy.api#documentation": "Priority ordered list of supported HTTP protocol versions." + } + }, + "eventStreamHttp": { + "target": "smithy.protocols#StringList", + "traits": { + "smithy.api#documentation": "Priority ordered list of supported HTTP protocol versions\nthat are required when using event streams." + } + } + }, + "traits": { + "smithy.api#documentation": "An RPC-based protocol that serializes CBOR payloads.", + "smithy.api#protocolDefinition": { + "traits": [ + "smithy.api#cors", + "smithy.api#endpoint", + "smithy.api#hostLabel", + "smithy.api#httpError" + ] + }, + "smithy.api#trait": { + "selector": "service" + }, + "smithy.api#traitValidators": { + "rpcv2Cbor.NoDocuments": { + "selector": "service ~> member :test(> document)", + "message": "This protocol does not support document types in most possible scenarios.", + "severity": "DANGER" + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#ClientOptionalDefaults": { + "type": "structure", + "members": { + "member": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#clientOptional": {}, + "smithy.api#default": 0 + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#ComplexError": { + "type": "structure", + "members": { + "TopLevel": { + "target": "smithy.api#String" + }, + "Nested": { + "target": "smithy.protocoltests.rpcv2Cbor#ComplexNestedErrorData" + } + }, + "traits": { + "smithy.api#documentation": "This error is thrown when a request is invalid.", + "smithy.api#error": "client", + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborComplexError", + "documentation": "Parses a complex error with no message member", + "protocol": "smithy.protocols#rpcv2Cbor", + "params": { + "TopLevel": "Top level", + "Nested": { + "Foo": "bar" + } + }, + "code": 400, + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v2ZfX3R5cGV4K3NtaXRoeS5wcm90b2NvbHRlc3RzLnJwY3YyQ2JvciNDb21wbGV4RXJyb3JoVG9wTGV2ZWxpVG9wIGxldmVsZk5lc3RlZL9jRm9vY2Jhcv//", + "bodyMediaType": "application/cbor" + }, + { + "id": "RpcV2CborEmptyComplexError", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 400, + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v2ZfX3R5cGV4K3NtaXRoeS5wcm90b2NvbHRlc3RzLnJwY3YyQ2JvciNDb21wbGV4RXJyb3L/", + "bodyMediaType": "application/cbor" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#ComplexNestedErrorData": { + "type": "structure", + "members": { + "Foo": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#Defaults": { + "type": "structure", + "members": { + "defaultString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "hi" + } + }, + "defaultBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": true + } + }, + "defaultList": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringList", + "traits": { + "smithy.api#default": [] + } + }, + "defaultTimestamp": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#default": 0 + } + }, + "defaultBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "YWJj" + } + }, + "defaultByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 10 + } + }, + "defaultLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 100 + } + }, + "defaultFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultMap": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringMap", + "traits": { + "smithy.api#default": {} + } + }, + "defaultEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestEnum", + "traits": { + "smithy.api#default": "FOO" + } + }, + "defaultIntEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestIntEnum", + "traits": { + "smithy.api#default": 1 + } + }, + "emptyString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "" + } + }, + "falseBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": false + } + }, + "emptyBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "" + } + }, + "zeroByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 0.0 + } + }, + "zeroDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 0.0 + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseBooleanMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Boolean" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseNumberMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Integer" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseSetMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#StringSet" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseStringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.rpcv2Cbor#DenseStructMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#GreetingStruct" + } + }, + "smithy.protocoltests.rpcv2Cbor#EmptyInputOutput": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#EmptyStructure" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#EmptyStructure" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "empty_input", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When Input structure is empty we write CBOR equivalent of {}", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "forbidHeaders": [ + "X-Amz-Target" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/EmptyInputOutput", + "bodyMediaType": "application/cbor", + "body": "v/8=" + }, + { + "id": "empty_input_no_body", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When Input structure is empty the server should accept an empty body", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/EmptyInputOutput", + "bodyMediaType": "application/cbor", + "body": "", + "appliesTo": "server" + }, + { + "id": "empty_input_no_body_has_accept", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When input structure, is empty the server should accept an empty body\neven if the Accept header is set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/EmptyInputOutput", + "bodyMediaType": "application/cbor", + "body": "", + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "empty_output", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When output structure is empty we write CBOR equivalent of {}", + "body": "v/8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200 + }, + { + "id": "empty_output_no_body", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When output structure is empty the client should accept an empty body", + "body": "", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#EmptyStructure": { + "type": "structure", + "members": {} + }, + "smithy.protocoltests.rpcv2Cbor#Float16": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#Float16Output" + }, + "traits": { + "smithy.api#tags": [ + "client-only" + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborFloat16Inf", + "documentation": "Ensures that clients can correctly parse float16 +Inf.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfl8AA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "Infinity" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16NegInf", + "documentation": "Ensures that clients can correctly parse float16 -Inf.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfn8AA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "-Infinity" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16LSBNaN", + "documentation": "Ensures that clients can correctly parse float16 NaN with high LSB.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfl8AQ==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "NaN" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16MSBNaN", + "documentation": "Ensures that clients can correctly parse float16 NaN with high MSB.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1Zfl+AA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": "NaN" + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + }, + { + "id": "RpcV2CborFloat16Subnormal", + "documentation": "Ensures that clients can correctly parse a subnormal float16.", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWV2YWx1ZfkAUA==", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "value": 4.76837158203125E-6 + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#Float16Output": { + "type": "structure", + "members": { + "value": { + "target": "smithy.api#Double" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#FractionalSeconds": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#FractionalSecondsOutput" + }, + "traits": { + "smithy.api#tags": [ + "client-only" + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborDateTimeWithFractionalSeconds", + "documentation": "Ensures that clients can correctly parse timestamps with fractional seconds", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2hkYXRldGltZcH7Qcw32zgPvnf/", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "datetime": 9.46845296123E8 + }, + "bodyMediaType": "application/cbor", + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#FractionalSecondsOutput": { + "type": "structure", + "members": { + "datetime": { + "target": "smithy.protocoltests.shared#DateTime" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#GreetingWithErrors": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#GreetingWithErrorsOutput" + }, + "errors": [ + { + "target": "smithy.protocoltests.rpcv2Cbor#ComplexError" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#InvalidGreeting" + } + ], + "traits": { + "smithy.api#documentation": "This operation has three possible return values:\n\n1. A successful response in the form of GreetingWithErrorsOutput\n2. An InvalidGreeting error.\n3. A ComplexError error.\n\nImplementations must be able to successfully take a response and\nproperly deserialize successful and error responses.", + "smithy.api#idempotent": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#GreetingWithErrorsOutput": { + "type": "structure", + "members": { + "greeting": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#InvalidGreeting": { + "type": "structure", + "members": { + "Message": { + "target": "smithy.api#String" + } + }, + "traits": { + "smithy.api#documentation": "This error is thrown when an invalid greeting value is provided.", + "smithy.api#error": "client", + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborInvalidGreetingError", + "documentation": "Parses simple RpcV2 Cbor errors", + "protocol": "smithy.protocols#rpcv2Cbor", + "params": { + "Message": "Hi" + }, + "code": 400, + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v2ZfX3R5cGV4LnNtaXRoeS5wcm90b2NvbHRlc3RzLnJwY3YyQ2JvciNJbnZhbGlkR3JlZXRpbmdnTWVzc2FnZWJIaf8=", + "bodyMediaType": "application/cbor" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#NoInputOutput": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.api#Unit" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "no_input", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Body is empty and no Content-Type header if no input", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor" + }, + "forbidHeaders": [ + "Content-Type", + "X-Amz-Target" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/NoInputOutput", + "body": "" + }, + { + "id": "NoInputServerAllowsEmptyCbor", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Servers should accept CBOR empty struct if no input.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/NoInputOutput", + "body": "v/8=", + "appliesTo": "server" + }, + { + "id": "NoInputServerAllowsEmptyBody", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Servers should accept an empty body if there is no input. Additionally,\nthey should not raise an error if the `Accept` header is set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Accept": "application/cbor", + "Content-Type": "application/cbor" + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/NoInputOutput", + "body": "", + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "no_output", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "A `Content-Type` header should not be set if the response body is empty.", + "body": "", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor" + }, + "forbidHeaders": [ + "Content-Type" + ], + "code": 200 + }, + { + "id": "NoOutputClientAllowsEmptyCbor", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Clients should accept a CBOR empty struct if there is no output.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v/8=", + "appliesTo": "client" + }, + { + "id": "NoOutputClientAllowsEmptyBody", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Clients should accept an empty body if there is no output and\nshould not raise an error if the `Content-Type` header is set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "", + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#OperationWithDefaults": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsInput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborClientPopulatesDefaultValuesInInput", + "documentation": "Client populates default values in input.", + "protocol": "smithy.protocols#rpcv2Cbor", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "bodyMediaType": "application/cbor", + "body": "v2hkZWZhdWx0c79tZGVmYXVsdFN0cmluZ2JoaW5kZWZhdWx0Qm9vbGVhbvVrZGVmYXVsdExpc3Sf/3BkZWZhdWx0VGltZXN0YW1wwQBrZGVmYXVsdEJsb2JDYWJja2RlZmF1bHRCeXRlAWxkZWZhdWx0U2hvcnQBbmRlZmF1bHRJbnRlZ2VyCmtkZWZhdWx0TG9uZxhkbGRlZmF1bHRGbG9hdPo/gAAAbWRlZmF1bHREb3VibGX6P4AAAGpkZWZhdWx0TWFwv/9rZGVmYXVsdEVudW1jRk9PbmRlZmF1bHRJbnRFbnVtAWtlbXB0eVN0cmluZ2BsZmFsc2VCb29sZWFu9GllbXB0eUJsb2JAaHplcm9CeXRlAGl6ZXJvU2hvcnQAa3plcm9JbnRlZ2VyAGh6ZXJvTG9uZwBpemVyb0Zsb2F0+gAAAABqemVyb0RvdWJsZfoAAAAA//8=", + "params": { + "defaults": {} + } + }, + { + "id": "RpcV2CborClientSkipsTopLevelDefaultValuesInInput", + "documentation": "Client skips top level default values in input.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v/8=", + "params": {} + }, + { + "id": "RpcV2CborClientUsesExplicitlyProvidedMemberValuesOverDefaults", + "documentation": "Client uses explicitly provided member values over defaults", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v2hkZWZhdWx0c7dtZGVmYXVsdFN0cmluZ2NieWVuZGVmYXVsdEJvb2xlYW71a2RlZmF1bHRMaXN0gWFhcGRlZmF1bHRUaW1lc3RhbXDB+z/wAAAAAAAAa2RlZmF1bHRCbG9iQmhpa2RlZmF1bHRCeXRlAmxkZWZhdWx0U2hvcnQCbmRlZmF1bHRJbnRlZ2VyFGtkZWZhdWx0TG9uZxjIbGRlZmF1bHRGbG9hdPpAAAAAbWRlZmF1bHREb3VibGX7QAAAAAAAAABqZGVmYXVsdE1hcKFkbmFtZWRKYWNra2RlZmF1bHRFbnVtY0JBUm5kZWZhdWx0SW50RW51bQJrZW1wdHlTdHJpbmdjZm9vbGZhbHNlQm9vbGVhbvVpZW1wdHlCbG9iQmhpaHplcm9CeXRlAWl6ZXJvU2hvcnQBa3plcm9JbnRlZ2VyAWh6ZXJvTG9uZwFpemVyb0Zsb2F0+j+AAABqemVyb0RvdWJsZfs/8AAAAAAAAP8=", + "params": { + "defaults": { + "defaultString": "bye", + "defaultBoolean": true, + "defaultList": [ + "a" + ], + "defaultTimestamp": 1, + "defaultBlob": "hi", + "defaultByte": 2, + "defaultShort": 2, + "defaultInteger": 20, + "defaultLong": 200, + "defaultFloat": 2.0, + "defaultDouble": 2.0, + "defaultMap": { + "name": "Jack" + }, + "defaultEnum": "BAR", + "defaultIntEnum": 2, + "emptyString": "foo", + "falseBoolean": true, + "emptyBlob": "hi", + "zeroByte": 1, + "zeroShort": 1, + "zeroInteger": 1, + "zeroLong": 1, + "zeroFloat": 1.0, + "zeroDouble": 1.0 + } + } + }, + { + "id": "RpcV2CborServerPopulatesDefaultsWhenMissingInRequestBody", + "documentation": "Server populates default values when missing in request body.", + "appliesTo": "server", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v2hkZWZhdWx0c6D/", + "params": { + "defaults": { + "defaultString": "hi", + "defaultBoolean": true, + "defaultList": [], + "defaultTimestamp": 0, + "defaultBlob": "abc", + "defaultByte": 1, + "defaultShort": 1, + "defaultInteger": 10, + "defaultLong": 100, + "defaultFloat": 1.0, + "defaultDouble": 1.0, + "defaultMap": {}, + "defaultEnum": "FOO", + "defaultIntEnum": 1, + "emptyString": "", + "falseBoolean": false, + "emptyBlob": "", + "zeroByte": 0, + "zeroShort": 0, + "zeroInteger": 0, + "zeroLong": 0, + "zeroFloat": 0.0, + "zeroDouble": 0.0 + }, + "topLevelDefault": "hi", + "otherTopLevelDefault": 0 + } + }, + { + "id": "RpcV2CborClientUsesExplicitlyProvidedValuesInTopLevel", + "documentation": "Any time a value is provided for a member in the top level of input, it is used, regardless of if its the default.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v290b3BMZXZlbERlZmF1bHRiaGl0b3RoZXJUb3BMZXZlbERlZmF1bHQA/w==", + "params": { + "topLevelDefault": "hi", + "otherTopLevelDefault": 0 + } + }, + { + "id": "RpcV2CborClientIgnoresNonTopLevelDefaultsOnMembersWithClientOptional", + "documentation": "Typically, non top-level members would have defaults filled in, but if they have the clientOptional trait, the defaults should be ignored.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/OperationWithDefaults", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v3ZjbGllbnRPcHRpb25hbERlZmF1bHRzoP8=", + "params": { + "clientOptionalDefaults": {} + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", + "documentation": "Client populates default values when missing in response.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v/8=", + "params": { + "defaultString": "hi", + "defaultBoolean": true, + "defaultList": [], + "defaultTimestamp": 0, + "defaultBlob": "abc", + "defaultByte": 1, + "defaultShort": 1, + "defaultInteger": 10, + "defaultLong": 100, + "defaultFloat": 1.0, + "defaultDouble": 1.0, + "defaultMap": {}, + "defaultEnum": "FOO", + "defaultIntEnum": 1, + "emptyString": "", + "falseBoolean": false, + "emptyBlob": "", + "zeroByte": 0, + "zeroShort": 0, + "zeroInteger": 0, + "zeroLong": 0, + "zeroFloat": 0.0, + "zeroDouble": 0.0 + } + }, + { + "id": "RpcV2CborClientIgnoresDefaultValuesIfMemberValuesArePresentInResponse", + "documentation": "Client ignores default values if member values are present in the response.", + "appliesTo": "client", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v21kZWZhdWx0U3RyaW5nY2J5ZW5kZWZhdWx0Qm9vbGVhbvRrZGVmYXVsdExpc3SBYWFwZGVmYXVsdFRpbWVzdGFtcMH7QAAAAAAAAABrZGVmYXVsdEJsb2JCaGlrZGVmYXVsdEJ5dGUCbGRlZmF1bHRTaG9ydAJuZGVmYXVsdEludGVnZXIUa2RlZmF1bHRMb25nGMhsZGVmYXVsdEZsb2F0+kAAAABtZGVmYXVsdERvdWJsZftAAAAAAAAAAGpkZWZhdWx0TWFwoWRuYW1lZEphY2trZGVmYXVsdEVudW1jQkFSbmRlZmF1bHRJbnRFbnVtAmtlbXB0eVN0cmluZ2Nmb29sZmFsc2VCb29sZWFu9WllbXB0eUJsb2JCaGloemVyb0J5dGUBaXplcm9TaG9ydAFremVyb0ludGVnZXIBaHplcm9Mb25nAWl6ZXJvRmxvYXT6P4AAAGp6ZXJvRG91Ymxl+z/wAAAAAAAA/w==", + "params": { + "defaultString": "bye", + "defaultBoolean": false, + "defaultList": [ + "a" + ], + "defaultTimestamp": 2, + "defaultBlob": "hi", + "defaultByte": 2, + "defaultShort": 2, + "defaultInteger": 20, + "defaultLong": 200, + "defaultFloat": 2.0, + "defaultDouble": 2.0, + "defaultMap": { + "name": "Jack" + }, + "defaultEnum": "BAR", + "defaultIntEnum": 2, + "emptyString": "foo", + "falseBoolean": true, + "emptyBlob": "hi", + "zeroByte": 1, + "zeroShort": 1, + "zeroInteger": 1, + "zeroLong": 1, + "zeroFloat": 1.0, + "zeroDouble": 1.0 + } + }, + { + "id": "RpcV2CborServerPopulatesDefaultsInResponseWhenMissingInParams", + "documentation": "Server populates default values in response when missing in params.", + "appliesTo": "server", + "tags": [ + "defaults" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "body": "v21kZWZhdWx0U3RyaW5nYmhpbmRlZmF1bHRCb29sZWFu9WtkZWZhdWx0TGlzdIBwZGVmYXVsdFRpbWVzdGFtcMH7AAAAAAAAAABrZGVmYXVsdEJsb2JDYWJja2RlZmF1bHRCeXRlAWxkZWZhdWx0U2hvcnQBbmRlZmF1bHRJbnRlZ2VyCmtkZWZhdWx0TG9uZxhkbGRlZmF1bHRGbG9hdPo/gAAAbWRlZmF1bHREb3VibGX7P/AAAAAAAABqZGVmYXVsdE1hcKBrZGVmYXVsdEVudW1jRk9PbmRlZmF1bHRJbnRFbnVtAWtlbXB0eVN0cmluZ2BsZmFsc2VCb29sZWFu9GllbXB0eUJsb2JAaHplcm9CeXRlAGl6ZXJvU2hvcnQAa3plcm9JbnRlZ2VyAGh6ZXJvTG9uZwBpemVyb0Zsb2F0+gAAAABqemVyb0RvdWJsZfsAAAAAAAAAAP8=", + "params": {} + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsInput": { + "type": "structure", + "members": { + "defaults": { + "target": "smithy.protocoltests.rpcv2Cbor#Defaults" + }, + "clientOptionalDefaults": { + "target": "smithy.protocoltests.rpcv2Cbor#ClientOptionalDefaults" + }, + "topLevelDefault": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "hi" + } + }, + "otherTopLevelDefault": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 0 + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#OperationWithDefaultsOutput": { + "type": "structure", + "members": { + "defaultString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "hi" + } + }, + "defaultBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": true + } + }, + "defaultList": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringList", + "traits": { + "smithy.api#default": [] + } + }, + "defaultTimestamp": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#default": 0 + } + }, + "defaultBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "YWJj" + } + }, + "defaultByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 1 + } + }, + "defaultInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 10 + } + }, + "defaultLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 100 + } + }, + "defaultFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 1.0 + } + }, + "defaultMap": { + "target": "smithy.protocoltests.rpcv2Cbor#TestStringMap", + "traits": { + "smithy.api#default": {} + } + }, + "defaultEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestEnum", + "traits": { + "smithy.api#default": "FOO" + } + }, + "defaultIntEnum": { + "target": "smithy.protocoltests.rpcv2Cbor#TestIntEnum", + "traits": { + "smithy.api#default": 1 + } + }, + "emptyString": { + "target": "smithy.api#String", + "traits": { + "smithy.api#default": "" + } + }, + "falseBoolean": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#default": false + } + }, + "emptyBlob": { + "target": "smithy.api#Blob", + "traits": { + "smithy.api#default": "" + } + }, + "zeroByte": { + "target": "smithy.api#Byte", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroShort": { + "target": "smithy.api#Short", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroInteger": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroLong": { + "target": "smithy.api#Long", + "traits": { + "smithy.api#default": 0 + } + }, + "zeroFloat": { + "target": "smithy.api#Float", + "traits": { + "smithy.api#default": 0.0 + } + }, + "zeroDouble": { + "target": "smithy.api#Double", + "traits": { + "smithy.api#default": 0.0 + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#OptionalInputOutput": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleStructure" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleStructure" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "optional_input", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When input is empty we write CBOR equivalent of {}", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "forbidHeaders": [ + "X-Amz-Target" + ], + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/OptionalInputOutput", + "bodyMediaType": "application/cbor", + "body": "v/8=" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "optional_output", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "When output is empty we write CBOR equivalent of {}", + "body": "v/8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200 + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapes": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutput" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborRecursiveShapes", + "documentation": "Serializes recursive structures", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RecursiveShapes", + "body": "v2ZuZXN0ZWS/Y2Zvb2RGb28xZm5lc3RlZL9jYmFyZEJhcjFvcmVjdXJzaXZlTWVtYmVyv2Nmb29kRm9vMmZuZXN0ZWS/Y2JhcmRCYXIy//////8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "nested": { + "foo": "Foo1", + "nested": { + "bar": "Bar1", + "recursiveMember": { + "foo": "Foo2", + "nested": { + "bar": "Bar2" + } + } + } + } + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborRecursiveShapes", + "documentation": "Serializes recursive structures", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2ZuZXN0ZWS/Y2Zvb2RGb28xZm5lc3RlZL9jYmFyZEJhcjFvcmVjdXJzaXZlTWVtYmVyv2Nmb29kRm9vMmZuZXN0ZWS/Y2JhcmRCYXIy//////8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "nested": { + "foo": "Foo1", + "nested": { + "bar": "Bar1", + "recursiveMember": { + "foo": "Foo2", + "nested": { + "bar": "Bar2" + } + } + } + } + } + }, + { + "id": "RpcV2CborRecursiveShapesUsingDefiniteLength", + "documentation": "Deserializes recursive structures encoded using a map with definite length", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWZuZXN0ZWSiY2Zvb2RGb28xZm5lc3RlZKJjYmFyZEJhcjFvcmVjdXJzaXZlTWVtYmVyomNmb29kRm9vMmZuZXN0ZWShY2JhcmRCYXIy", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "nested": { + "foo": "Foo1", + "nested": { + "bar": "Bar1", + "recursiveMember": { + "foo": "Foo2", + "nested": { + "bar": "Bar2" + } + } + } + } + }, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutput": { + "type": "structure", + "members": { + "nested": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested1" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested1": { + "type": "structure", + "members": { + "foo": { + "target": "smithy.api#String" + }, + "nested": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested2" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested2": { + "type": "structure", + "members": { + "bar": { + "target": "smithy.api#String" + }, + "recursiveMember": { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapesInputOutputNested1" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMaps": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMapsInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMapsInputOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "The example tests basic map serialization.", + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborMaps", + "documentation": "Serializes maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborDenseMaps", + "body": "oW5kZW5zZVN0cnVjdE1hcKJjZm9voWJoaWV0aGVyZWNiYXqhYmhpY2J5ZQ==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "denseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborSerializesZeroValuesInMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborDenseMaps", + "body": "om5kZW5zZU51bWJlck1hcKFheABvZGVuc2VCb29sZWFuTWFwoWF49A==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "denseNumberMap": { + "x": 0 + }, + "denseBooleanMap": { + "x": false + } + } + }, + { + "id": "RpcV2CborSerializesDenseSetMap", + "documentation": "A request that contains a dense map of sets.", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborDenseMaps", + "body": "oWtkZW5zZVNldE1hcKJheIBheYJhYWFi", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "denseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborMaps", + "documentation": "Deserializes maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oW5kZW5zZVN0cnVjdE1hcKJjZm9voWJoaWV0aGVyZWNiYXqhYmhpY2J5ZQ==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborDeserializesZeroValuesInMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "om5kZW5zZU51bWJlck1hcKFheABvZGVuc2VCb29sZWFuTWFwoWF49A==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseNumberMap": { + "x": 0 + }, + "denseBooleanMap": { + "x": false + } + } + }, + { + "id": "RpcV2CborDeserializesDenseSetMap", + "documentation": "A response that contains a dense map of sets", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWtkZW5zZVNldE1hcKJheIBheYJhYWFi", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + }, + { + "id": "RpcV2CborDeserializesDenseSetMapAndSkipsNull", + "documentation": "Clients SHOULD tolerate seeing a null value in a dense map, and they SHOULD\ndrop the null key-value pair.", + "protocol": "smithy.protocols#rpcv2Cbor", + "appliesTo": "client", + "code": 200, + "body": "oWtkZW5zZVNldE1hcKNheIBheYJhYWFiYXr2", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "denseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMapsInputOutput": { + "type": "structure", + "members": { + "denseStructMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseStructMap" + }, + "denseNumberMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseNumberMap" + }, + "denseBooleanMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseBooleanMap" + }, + "denseStringMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseStringMap" + }, + "denseSetMap": { + "target": "smithy.protocoltests.rpcv2Cbor#DenseSetMap" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborListInputOutput": { + "type": "structure", + "members": { + "stringList": { + "target": "smithy.protocoltests.shared#StringList" + }, + "stringSet": { + "target": "smithy.protocoltests.shared#StringSet" + }, + "integerList": { + "target": "smithy.protocoltests.shared#IntegerList" + }, + "booleanList": { + "target": "smithy.protocoltests.shared#BooleanList" + }, + "timestampList": { + "target": "smithy.protocoltests.shared#TimestampList" + }, + "enumList": { + "target": "smithy.protocoltests.shared#FooEnumList" + }, + "intEnumList": { + "target": "smithy.protocoltests.shared#IntegerEnumList" + }, + "nestedStringList": { + "target": "smithy.protocoltests.shared#NestedStringList" + }, + "structureList": { + "target": "smithy.protocoltests.rpcv2Cbor#StructureList" + }, + "blobList": { + "target": "smithy.protocoltests.shared#BlobList" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborLists": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborListInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborListInputOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "This test case serializes JSON lists for the following cases for both\ninput and output:\n\n1. Normal lists.\n2. Normal sets.\n3. Lists of lists.\n4. Lists of structures.", + "smithy.api#idempotent": {}, + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborLists", + "documentation": "Serializes RpcV2 Cbor lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "v2pzdHJpbmdMaXN0gmNmb29jYmFyaXN0cmluZ1NldIJjZm9vY2JhcmtpbnRlZ2VyTGlzdIIBAmtib29sZWFuTGlzdIL19G10aW1lc3RhbXBMaXN0gsH7QdTX+/OAAADB+0HU1/vzgAAAaGVudW1MaXN0gmNGb29hMGtpbnRFbnVtTGlzdIIBAnBuZXN0ZWRTdHJpbmdMaXN0goJjZm9vY2JhcoJjYmF6Y3F1eG1zdHJ1Y3R1cmVMaXN0gqJhYWExYWJhMqJhYWEzYWJhNGhibG9iTGlzdIJDZm9vQ2Jhcv8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [ + "foo", + "bar" + ], + "stringSet": [ + "foo", + "bar" + ], + "integerList": [ + 1, + 2 + ], + "booleanList": [ + true, + false + ], + "timestampList": [ + 1398796238, + 1398796238 + ], + "enumList": [ + "Foo", + "0" + ], + "intEnumList": [ + 1, + 2 + ], + "nestedStringList": [ + [ + "foo", + "bar" + ], + [ + "baz", + "qux" + ] + ], + "structureList": [ + { + "a": "1", + "b": "2" + }, + { + "a": "3", + "b": "4" + } + ], + "blobList": [ + "foo", + "bar" + ] + } + }, + { + "id": "RpcV2CborListsEmpty", + "documentation": "Serializes empty JSON lists", + "tags": [ + "client-indefinite" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "v2pzdHJpbmdMaXN0n///", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [] + } + }, + { + "id": "RpcV2CborListsEmptyUsingDefiniteLength", + "documentation": "Serializes empty JSON definite length lists", + "tags": [ + "client-definite" + ], + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "oWpzdHJpbmdMaXN0gA==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [] + } + }, + { + "id": "RpcV2CborIndefiniteStringInsideIndefiniteList", + "documentation": "Can deserialize indefinite length text strings inside an indefinite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "v2pzdHJpbmdMaXN0n394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborIndefiniteStringInsideDefiniteList", + "documentation": "Can deserialize indefinite length text strings inside a definite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborLists", + "body": "oWpzdHJpbmdMaXN0g394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborLists", + "documentation": "Serializes RpcV2 Cbor lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2pzdHJpbmdMaXN0n2Nmb29jYmFy/2lzdHJpbmdTZXSfY2Zvb2NiYXL/a2ludGVnZXJMaXN0nwEC/2tib29sZWFuTGlzdJ/19P9tdGltZXN0YW1wTGlzdJ/B+0HU1/vzgAAAwftB1Nf784AAAP9oZW51bUxpc3SfY0Zvb2Ew/2tpbnRFbnVtTGlzdJ8BAv9wbmVzdGVkU3RyaW5nTGlzdJ+fY2Zvb2NiYXL/n2NiYXpjcXV4//9tc3RydWN0dXJlTGlzdJ+/YWFhMWFiYTL/v2FhYTNhYmE0//9oYmxvYkxpc3SfQ2Zvb0NiYXL//w==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [ + "foo", + "bar" + ], + "stringSet": [ + "foo", + "bar" + ], + "integerList": [ + 1, + 2 + ], + "booleanList": [ + true, + false + ], + "timestampList": [ + 1398796238, + 1398796238 + ], + "enumList": [ + "Foo", + "0" + ], + "intEnumList": [ + 1, + 2 + ], + "nestedStringList": [ + [ + "foo", + "bar" + ], + [ + "baz", + "qux" + ] + ], + "structureList": [ + { + "a": "1", + "b": "2" + }, + { + "a": "3", + "b": "4" + } + ], + "blobList": [ + "foo", + "bar" + ] + } + }, + { + "id": "RpcV2CborListsEmpty", + "documentation": "Serializes empty RpcV2 Cbor lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2pzdHJpbmdMaXN0n///", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [] + } + }, + { + "id": "RpcV2CborIndefiniteStringInsideIndefiniteListCanDeserialize", + "documentation": "Can deserialize indefinite length text strings inside an indefinite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2pzdHJpbmdMaXN0n394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborIndefiniteStringInsideDefiniteListCanDeserialize", + "documentation": "Can deserialize indefinite length text strings inside a definite length list", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "oWpzdHJpbmdMaXN0g394HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcsdyB3aGljaCB3aWxsIGJlIGNodW5rZWQsbiBvbiBlYWNoIGNvbW1h/394NUFub3RoZXIgZXhhbXBsZSBpbmRlZmluaXRlIHN0cmluZyB3aXRoIG9ubHkgb25lIGNodW5r/3ZUaGlzIGlzIGEgcGxhaW4gc3RyaW5n", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringList": [ + "An example indefinite string, which will be chunked, on each comma", + "Another example indefinite string with only one chunk", + "This is a plain string" + ] + }, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMaps": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMapsInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMapsInputOutput" + }, + "errors": [ + { + "target": "smithy.framework#ValidationException" + } + ], + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborSparseMaps", + "documentation": "Serializes sparse maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v29zcGFyc2VTdHJ1Y3RNYXC/Y2Zvb79iaGlldGhlcmX/Y2Jher9iaGljYnll////", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborSerializesNullMapValues", + "documentation": "Serializes null map values in sparse maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v3BzcGFyc2VCb29sZWFuTWFwv2F49v9vc3BhcnNlTnVtYmVyTWFwv2F49v9vc3BhcnNlU3RyaW5nTWFwv2F49v9vc3BhcnNlU3RydWN0TWFwv2F49v//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseBooleanMap": { + "x": null + }, + "sparseNumberMap": { + "x": null + }, + "sparseStringMap": { + "x": null + }, + "sparseStructMap": { + "x": null + } + } + }, + { + "id": "RpcV2CborSerializesSparseSetMap", + "documentation": "A request that contains a sparse map of sets", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v2xzcGFyc2VTZXRNYXC/YXif/2F5n2FhYWL///8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + }, + { + "id": "RpcV2CborSerializesSparseSetMapAndRetainsNull", + "documentation": "A request that contains a sparse map of sets.", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v2xzcGFyc2VTZXRNYXC/YXif/2F5n2FhYWL/YXr2//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ], + "z": null + } + } + }, + { + "id": "RpcV2CborSerializesZeroValuesInSparseMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", + "body": "v29zcGFyc2VOdW1iZXJNYXC/YXgA/3BzcGFyc2VCb29sZWFuTWFwv2F49P//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "sparseNumberMap": { + "x": 0 + }, + "sparseBooleanMap": { + "x": false + } + } + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborSparseJsonMaps", + "documentation": "Deserializes sparse maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v29zcGFyc2VTdHJ1Y3RNYXC/Y2Zvb79iaGlldGhlcmX/Y2Jher9iaGljYnll////", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseStructMap": { + "foo": { + "hi": "there" + }, + "baz": { + "hi": "bye" + } + } + } + }, + { + "id": "RpcV2CborDeserializesNullMapValues", + "documentation": "Deserializes null map values", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v3BzcGFyc2VCb29sZWFuTWFwv2F49v9vc3BhcnNlTnVtYmVyTWFwv2F49v9vc3BhcnNlU3RyaW5nTWFwv2F49v9vc3BhcnNlU3RydWN0TWFwv2F49v//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseBooleanMap": { + "x": null + }, + "sparseNumberMap": { + "x": null + }, + "sparseStringMap": { + "x": null + }, + "sparseStructMap": { + "x": null + } + } + }, + { + "id": "RpcV2CborDeserializesSparseSetMap", + "documentation": "A response that contains a sparse map of sets", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2xzcGFyc2VTZXRNYXC/YXmfYWFhYv9heJ////8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ] + } + } + }, + { + "id": "RpcV2CborDeserializesSparseSetMapAndRetainsNull", + "documentation": "A response that contains a sparse map of sets with a null", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v2xzcGFyc2VTZXRNYXC/YXif/2F5n2FhYWL/YXr2//8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseSetMap": { + "x": [], + "y": [ + "a", + "b" + ], + "z": null + } + } + }, + { + "id": "RpcV2CborDeserializesZeroValuesInSparseMaps", + "documentation": "Ensure that 0 and false are sent over the wire in all maps and lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v29zcGFyc2VOdW1iZXJNYXC/YXgA/3BzcGFyc2VCb29sZWFuTWFwv2F49P//", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseNumberMap": { + "x": 0 + }, + "sparseBooleanMap": { + "x": false + } + } + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMapsInputOutput": { + "type": "structure", + "members": { + "sparseStructMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseStructMap" + }, + "sparseNumberMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNumberMap" + }, + "sparseBooleanMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseBooleanMap" + }, + "sparseStringMap": { + "target": "smithy.protocoltests.shared#SparseStringMap" + }, + "sparseSetMap": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseSetMap" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol": { + "type": "service", + "version": "2020-07-14", + "operations": [ + { + "target": "smithy.protocoltests.rpcv2Cbor#EmptyInputOutput" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#Float16" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#FractionalSeconds" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#GreetingWithErrors" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#NoInputOutput" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#OperationWithDefaults" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#OptionalInputOutput" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RecursiveShapes" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborDenseMaps" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborLists" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#RpcV2CborSparseMaps" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleScalarProperties" + }, + { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNullsOperation" + } + ], + "traits": { + "smithy.api#title": "RpcV2 Protocol Service", + "smithy.protocols#rpcv2Cbor": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SimpleScalarProperties": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleScalarStructure" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#SimpleScalarStructure" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborSimpleScalarProperties", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Serializes simple scalar properties", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + } + }, + { + "id": "RpcV2CborSimpleScalarPropertiesUsingIndefiniteLength", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should be capable of deserializing simple scalar properties\nencoded using a map with a definite length. The server should also be able to parse\na key encoded using an indefinite length string.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "qmlieXRlVmFsdWUFf2Zkb3VibGVlVmFsdWX/+z/+OVgQYk3Tf2VmYWxzZWdCb29sZWFuZVZhbHVl//RqZmxvYXRWYWx1ZfpA9AAAbGludGVnZXJWYWx1ZRkBAGlsb25nVmFsdWUZJpFqc2hvcnRWYWx1ZRkmqn9mc3RyaW5nZVZhbHVl/2ZzaW1wbGVwdHJ1ZUJvb2xlYW5WYWx1ZfVpYmxvYlZhbHVlQ2Zvbw==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborClientDoesntSerializeNullStructureValues", + "documentation": "RpcV2 Cbor should not serialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v/8=", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": { + "stringValue": null + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborServerDoesntDeSerializeNullStructureValues", + "documentation": "RpcV2 Cbor should not deserialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tzdHJpbmdWYWx1Zfb/", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "params": {}, + "appliesTo": "server" + }, + { + "id": "RpcV2CborSupportsNaNFloatInputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling NaN float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zft/+AAAAAAAAGpmbG9hdFZhbHVl+n/AAAD/", + "params": { + "doubleValue": "NaN", + "floatValue": "NaN" + } + }, + { + "id": "RpcV2CborSupportsInfinityFloatInputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zft/8AAAAAAAAGpmbG9hdFZhbHVl+n+AAAD/", + "params": { + "doubleValue": "Infinity", + "floatValue": "Infinity" + } + }, + { + "id": "RpcV2CborSupportsNegativeInfinityFloatInputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zfv/8AAAAAAAAGpmbG9hdFZhbHVl+v+AAAD/", + "params": { + "doubleValue": "-Infinity", + "floatValue": "-Infinity" + } + }, + { + "id": "RpcV2CborIndefiniteLengthStringsCanBeDeserialized", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should be capable of deserializing indefinite length text strings.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "oWtzdHJpbmdWYWx1ZX94HUFuIGV4YW1wbGUgaW5kZWZpbml0ZSBzdHJpbmcscSBjaHVua2VkIG9uIGNvbW1h/w==", + "params": { + "stringValue": "An example indefinite string, chunked on comma" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborIndefiniteLengthByteStringsCanBeDeserialized", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should be capable of deserializing indefinite length byte strings.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "oWlibG9iVmFsdWVfWCJBbiBleGFtcGxlIGluZGVmaW5pdGUtYnl0ZSBzdHJpbmcsUSBjaHVua2VkIG9uIGNvbW1h/w==", + "params": { + "blobValue": "An example indefinite-byte string, chunked on comma" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborSupportsUpcastingData", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports upcasting from a smaller byte representation of the same data type.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2tkb3VibGVWYWx1Zfk+AGpmbG9hdFZhbHVl+UegbGludGVnZXJWYWx1ZRg4aWxvbmdWYWx1ZRkBAGpzaG9ydFZhbHVlCv8=", + "params": { + "doubleValue": 1.5, + "floatValue": 7.625, + "integerValue": 56, + "longValue": 256, + "shortValue": 10 + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborExtraFieldsInTheBodyShouldBeSkippedByServers", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The server should skip over additional fields that are not part of the structure. This allows a\nclient generated against a newer Smithy model to be able to communicate with a server that is\ngenerated against an older Smithy model.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABrZXh0cmFPYmplY3S/c2luZGVmaW5pdGVMZW5ndGhNYXC/a3dpdGhBbkFycmF5nwECA///cWRlZmluaXRlTGVuZ3RoTWFwo3J3aXRoQURlZmluaXRlQXJyYXmDAQIDeB1hbmRTb21lSW5kZWZpbml0ZUxlbmd0aFN0cmluZ3gfdGhhdCBoYXMsIGJlZW4gY2h1bmtlZCBvbiBjb21tYWxub3JtYWxTdHJpbmdjZm9vanNob3J0VmFsdWUZJw9uc29tZU90aGVyRmllbGR2dGhpcyBzaG91bGQgYmUgc2tpcHBlZP9saW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborServersShouldHandleNoAcceptHeader", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Servers should tolerate requests without an Accept header set.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "method": "POST", + "bodyMediaType": "application/cbor", + "uri": "/service/RpcV2Protocol/operation/SimpleScalarProperties", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "server" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborSimpleScalarProperties", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Serializes simple scalar properties", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "bodyMediaType": "application/cbor", + "body": "v3B0cnVlQm9vbGVhblZhbHVl9XFmYWxzZUJvb2xlYW5WYWx1ZfRpYnl0ZVZhbHVlBWtkb3VibGVWYWx1Zfs//jlYEGJN02pmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAanNob3J0VmFsdWUZJqprc3RyaW5nVmFsdWVmc2ltcGxlaWJsb2JWYWx1ZUNmb2//", + "code": 200, + "params": { + "trueBooleanValue": true, + "falseBooleanValue": false, + "byteValue": 5, + "doubleValue": 1.889, + "floatValue": 7.625, + "integerValue": 256, + "shortValue": 9898, + "stringValue": "simple", + "blobValue": "foo" + } + }, + { + "id": "RpcV2CborSimpleScalarPropertiesUsingDefiniteLength", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Deserializes simple scalar properties encoded using a map with definite length", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "bodyMediaType": "application/cbor", + "body": "qXB0cnVlQm9vbGVhblZhbHVl9XFmYWxzZUJvb2xlYW5WYWx1ZfRpYnl0ZVZhbHVlBWtkb3VibGVWYWx1Zfs//jlYEGJN02pmbG9hdFZhbHVl+kD0AABsaW50ZWdlclZhbHVlGQEAanNob3J0VmFsdWUZJqprc3RyaW5nVmFsdWVmc2ltcGxlaWJsb2JWYWx1ZUNmb28=", + "code": 200, + "params": { + "trueBooleanValue": true, + "falseBooleanValue": false, + "byteValue": 5, + "doubleValue": 1.889, + "floatValue": 7.625, + "integerValue": 256, + "shortValue": 9898, + "stringValue": "simple", + "blobValue": "foo" + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborClientDoesntDeserializeNullStructureValues", + "documentation": "RpcV2 Cbor should not deserialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "body": "v2tzdHJpbmdWYWx1Zfb/", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": {}, + "appliesTo": "client" + }, + { + "id": "RpcV2CborServerDoesntSerializeNullStructureValues", + "documentation": "RpcV2 Cbor should not serialize null structure values", + "protocol": "smithy.protocols#rpcv2Cbor", + "body": "v/8=", + "code": 200, + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "stringValue": null + }, + "appliesTo": "server" + }, + { + "id": "RpcV2CborSupportsNaNFloatOutputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling NaN float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zft/+AAAAAAAAGpmbG9hdFZhbHVl+n/AAAD/", + "params": { + "doubleValue": "NaN", + "floatValue": "NaN" + } + }, + { + "id": "RpcV2CborSupportsInfinityFloatOutputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zft/8AAAAAAAAGpmbG9hdFZhbHVl+n+AAAD/", + "params": { + "doubleValue": "Infinity", + "floatValue": "Infinity" + } + }, + { + "id": "RpcV2CborSupportsNegativeInfinityFloatOutputs", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports handling Negative Infinity float values.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zfv/8AAAAAAAAGpmbG9hdFZhbHVl+v+AAAD/", + "params": { + "doubleValue": "-Infinity", + "floatValue": "-Infinity" + } + }, + { + "id": "RpcV2CborSupportsUpcastingDataOnDeserialize", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "Supports upcasting from a smaller byte representation of the same data type.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2tkb3VibGVWYWx1Zfk+AGpmbG9hdFZhbHVl+UegbGludGVnZXJWYWx1ZRg4aWxvbmdWYWx1ZRkBAGpzaG9ydFZhbHVlCv8=", + "params": { + "doubleValue": 1.5, + "floatValue": 7.625, + "integerValue": 56, + "longValue": 256, + "shortValue": 10 + }, + "appliesTo": "client" + }, + { + "id": "RpcV2CborExtraFieldsInTheBodyShouldBeSkippedByClients", + "protocol": "smithy.protocols#rpcv2Cbor", + "documentation": "The client should skip over additional fields that are not part of the structure. This allows a\nclient generated against an older Smithy model to be able to communicate with a server that is\ngenerated against a newer Smithy model.", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "code": 200, + "bodyMediaType": "application/cbor", + "body": "v2lieXRlVmFsdWUFa2RvdWJsZVZhbHVl+z/+OVgQYk3TcWZhbHNlQm9vbGVhblZhbHVl9GpmbG9hdFZhbHVl+kD0AABrZXh0cmFPYmplY3S/c2luZGVmaW5pdGVMZW5ndGhNYXC/a3dpdGhBbkFycmF5nwECA///cWRlZmluaXRlTGVuZ3RoTWFwo3J3aXRoQURlZmluaXRlQXJyYXmDAQIDeB1hbmRTb21lSW5kZWZpbml0ZUxlbmd0aFN0cmluZ3gfdGhhdCBoYXMsIGJlZW4gY2h1bmtlZCBvbiBjb21tYWxub3JtYWxTdHJpbmdjZm9vanNob3J0VmFsdWUZJw9uc29tZU90aGVyRmllbGR2dGhpcyBzaG91bGQgYmUgc2tpcHBlZP9saW50ZWdlclZhbHVlGQEAaWxvbmdWYWx1ZRkmkWpzaG9ydFZhbHVlGSaqa3N0cmluZ1ZhbHVlZnNpbXBsZXB0cnVlQm9vbGVhblZhbHVl9WlibG9iVmFsdWVDZm9v/w==", + "params": { + "byteValue": 5, + "doubleValue": 1.889, + "falseBooleanValue": false, + "floatValue": 7.625, + "integerValue": 256, + "longValue": 9873, + "shortValue": 9898, + "stringValue": "simple", + "trueBooleanValue": true, + "blobValue": "foo" + }, + "appliesTo": "client" + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#SimpleScalarStructure": { + "type": "structure", + "members": { + "trueBooleanValue": { + "target": "smithy.api#Boolean" + }, + "falseBooleanValue": { + "target": "smithy.api#Boolean" + }, + "byteValue": { + "target": "smithy.api#Byte" + }, + "doubleValue": { + "target": "smithy.api#Double" + }, + "floatValue": { + "target": "smithy.api#Float" + }, + "integerValue": { + "target": "smithy.api#Integer" + }, + "longValue": { + "target": "smithy.api#Long" + }, + "shortValue": { + "target": "smithy.api#Short" + }, + "stringValue": { + "target": "smithy.api#String" + }, + "blobValue": { + "target": "smithy.api#Blob" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#SimpleStructure": { + "type": "structure", + "members": { + "value": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseBooleanMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Boolean" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseNullsOperation": { + "type": "operation", + "input": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNullsOperationInputOutput" + }, + "output": { + "target": "smithy.protocoltests.rpcv2Cbor#SparseNullsOperationInputOutput" + }, + "traits": { + "smithy.test#httpRequestTests": [ + { + "id": "RpcV2CborSparseMapsSerializeNullValues", + "documentation": "Serializes null values in maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v29zcGFyc2VTdHJpbmdNYXC/Y2Zvb/b//w==", + "params": { + "sparseStringMap": { + "foo": null + } + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SparseNullsOperation" + }, + { + "id": "RpcV2CborSparseListsSerializeNull", + "documentation": "Serializes null values in lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor", + "Accept": "application/cbor" + }, + "requireHeaders": [ + "Content-Length" + ], + "body": "v3BzcGFyc2VTdHJpbmdMaXN0n/b//w==", + "params": { + "sparseStringList": [ + null + ] + }, + "method": "POST", + "uri": "/service/RpcV2Protocol/operation/SparseNullsOperation" + } + ], + "smithy.test#httpResponseTests": [ + { + "id": "RpcV2CborSparseMapsDeserializeNullValues", + "documentation": "Deserializes null values in maps", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v29zcGFyc2VTdHJpbmdNYXC/Y2Zvb/b//w==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseStringMap": { + "foo": null + } + } + }, + { + "id": "RpcV2CborSparseListsDeserializeNull", + "documentation": "Deserializes null values in lists", + "protocol": "smithy.protocols#rpcv2Cbor", + "code": 200, + "body": "v3BzcGFyc2VTdHJpbmdMaXN0n/b//w==", + "bodyMediaType": "application/cbor", + "headers": { + "smithy-protocol": "rpc-v2-cbor", + "Content-Type": "application/cbor" + }, + "params": { + "sparseStringList": [ + null + ] + } + } + ] + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseNullsOperationInputOutput": { + "type": "structure", + "members": { + "sparseStringList": { + "target": "smithy.protocoltests.shared#SparseStringList" + }, + "sparseStringMap": { + "target": "smithy.protocoltests.shared#SparseStringMap" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseNumberMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Integer" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseSetMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#StringSet" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#SparseStructMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.protocoltests.shared#GreetingStruct" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.rpcv2Cbor#StructureList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.rpcv2Cbor#StructureListMember" + } + }, + "smithy.protocoltests.rpcv2Cbor#StructureListMember": { + "type": "structure", + "members": { + "a": { + "target": "smithy.api#String" + }, + "b": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.rpcv2Cbor#TestEnum": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FOO" + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAR" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAZ" + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#TestIntEnum": { + "type": "intEnum", + "members": { + "ONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 1 + } + }, + "TWO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 2 + } + } + } + }, + "smithy.protocoltests.rpcv2Cbor#TestStringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.rpcv2Cbor#TestStringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.shared#BlobList": { + "type": "list", + "member": { + "target": "smithy.api#Blob" + } + }, + "smithy.protocoltests.shared#BooleanList": { + "type": "list", + "member": { + "target": "smithy.api#Boolean" + } + }, + "smithy.protocoltests.shared#DateTime": { + "type": "timestamp", + "traits": { + "smithy.api#timestampFormat": "date-time" + } + }, + "smithy.protocoltests.shared#FooEnum": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Foo" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Baz" + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Bar" + } + }, + "ONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "1" + } + }, + "ZERO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "0" + } + } + } + }, + "smithy.protocoltests.shared#FooEnumList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.shared#FooEnum" + } + }, + "smithy.protocoltests.shared#GreetingStruct": { + "type": "structure", + "members": { + "hi": { + "target": "smithy.api#String" + } + } + }, + "smithy.protocoltests.shared#IntegerEnum": { + "type": "intEnum", + "members": { + "A": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 1 + } + }, + "B": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 2 + } + }, + "C": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 3 + } + } + } + }, + "smithy.protocoltests.shared#IntegerEnumList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.shared#IntegerEnum" + } + }, + "smithy.protocoltests.shared#IntegerList": { + "type": "list", + "member": { + "target": "smithy.api#Integer" + } + }, + "smithy.protocoltests.shared#NestedStringList": { + "type": "list", + "member": { + "target": "smithy.protocoltests.shared#StringList" + }, + "traits": { + "smithy.api#documentation": "A list of lists of strings." + } + }, + "smithy.protocoltests.shared#SparseStringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.shared#SparseStringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#sparse": {} + } + }, + "smithy.protocoltests.shared#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + } + }, + "smithy.protocoltests.shared#StringSet": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#uniqueItems": {} + } + }, + "smithy.protocoltests.shared#TimestampList": { + "type": "list", + "member": { + "target": "smithy.api#Timestamp" + } + }, + "smithy.test#AppliesTo": { + "type": "enum", + "members": { + "CLIENT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "The test only applies to client implementations.", + "smithy.api#enumValue": "client" + } + }, + "SERVER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "The test only applies to server implementations.", + "smithy.api#enumValue": "server" + } + } + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#HttpRequestTestCase": { + "type": "structure", + "members": { + "id": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The identifier of the test case. This identifier can be used by\nprotocol test implementations to filter out unsupported test\ncases by ID, to generate test case names, etc. The provided `id`\nMUST match Smithy's `identifier` ABNF. No two `httpRequestTests`\ntest cases can share the same ID.", + "smithy.api#pattern": "^[A-Za-z_][A-Za-z0-9_]+$", + "smithy.api#required": {} + } + }, + "protocol": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The name of the protocol to test.", + "smithy.api#idRef": { + "selector": "[trait|protocolDefinition]", + "failWhenMissing": true + }, + "smithy.api#required": {} + } + }, + "method": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The expected serialized HTTP request method.", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#required": {} + } + }, + "uri": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The request-target of the HTTP request, not including\nthe query string (for example, \"/foo/bar\").", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#required": {} + } + }, + "host": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The host / endpoint provided to the client, not including the path\nor scheme (for example, \"example.com\")." + } + }, + "resolvedHost": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The host / endpoint that the client should send to, not including\nthe path or scheme (for example, \"prefix.example.com\").\n\nThis can differ from the host provided to the client if the `hostPrefix`\nmember of the `endpoint` trait is set, for instance." + } + }, + "authScheme": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The optional authentication scheme shape ID to assume. It's\npossible that specific authentication schemes might influence\nthe serialization logic of an HTTP request.", + "smithy.api#idRef": { + "selector": "[trait|authDefinition]", + "failWhenMissing": true + } + } + }, + "queryParams": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of the expected serialized query string parameters.\n\nEach element in the list is a query string key value pair\nthat starts with the query string parameter name optionally\nfollowed by \"=\", optionally followed by the query string\nparameter value. For example, \"foo=bar\", \"foo=\", and \"foo\"\nare all valid values. The query string parameter name and\nthe value MUST appear in the format in which it is expected\nto be sent over the wire; if a key or value needs to be\npercent-encoded, then it MUST appear percent-encoded in this list.\n\nA serialized HTTP request is not in compliance with the protocol\nif any query string parameter defined in `queryParams` is not\ndefined in the request or if the value of a query string parameter\nin the request differs from the expected value.\n\n`queryParams` applies no constraints on additional query parameters." + } + }, + "forbidQueryParams": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of query string parameter names that must not appear in the\nserialized HTTP request.\n\nEach value MUST appear in the format in which it is sent over the\nwire; if a key needs to be percent-encoded, then it MUST appear\npercent-encoded in this list." + } + }, + "requireQueryParams": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of query string parameter names that MUST appear in the\nserialized request URI, but no assertion is made on the value.\n\nEach value MUST appear in the format in which it is sent over the\nwire; if a key needs to be percent-encoded, then it MUST appear\npercent-encoded in this list." + } + }, + "headers": { + "target": "smithy.test#StringMap", + "traits": { + "smithy.api#documentation": "Defines a map of expected HTTP headers.\n\nHeaders that are not listed in this map are ignored unless they are\nexplicitly forbidden through `forbidHeaders`." + } + }, + "forbidHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must not appear in the serialized\nHTTP request." + } + }, + "requireHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must appear in the serialized\nHTTP message, but no assertion is made on the value.\n\nHeaders listed in `headers` do not need to appear in this list." + } + }, + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The expected HTTP message body.\n\nIf no request body is defined, then no assertions are made about\nthe body of the message." + } + }, + "bodyMediaType": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The media type of the `body`.\n\nThis is used to help test runners to parse and validate the expected\ndata against generated data." + } + }, + "params": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines the input parameters used to generated the HTTP request.\n\nThese parameters MUST be compatible with the input of the operation." + } + }, + "vendorParams": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines vendor-specific parameters that are used to influence the\nrequest. For example, some vendors might utilize environment\nvariables, configuration files on disk, or other means to influence\nthe serialization formats used by clients or servers.\n\nIf a `vendorParamsShape` is set, these parameters MUST be compatible\nwith that shape's definition." + } + }, + "vendorParamsShape": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A shape to be used to validate the `vendorParams` member contents.\n\nIf set, the parameters in `vendorParams` MUST be compatible with this\nshape's definition.", + "smithy.api#idRef": { + "failWhenMissing": true + } + } + }, + "documentation": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A description of the test and what is being asserted." + } + }, + "tags": { + "target": "smithy.test#NonEmptyStringList", + "traits": { + "smithy.api#documentation": "Applies a list of tags to the test." + } + }, + "appliesTo": { + "target": "smithy.test#AppliesTo", + "traits": { + "smithy.api#documentation": "Indicates that the test case is only to be implemented by \"client\" or\n\"server\" implementations. This property is useful for identifying and\ntesting edge cases of clients and servers that are impossible or\nundesirable to test in *both* client and server implementations." + } + } + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#HttpResponseTestCase": { + "type": "structure", + "members": { + "id": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The identifier of the test case. This identifier can be used by\nprotocol test implementations to filter out unsupported test\ncases by ID, to generate test case names, etc. The provided `id`\nMUST match Smithy's `identifier` ABNF. No two `httpResponseTests`\ntest cases can share the same ID.", + "smithy.api#pattern": "^[A-Za-z_][A-Za-z0-9_]+$", + "smithy.api#required": {} + } + }, + "protocol": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The shape ID of the protocol to test.", + "smithy.api#idRef": { + "selector": "[trait|protocolDefinition]", + "failWhenMissing": true + }, + "smithy.api#required": {} + } + }, + "code": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#documentation": "Defines the HTTP response code.", + "smithy.api#range": { + "min": 100, + "max": 599 + }, + "smithy.api#required": {} + } + }, + "authScheme": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The optional authentication scheme shape ID to assume. It's possible\nthat specific authentication schemes might influence the serialization\nlogic of an HTTP response.", + "smithy.api#idRef": { + "selector": "[trait|authDefinition]", + "failWhenMissing": true + } + } + }, + "headers": { + "target": "smithy.test#StringMap", + "traits": { + "smithy.api#documentation": "A map of expected HTTP headers. Each key represents a header field\nname and each value represents the expected header value. An HTTP\nresponse is not in compliance with the protocol if any listed header\nis missing from the serialized response or if the expected header\nvalue differs from the serialized response value.\n\n`headers` applies no constraints on additional headers." + } + }, + "forbidHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must not appear." + } + }, + "requireHeaders": { + "target": "smithy.test#StringList", + "traits": { + "smithy.api#documentation": "A list of header field names that must appear in the serialized\nHTTP message, but no assertion is made on the value.\n\nHeaders listed in `headers` map do not need to appear in this list." + } + }, + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Defines the HTTP message body.\n\nIf no response body is defined, then no assertions are made about\nthe body of the message." + } + }, + "bodyMediaType": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The media type of the `body`.\n\nThis is used to help test runners to parse and validate the expected\ndata against generated data. Binary media type formats require that\nthe contents of `body` are base64 encoded." + } + }, + "params": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines the output parameters deserialized from the HTTP response.\n\nThese parameters MUST be compatible with the output of the operation." + } + }, + "vendorParams": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "Defines vendor-specific parameters that are used to influence the\nresponse. For example, some vendors might utilize environment\nvariables, configuration files on disk, or other means to influence\nthe serialization formats used by clients or servers.\n\nIf a `vendorParamsShape` is set, these parameters MUST be compatible\nwith that shape's definition." + } + }, + "vendorParamsShape": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A shape to be used to validate the `vendorParams` member contents.\n\nIf set, the parameters in `vendorParams` MUST be compatible with this\nshape's definition.", + "smithy.api#idRef": { + "failWhenMissing": true + } + } + }, + "documentation": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "A description of the test and what is being asserted." + } + }, + "tags": { + "target": "smithy.test#NonEmptyStringList", + "traits": { + "smithy.api#documentation": "Applies a list of tags to the test." + } + }, + "appliesTo": { + "target": "smithy.test#AppliesTo", + "traits": { + "smithy.api#documentation": "Indicates that the test case is only to be implemented by \"client\" or\n\"server\" implementations. This property is useful for identifying and\ntesting edge cases of clients and servers that are impossible or\nundesirable to test in *both* client and server implementations." + } + } + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#NonEmptyString": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1 + }, + "smithy.api#private": {} + } + }, + "smithy.test#NonEmptyStringList": { + "type": "list", + "member": { + "target": "smithy.test#NonEmptyString" + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#StringList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#StringMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#private": {} + } + }, + "smithy.test#httpRequestTests": { + "type": "list", + "member": { + "target": "smithy.test#HttpRequestTestCase" + }, + "traits": { + "smithy.api#documentation": "Define how an HTTP request is serialized given a specific protocol,\nauthentication scheme, and set of input parameters.", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#trait": { + "selector": "operation" + } + } + }, + "smithy.test#httpResponseTests": { + "type": "list", + "member": { + "target": "smithy.test#HttpResponseTestCase" + }, + "traits": { + "smithy.api#documentation": "Define how an HTTP response is serialized given a specific protocol,\nauthentication scheme, and set of output or error parameters.", + "smithy.api#length": { + "min": 1 + }, + "smithy.api#trait": { + "selector": ":test(operation, structure[trait|error])" + } + } + } + } +} diff --git a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift new file mode 100644 index 000000000..131e07c78 --- /dev/null +++ b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift @@ -0,0 +1,34 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import Smithy +@testable import SmithyCodegenCore + +class SmithyCodegenCoreTests: XCTestCase { + + // This test runs against the real model for smithy-rpcv2-cbor protocol tests, + // which is embedded in this test bundle as a resource. + // + // When the code generator runs as part of the build system, + // the debugger does not attach and the only indication you have of what went wrong + // is the error message that gets written to the build logs. + // + // Running the code generator from here allows the debugger to attach so that you + // can debug a code generation step that is failing during builds. + func test_generates_rpcv2_cbor_protocol() throws { + let tempDirURL = FileManager.default.temporaryDirectory + let generator = try CodeGenerator( + service: "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", + modelFileURL: Bundle.module.url(forResource: "smithy-rpcv2-cbor", withExtension: "json")!, + schemasFileURL: tempDirURL.appendingPathComponent("Schemas.swift"), + serializeFileURL: tempDirURL.appendingPathComponent("Serialize.swift"), + deserializeFileURL: tempDirURL.appendingPathComponent("Deserialize.swift") + ) + try generator.run() + } +} 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 6ea786756..25156c711 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt @@ -96,6 +96,9 @@ class DirectedSwiftCodegen( LOGGER.info("[${service.id}] Generating additional files") integrations.forEach { it.writeAdditionalFiles(context, ctx, writers) } + + LOGGER.info("[${service.id}] Generating Smithy model file info") + SmithyModelFileInfoGenerator(ctx).writeSmithyModelFileInfo() } LOGGER.info("[${service.id}] Generating package manifest file") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt new file mode 100644 index 000000000..6daae0e29 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt @@ -0,0 +1,20 @@ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator + +class SmithyModelFileInfoGenerator( + val ctx: ProtocolGenerator.GenerationContext, +) { + fun writeSmithyModelFileInfo() { + if (ctx.settings.moduleName.startsWith("Internal")) return + val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" + ctx.delegator.useFileWriter(filename) { writer -> + val service = ctx.settings.service + val path = ctx.settings.modelPath + writer.openBlock("{", "}") { + writer.write("\"service\": \"$service\",") + writer.write("\"path\": \"$path\"") + } + } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt index 41360ae2b..9c2397927 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSettings.kt @@ -21,9 +21,12 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.protocol.traits.Rpcv2CborTrait +import software.amazon.smithy.swift.codegen.utils.clientName +import software.amazon.smithy.swift.codegen.utils.sdkId import java.util.logging.Logger import kotlin.collections.listOf import kotlin.jvm.optionals.getOrElse +import kotlin.jvm.optionals.getOrNull import kotlin.streams.toList private const val SERVICE = "service" @@ -42,6 +45,7 @@ private const val COPYRIGHT_NOTICE = "copyrightNotice" private const val VISIBILITY = "visibility" private const val INTERNAL_CLIENT = "internalClient" private const val OPERATIONS = "operations" +private const val MODEL_PATH = "modelPath" // Prioritized list of protocols supported for code generation private val DEFAULT_PROTOCOL_RESOLUTION_PRIORITY = @@ -70,6 +74,7 @@ class SwiftSettings( val visibility: String, val internalClient: Boolean, val operations: List, + val modelPath: String, ) { companion object { private val LOGGER: Logger = Logger.getLogger(SwiftSettings::class.java.name) @@ -102,6 +107,7 @@ class SwiftSettings( VISIBILITY, INTERNAL_CLIENT, OPERATIONS, + MODEL_PATH, ), ) @@ -110,6 +116,7 @@ class SwiftSettings( .getStringMember(SERVICE) .map(StringNode::expectShapeId) .orElseGet { inferService(model) } + val service = model.getShape(serviceId).getOrNull() as? ServiceShape val moduleName = config.expectStringMember(MODULE_NAME).value val version = config.expectStringMember(MODULE_VERSION).value @@ -118,7 +125,7 @@ class SwiftSettings( val author = config.expectStringMember(AUTHOR).value val gitRepo = config.expectStringMember(GIT_REPO).value val swiftVersion = config.expectStringMember(SWIFT_VERSION).value - val sdkId = sanitizeSdkId(config.getStringMemberOrDefault(SDK_ID, serviceId.name)) + val sdkId = config.getStringMemberOrDefault(SDK_ID, service?.sdkId ?: serviceId.name) val mergeModels = config.getBooleanMemberOrDefault(MERGE_MODELS) val copyrightNotice = config.getStringMemberOrDefault( @@ -128,6 +135,7 @@ class SwiftSettings( val visibility = config.getStringMemberOrDefault(VISIBILITY, "public") val internalClient = config.getBooleanMemberOrDefault(INTERNAL_CLIENT, false) val operations = config.getArrayMember(OPERATIONS).getOrElse { Node.arrayNode() }.map { it.expectStringNode().value } + val modelPath = config.getStringMemberOrDefault(MODEL_PATH, "Sources/$moduleName/model.json") return SwiftSettings( serviceId, @@ -144,11 +152,10 @@ class SwiftSettings( visibility, internalClient, operations, + modelPath, ) } - private fun sanitizeSdkId(sdkId: String): String = sdkId.removeSuffix(" Service") - // infer the service to generate from a model private fun inferService(model: Model): ShapeId { val services = @@ -227,13 +234,20 @@ class SwiftSettings( "The following protocol generators were found on the class path: $supportedProtocolTraits", ) } + + val testModuleName: String + get() = "${moduleName}Tests" + + val sdkIdStrippingService: String + get() = sdkId.removeSuffix(" Service") + + val clientBaseNamePreservingService: String + get() = sdkId.clientName() + + val clientBaseName: String + get() = sdkIdStrippingService.clientName() } class UnresolvableProtocolException( message: String, ) : CodegenException(message) - -val SwiftSettings.testModuleName: String - get() { - return "${this.moduleName}Tests" - } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt index cf51fd5c3..cbd1249bb 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt @@ -64,7 +64,6 @@ import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTimestampsTypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes import software.amazon.smithy.swift.codegen.utils.ModelFileUtils -import software.amazon.smithy.swift.codegen.utils.clientName import software.amazon.smithy.swift.codegen.utils.toLowerCamelCase import software.amazon.smithy.utils.StringUtils.lowerCase import java.util.logging.Logger @@ -79,7 +78,6 @@ class SwiftSymbolProvider( val swiftSettings: SwiftSettings, ) : SymbolProvider, ShapeVisitor { - private val sdkId = swiftSettings.sdkId private val service: ServiceShape? = try { swiftSettings.getService(model) @@ -253,7 +251,7 @@ class SwiftSymbolProvider( .build() override fun serviceShape(shape: ServiceShape): Symbol { - val name = sdkId.clientName() + val name = swiftSettings.clientBaseName return createSymbolBuilder(shape, "${name}Client", SwiftDeclaration.CLASS) .definitionFile(formatModuleName(name)) .build() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt index ac2fab27f..8063a727a 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 @@ -57,6 +58,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 @@ -71,6 +73,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 @@ -137,6 +140,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)) { @@ -185,12 +189,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 } @@ -200,10 +206,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 } @@ -361,6 +399,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/HttpProtocolClientGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt index fed20bd5b..c70774321 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt @@ -13,7 +13,6 @@ import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.middleware.MiddlewareExecutionGenerator import software.amazon.smithy.swift.codegen.middleware.OperationMiddleware import software.amazon.smithy.swift.codegen.model.toUpperCamelCase -import software.amazon.smithy.swift.codegen.utils.toUpperCamelCase /** * Renders an implementation of a service interface for HTTP protocol @@ -46,10 +45,10 @@ open class HttpProtocolClientGenerator( val operationsIndex = OperationIndex.of(model) writer.openBlock("extension \$L {", "}", serviceSymbol.name) { - val serviceName = ctx.settings.sdkId.toUpperCamelCase() + val clientName = ctx.settings.clientBaseName operations.forEach { operation -> ServiceGenerator.renderOperationDefinition( - serviceName, + clientName, model, serviceShape, symbolProvider, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt index fd42f858d..f5fd9456c 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolServiceClient.kt @@ -21,7 +21,7 @@ open class HttpProtocolServiceClient( private val writer: SwiftWriter, private val serviceConfig: ServiceConfig, ) { - private val serviceName: String = ctx.settings.sdkId + private val serviceName: String = ctx.settings.sdkIdStrippingService open val clientProtocolSymbol: Symbol = ClientRuntimeTypes.Core.Client diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt index 0ff2e5ad1..00869532d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt @@ -14,7 +14,6 @@ import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait import software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.model.toUpperCamelCase -import software.amazon.smithy.swift.codegen.testModuleName import java.util.TreeSet import java.util.logging.Logger diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/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/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt index ea7ca26da..feb94f0ba 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/protocols/rpcv2cbor/SmithyHTTPBindingProtocolGenerator.kt @@ -21,7 +21,6 @@ import software.amazon.smithy.swift.codegen.integration.SmithyHttpProtocolClient import software.amazon.smithy.swift.codegen.integration.middlewares.OperationEndpointResolverMiddleware import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable import software.amazon.smithy.swift.codegen.model.getTrait -import software.amazon.smithy.swift.codegen.testModuleName abstract class SmithyHTTPBindingProtocolGenerator( customizations: HTTPProtocolCustomizable, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/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" + } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/ServiceShapeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/ServiceShapeUtils.kt new file mode 100644 index 000000000..74c1743b8 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/ServiceShapeUtils.kt @@ -0,0 +1,9 @@ +package software.amazon.smithy.swift.codegen.utils + +import software.amazon.smithy.aws.traits.ServiceTrait +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.swift.codegen.model.getTrait + +// Utility function for returning sdkId from service +val ServiceShape.sdkId: String? + get() = getTrait()?.sdkId diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt index 1eb8dfb64..f8602de58 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/codegencomponents/SwiftSettingsTest.kt @@ -195,6 +195,7 @@ class SwiftSettingsTest { visibility = "public", internalClient = false, emptyList(), + modelPath = "/path/to/model.json", ) private fun createServiceWithProtocols(protocols: Set): ServiceShape {