diff --git a/Package.swift b/Package.swift index 828214760..a6ea3fb89 100644 --- a/Package.swift +++ b/Package.swift @@ -308,7 +308,9 @@ let package = Package( ] ), .target( - name: "SmithyCodegenCore" + name: "SmithyCodegenCore", + dependencies: ["SmithySerialization"], + resources: [ .process("Resources") ] ), .testTarget( name: "ClientRuntimeTests", @@ -397,5 +399,9 @@ let package = Package( name: "SmithyStreamsTests", dependencies: ["SmithyStreams", "Smithy"] ), + .testTarget( + name: "SmithyCodegenCoreTests", + dependencies: ["SmithyCodegenCore"] + ), ].compactMap { $0 } ) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 180e3bb5e..560ce9ced 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -52,16 +52,20 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Construct the schemas.swift path. let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") + // Construct the structconsumers.swift path. + let serializableStructsSwiftPath = outputDirectoryPath.appending("\(name)SerializableStructs.swift") + // Construct the build command that invokes SmithyCodegenCLI. return .buildCommand( displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", executable: generatorToolPath, arguments: [ "--schemas-path", schemasSwiftPath, + "--serializable-structs-path", serializableStructsSwiftPath, modelPath ], inputFiles: [inputPath, modelPath], - outputFiles: [schemasSwiftPath] + outputFiles: [schemasSwiftPath, serializableStructsSwiftPath] ) } } diff --git a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift index 4868cc7ee..f6dd0d46a 100644 --- a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift +++ b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift @@ -13,10 +13,10 @@ import protocol SmithyHTTPAuthAPI.AuthSchemeResolver import protocol SmithyHTTPAuthAPI.AuthSchemeResolverParameters import struct SmithyRetries.DefaultRetryStrategy import struct SmithyRetries.ExponentialBackoffStrategy -import SmithyTelemetryAPI import protocol SmithyRetriesAPI.RetryErrorInfoProvider import protocol SmithyRetriesAPI.RetryStrategy import struct SmithyRetriesAPI.RetryStrategyOptions +import SmithyTelemetryAPI public struct DefaultSDKRuntimeConfiguration { diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 1b61a9467..8658bb00d 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -19,8 +19,8 @@ import protocol SmithyHTTPAPI.HTTPClient import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPResponse import enum SmithyHTTPAPI.HTTPStatusCode -import class SmithyHTTPClientAPI.HttpTelemetry import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry import class SmithyStreams.BufferedStream import SmithyTelemetryAPI #if os(Linux) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift index 7d871a63e..492f9cde9 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift @@ -8,8 +8,8 @@ import AwsCommonRuntimeKit import struct Smithy.Attributes import enum Smithy.ByteStream -import class SmithyHTTPClientAPI.HttpTelemetry import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry extension HTTP2Stream { /// Returns the recommended size, in bytes, for the data to write diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift b/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift index a5c420e18..54044b683 100644 --- a/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift +++ b/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift @@ -13,8 +13,6 @@ import class Foundation.DispatchQueue import class Foundation.InputStream import class Foundation.NSObject import class Foundation.OutputStream -import class SmithyHTTPClientAPI.HttpTelemetry -import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys import class Foundation.RunLoop import class Foundation.Stream import protocol Foundation.StreamDelegate @@ -24,6 +22,8 @@ import class Foundation.Timer import struct Smithy.Attributes import protocol Smithy.LogAgent import protocol Smithy.ReadableStream +import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry /// Reads data from a smithy-swift native `ReadableStream` and streams the data through to a Foundation `InputStream`. /// diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift index 2a0c6ea20..aab80dc2a 100644 --- a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift +++ b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift @@ -18,8 +18,6 @@ import class Foundation.NSRecursiveLock import var Foundation.NSURLAuthenticationMethodClientCertificate import var Foundation.NSURLAuthenticationMethodServerTrust import struct Foundation.TimeInterval -import class SmithyHTTPClientAPI.HttpTelemetry -import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys import class Foundation.URLAuthenticationChallenge import struct Foundation.URLComponents import class Foundation.URLCredential @@ -29,7 +27,6 @@ import class Foundation.URLResponse import class Foundation.URLSession import class Foundation.URLSessionConfiguration import protocol Foundation.URLSessionDataDelegate -import SmithyTelemetryAPI import class Foundation.URLSessionDataTask import class Foundation.URLSessionTask import class Foundation.URLSessionTaskMetrics @@ -44,7 +41,10 @@ import protocol SmithyHTTPAPI.HTTPClient import class SmithyHTTPAPI.HTTPRequest import class SmithyHTTPAPI.HTTPResponse import enum SmithyHTTPAPI.HTTPStatusCode +import enum SmithyHTTPClientAPI.HttpMetricsAttributesKeys +import class SmithyHTTPClientAPI.HttpTelemetry import class SmithyStreams.BufferedStream +import SmithyTelemetryAPI /// A client that can be used to make requests to AWS services using `Foundation`'s `URLSession` HTTP client. /// diff --git a/Sources/Smithy/Schema/Node.swift b/Sources/Smithy/Node.swift similarity index 100% rename from Sources/Smithy/Schema/Node.swift rename to Sources/Smithy/Node.swift diff --git a/Sources/Smithy/Schema/Schema.swift b/Sources/Smithy/Schema/Schema.swift index f8cac05fb..f82adc1a7 100644 --- a/Sources/Smithy/Schema/Schema.swift +++ b/Sources/Smithy/Schema/Schema.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -/// A class which describes selected Smithy model information for a Smithy model shape. +/// A class which describes selected, modeled information for a Smithy shape. /// /// Typically, the Schema contains only modeled info & properties that are relevant to /// serialization, transport bindings, and other functions performed by the SDK. @@ -29,7 +29,13 @@ public final class Schema: Sendable { public let members: [Schema] /// The target schema for this schema. Will only be used when this is a member schema. - public let target: Schema? + public var target: Schema? { + _target() + } + + /// Target schema is passed as an autoclosure so that schemas with self-referencing targets will not cause + /// an infinite loop when accessed. + private let _target: @Sendable () -> Schema? /// The index of this schema, if it represents a Smithy member. /// @@ -49,24 +55,14 @@ public final class Schema: Sendable { type: ShapeType, traits: [ShapeID: Node] = [:], members: [Schema] = [], - target: Schema? = nil, + target: @Sendable @escaping @autoclosure () -> Schema? = nil, index: Int = -1 ) { self.id = id self.type = type self.traits = traits self.members = members - self.target = target + self._target = target self.index = index } } - -public extension Schema { - - /// The member name for this schema, if any. - /// - /// Member name is computed from the schema's ID. - var memberName: String? { - id.member - } -} diff --git a/Sources/Smithy/Schema/ShapeID.swift b/Sources/Smithy/Schema/ShapeID.swift deleted file mode 100644 index 647bbfeb8..000000000 --- a/Sources/Smithy/Schema/ShapeID.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -/// Represents a single Smithy shape ID. -/// -/// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id). -public struct ShapeID: Sendable, Hashable { - public let namespace: String - public let name: String - public let member: String? - - /// Creates a Shape ID for a Smithy shape. - /// - /// This initializer does no validation of length or of allowed characters in the Shape ID; - /// that is to be ensured by the caller (typically calls to this initializer will be code-generated - /// from previously validated Smithy models.) - /// - Parameters: - /// - namespace: The namespace for this shape, i.e. `smithy.api`. - /// - name: The name for this shape, i.e. `Integer`. - /// - member: The optional member name for this shape. - public init(_ namespace: String, _ name: String, _ member: String? = nil) { - self.namespace = namespace - self.name = name - self.member = member - } -} - -extension ShapeID: CustomStringConvertible { - - /// Returns the absolute Shape ID in a single, printable string. - public var description: String { - if let member = self.member { - return "\(namespace)#\(name)$\(member)" - } else { - return "\(namespace)#\(name)" - } - } -} diff --git a/Sources/Smithy/ShapeID.swift b/Sources/Smithy/ShapeID.swift new file mode 100644 index 000000000..a429b0fed --- /dev/null +++ b/Sources/Smithy/ShapeID.swift @@ -0,0 +1,90 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Represents a Smithy shape ID. +/// +/// The id that ShapeID is created from is presumed to be properly formed, since this type will usually +/// be constructed from previously validated models. +/// +/// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id). +public struct ShapeID: Hashable, Sendable { + public let namespace: String + public let name: String + public let member: String? + + /// Creates a Shape ID for a Smithy shape. + /// + /// This initializer does no validation of length or of allowed characters in the Shape ID; + /// that is to be ensured by the caller (typically calls to this initializer will be code-generated + /// from previously validated Smithy models.) + /// - Parameters: + /// - namespace: The namespace for this shape, i.e. `smithy.api`. + /// - name: The name for this shape, i.e. `Integer`. + /// - member: The optional member name for this shape. + public init(_ namespace: String, _ name: String, _ member: String? = nil) { + self.namespace = namespace + self.name = name + self.member = member + } + + public init(_ id: String) throws { + let splitOnPound = id.split(separator: "#") + guard splitOnPound.count == 2 else { + throw ShapeIDError("id \"\(id)\" does not have a #") + } + guard let namespace = splitOnPound.first, !namespace.isEmpty else { + throw ShapeIDError("id \"\(id)\" does not have a nonempty namespace") + } + self.namespace = String(namespace) + let splitOnDollar = splitOnPound.last!.split(separator: "$") + switch splitOnDollar.count { + case 2: + self.name = String(splitOnDollar.first!) + self.member = String(splitOnDollar.last!) + case 1: + self.name = String(splitOnDollar.first!) + self.member = nil + default: + throw ShapeIDError("id \"\(id)\" has more than one $") + } + } + + public init(id: ShapeID, member: String) { + self.namespace = id.namespace + self.name = id.name + self.member = member + } + + public var id: String { + if let member { + return "\(namespace)#\(name)$\(member)" + } else { + return "\(namespace)#\(name)" + } + } +} + +extension ShapeID: Comparable { + + public static func < (lhs: ShapeID, rhs: ShapeID) -> Bool { + return lhs.id.lowercased() < rhs.id.lowercased() + } +} + +extension ShapeID: CustomStringConvertible { + + /// Returns the absolute Shape ID in a single, printable string. + public var description: String { id } +} + +public struct ShapeIDError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/Smithy/Schema/ShapeType.swift b/Sources/Smithy/ShapeType.swift similarity index 100% rename from Sources/Smithy/Schema/ShapeType.swift rename to Sources/Smithy/ShapeType.swift diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index bf0b608c8..6c75a3f96 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -18,8 +18,8 @@ struct SmithyCodegenCLI: AsyncParsableCommand { @Option(help: "The full or relative path to write the schemas output file.") var schemasPath: String? - @Option(help: "The full or relative path to write the struct consumers output file.") - var structConsumersPath: String? + @Option(help: "The full or relative path to write the SerializableStructs output file.") + var serializableStructsPath: String? func run() async throws { let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() @@ -35,10 +35,14 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) + // If --serializable-structs-path was supplied, create the serializable structs file URL + let serializableStructsFileURL = resolve(paramName: "--serializable-structs-path", path: serializableStructsPath) + // Use resolved file URLs to run code generator try CodeGenerator( modelFileURL: modelFileURL, - schemasFileURL: schemasFileURL + schemasFileURL: schemasFileURL, + serializableStructsFileURL: serializableStructsFileURL ).run() } diff --git a/Sources/SmithyCodegenCore/AST/ASTMember.swift b/Sources/SmithyCodegenCore/AST/ASTMember.swift index 5d3a30c35..6602103b2 100644 --- a/Sources/SmithyCodegenCore/AST/ASTMember.swift +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -5,8 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#ast-member struct ASTMember: Decodable { let target: String - let traits: [String: ASTNode]? + let traits: [String: Node]? } diff --git a/Sources/SmithyCodegenCore/AST/ASTModel.swift b/Sources/SmithyCodegenCore/AST/ASTModel.swift index 07de8bf89..c3c13d8ee 100644 --- a/Sources/SmithyCodegenCore/AST/ASTModel.swift +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -5,9 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#top-level-properties struct ASTModel: Decodable { let smithy: String - let metadata: ASTNode? + let metadata: Node? let shapes: [String: ASTShape] } diff --git a/Sources/SmithyCodegenCore/AST/ASTShape.swift b/Sources/SmithyCodegenCore/AST/ASTShape.swift index abeb8d425..a2ffd146b 100644 --- a/Sources/SmithyCodegenCore/AST/ASTShape.swift +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -5,11 +5,13 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#json-ast // This Swift type captures fields for all AST shape types struct ASTShape: Decodable { let type: ASTType - let traits: [String: ASTNode]? + let traits: [String: Node]? let member: ASTMember? let key: ASTMember? let value: ASTMember? diff --git a/Sources/SmithyCodegenCore/AST/ASTNode.swift b/Sources/SmithyCodegenCore/AST/Node+AST.swift similarity index 55% rename from Sources/SmithyCodegenCore/AST/ASTNode.swift rename to Sources/SmithyCodegenCore/AST/Node+AST.swift index a3d6fd58b..72f2e3239 100644 --- a/Sources/SmithyCodegenCore/AST/ASTNode.swift +++ b/Sources/SmithyCodegenCore/AST/Node+AST.swift @@ -5,24 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // -/// Contains the value of a Smithy Node, as used in a JSON AST. -/// -/// Smithy node data is basically the same as the data that can be stored in JSON. -/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. -/// -/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values -enum ASTNode { - case object([String: ASTNode]) - case list([ASTNode]) - case string(String) - case number(Double) - case boolean(Bool) - case null -} +import enum Smithy.Node -extension ASTNode: Decodable { +extension Node: Decodable { - init(from decoder: any Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null @@ -34,9 +21,9 @@ extension ASTNode: Decodable { self = .number(double) } else if let string = try? container.decode(String.self) { self = .string(string) - } else if let array = try? container.decode([ASTNode].self) { + } else if let array = try? container.decode([Node].self) { self = .list(array) - } else if let dictionary = try? container.decode([String: ASTNode].self) { + } else if let dictionary = try? container.decode([String: Node].self) { self = .object(dictionary) } else { throw ASTError("Undecodable value in AST node") diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 24f7b056c..ed857e16f 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -6,20 +6,23 @@ // import struct Foundation.Data -import class Foundation.FileManager import class Foundation.JSONDecoder import struct Foundation.URL + public struct CodeGenerator { let modelFileURL: URL let schemasFileURL: URL? + let serializableStructsFileURL: URL? public init( modelFileURL: URL, - schemasFileURL: URL? + schemasFileURL: URL?, + serializableStructsFileURL: URL? ) { self.modelFileURL = modelFileURL self.schemasFileURL = schemasFileURL + self.serializableStructsFileURL = serializableStructsFileURL } public func run() throws { @@ -27,15 +30,22 @@ public struct CodeGenerator { let modelData = try Data(contentsOf: modelFileURL) let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) - // In the future, AST will be used to create a Model. - // Model will be used to generate code. + // Create the model from the AST + let model = try Model(astModel: astModel) + + // Create a generation context from the model + let ctx = try GenerationContext(model: model) - // This code simply writes an empty schemas file, since it is expected to exist after the - // code generator plugin runs. - // - // Actual code generation will be implemented here later. + // If a schemas file URL was provided, generate it if let schemasFileURL { - FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) + let schemasContents = try SmithySchemaCodegen().generate(ctx: ctx) + try Data(schemasContents.utf8).write(to: schemasFileURL) + } + + // If a serializable structs file URL was provided, generate it + if let serializableStructsFileURL { + let serializableStructsContents = try SerializableStructsCodegen().generate(ctx: ctx) + try Data(serializableStructsContents.utf8).write(to: serializableStructsFileURL) } } } diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift new file mode 100644 index 000000000..8cc9626d6 --- /dev/null +++ b/Sources/SmithyCodegenCore/GenerationContext.swift @@ -0,0 +1,27 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct GenerationContext { + public let service: ServiceShape + public let model: Model + public let symbolProvider: SymbolProvider + + /// Creates a ``GenerationContext`` from a model. + /// + /// The model must contain exactly one service. + /// - Parameter model: The ``Model`` to create the generation context from. + /// - Throws: ``ModelError`` if the model does not contain exactly one service. + init(model: Model) throws { + let services = model.shapes.values.filter { $0.type == .service } + guard services.count == 1, let service = services.first as? ServiceShape else { + throw ModelError("Model contains \(services.count) services") + } + self.service = service + self.model = model + self.symbolProvider = SymbolProvider(service: service, model: model) + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model+AST.swift b/Sources/SmithyCodegenCore/Model/Model+AST.swift new file mode 100644 index 000000000..5154a0eea --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -0,0 +1,162 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +extension Model { + + /// Creates a Smithy model from a JSON AST model. + /// + /// Compared to the AST model, this model has custom shape types, members are included in the main body of shapes + /// along with other shape types, and all Shape IDs are fully-qualified + /// (i.e. members have the enclosing shape's namespace & name, along with their own member name.) + /// - Parameter astModel: The JSON AST model to be created. + convenience init(astModel: ASTModel) throws { + // Get all of the members from the AST model, create pairs of ShapeID & MemberShape + let idToMemberShapePairs = try astModel.shapes + .flatMap { try Self.memberShapePairs(id: $0.key, astShape: $0.value) } + let memberShapes = Dictionary(uniqueKeysWithValues: idToMemberShapePairs) + + // Get all of the non-members from the AST model, create pairs of ShapeID & various shape subclasses + let idToShapePairs = try astModel.shapes + .map { try Self.shapePair(id: $0.key, astShape: $0.value, memberShapes: memberShapes) } + + // Combine all shapes (member & nonmember) into one large Dict for inclusion in the model + let shapes = Dictionary(uniqueKeysWithValues: idToShapePairs + idToMemberShapePairs) + + // Initialize the properties of self + self.init(version: astModel.smithy, metadata: astModel.metadata, shapes: shapes) + + // self is now initialized, set all of the Shapes with references back to this model + self.shapes.values.forEach { $0.model = self } + + // Verify that there is exactly one Service + let services = self.shapes.values.filter { $0.type == .service } + guard services.count == 1 else { throw ModelError("Model has \(services.count) services") } + } + + private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, MemberShape)] { + var baseMembers = (astShape.members ?? [:]) + + // If this AST shape is an array, add a member for its element + if let member = astShape.member { + baseMembers["member"] = member + } + + // If this AST shape is a map, add members for its key & value + if let key = astShape.key { + baseMembers["key"] = key + } + if let value = astShape.value { + baseMembers["value"] = value + } + + // Map the AST members to ShapeID-to-MemberShape pairs & return the list of pairs + return try baseMembers.map { astMember in + // Create a ShapeID for this member + let memberID = ShapeID(id: try ShapeID(id), member: astMember.key) + + // Create traits for this member + let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value) } + let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? []) + + // Create a Shape ID for this member's target + let targetID = try ShapeID(astMember.value.target) + + // Create the ShapeID-to-MemberShape pair + return (memberID, MemberShape(id: memberID, traits: traits, targetID: targetID)) + } + } + + private static func shapePair(id: String, astShape: ASTShape, memberShapes: [ShapeID: MemberShape]) throws -> (ShapeID, Shape) { + // Create the ShapeID for this shape from the AST shape's string ID. + let shapeID = try ShapeID(id) + + // Create model traits from the AST traits. + let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value) } ?? [] + let traits = Dictionary(uniqueKeysWithValues: idToTraitPairs) + + // Based on the AST shape type, create the appropriate Shape type. + switch astShape.type { + case .service: + let shape = ServiceShape( + id: shapeID, + traits: traits, + errorIDs: try astShape.errors?.map { try $0.id } ?? [] + ) + return (shapeID, shape) + case .operation: + let shape = OperationShape( + id: shapeID, + traits: traits, + input: try astShape.input?.id, + output: try astShape.output?.id + ) + return (shapeID, shape) + case .structure: + let shape = StructureShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .union: + let shape = UnionShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .enum: + let shape = EnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .intEnum: + let shape = IntEnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .list, .set: + let shape = ListShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .map: + let shape = MapShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + default: + let shape = Shape( + id: shapeID, + type: try astShape.type.modelType, + traits: traits + ) + + // Return the ShapeID-to-Shape pair. + return (shapeID, shape) + } + } + + private static func memberIDs(for shapeID: ShapeID, memberShapes: [ShapeID: MemberShape]) -> [ShapeID] { + // Given all the member shapes in this model, select the ones for the passed shape ID + // and return their IDs in sorted order. + memberShapes.keys.filter { + $0.namespace == shapeID.namespace && $0.name == shapeID.name && $0.member != nil + }.sorted() + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift new file mode 100644 index 000000000..2ee603c47 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +public class Model { + public let version: String + public let metadata: Node? + public let shapes: [ShapeID: Shape] + + init(version: String, metadata: Node?, shapes: [ShapeID: Shape]) { + self.version = version + self.metadata = metadata + self.shapes = shapes + } + + func expectShape(id: ShapeID) throws -> Shape { + guard let shape = shapes[id] else { + throw ModelError("ShapeID \(id) was expected in model but not found") + } + return shape + } +} diff --git a/Sources/SmithyCodegenCore/Model/ModelError.swift b/Sources/SmithyCodegenCore/Model/ModelError.swift new file mode 100644 index 000000000..97d45fdb5 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ModelError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ModelError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift new file mode 100644 index 000000000..e6d67a6a7 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension ASTReference { + + var id: ShapeID { + get throws { + return try ShapeID(target) + } + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift new file mode 100644 index 000000000..3f8332c5b --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -0,0 +1,68 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.ShapeType + +extension ASTType { + + var modelType: ShapeType { + get throws { + switch self { + case .blob: + return .blob + case .boolean: + return .boolean + case .string: + return .string + case .timestamp: + return .timestamp + case .byte: + return .byte + case .short: + return .short + case .integer: + return .integer + case .long: + return .long + case .float: + return .float + case .document: + return .document + case .double: + return .double + case .bigDecimal: + return .bigDecimal + case .bigInteger: + return .bigInteger + case .`enum`: + return .`enum` + case .intEnum: + return .intEnum + case .list: + return .list + case .set: + return .set + case .map: + return .map + case .structure: + return .structure + case .union: + return .union + case .member: + return .member + case .service: + return .service + case .resource: + return .resource + case .operation: + return .operation + case .apply: + throw ModelError("\"apply\" AST shapes not implemented") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/ModelTransformer/ASTShape+isDeprecated.swift b/Sources/SmithyCodegenCore/ModelTransformer/ASTShape+isDeprecated.swift new file mode 100644 index 000000000..99ffbc26a --- /dev/null +++ b/Sources/SmithyCodegenCore/ModelTransformer/ASTShape+isDeprecated.swift @@ -0,0 +1,41 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Date +import class Foundation.DateFormatter +import struct Foundation.Locale +import struct Foundation.TimeZone +import enum Smithy.Node + +//protocol HasASTTraits { +// var traits: [String: ASTNode]? { get } +//} +// +//extension ASTShape: HasASTTraits {} +// +//extension ASTMember: HasASTTraits {} +// +//extension HasASTTraits { +// +// var isDeprecated: Bool { +// guard let node = traits?["smithy.api#deprecated"] else { return false } +// guard case .object(let object) = node else { fatalError("Deprecated trait content is not an object") } +// guard case .string(let since) = object["since"] else { return false } +// guard let deprecationDate = formatter.date(from: since) else { return false } +// +// let removeIfDeprecatedBefore = Date(timeIntervalSince1970: 1726531200.0) // this is '2024-09-17' +// return deprecationDate < removeIfDeprecatedBefore +// } +//} +// +//private let formatter = { +// let df = DateFormatter() +// df.dateFormat = "yyyy-MM-dd" +// df.timeZone = TimeZone(identifier: "UTC") +// df.locale = Locale(identifier: "en_US_POSIX") +// return df +//}() diff --git a/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt b/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt new file mode 100644 index 000000000..8f50fc772 --- /dev/null +++ b/Sources/SmithyCodegenCore/Resources/DefaultSwiftHeader.txt @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by SmithyCodegenCLI. DO NOT EDIT! diff --git a/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift b/Sources/SmithyCodegenCore/Schemas/Shape+Schema.swift new file mode 100644 index 000000000..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/Schemas/SmithySchemaCodegen.swift b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift new file mode 100644 index 000000000..5da9854f9 --- /dev/null +++ b/Sources/SmithyCodegenCore/Schemas/SmithySchemaCodegen.swift @@ -0,0 +1,90 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID +import let SmithySerialization.permittedTraitIDs + +package struct SmithySchemaCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import class Smithy.Schema") + writer.write("import enum Smithy.Prelude") + writer.write("") + + // Write schemas for all inputs & outputs and their descendants. + let shapes = try ctx.model.shapes.values + .filter { $0.type == .structure } + .filter { + try $0.hasTrait(try .init("smithy.api#input")) || + $0.hasTrait(try .init("smithy.api#output")) || + $0.hasTrait(try .init("smithy.api#error"))} + .map { [$0] + $0.descendants } + .flatMap { $0 } + .filter { $0.id.namespace != "smithy.api" } + let sortedShapes = Array(Set(shapes)).sorted { $0.id.id.lowercased() < $1.id.id.lowercased() } + writer.write("// Number of schemas: \(sortedShapes.count)") + writer.write("") + for shape in sortedShapes { + try writer.openBlock("public var \(shape.schemaVarName): Smithy.Schema {", "}") { writer in + try writeSchema(writer: writer, shape: shape) + writer.unwrite(",") + } + writer.write("") + } + writer.unwrite("\n") + return writer.finalize() + } + + private func writeSchema(writer: SwiftWriter, shape: Shape, index: Int? = nil) throws { + try writer.openBlock(".init(", "),") { writer in + writer.write("id: \(shape.id.rendered),") + writer.write("type: .\(shape.type),") + let relevantTraitIDs = shape.traits.keys.filter { permittedTraitIDs.contains($0.id) } + let traitIDs = Array(relevantTraitIDs).sorted() + if !traitIDs.isEmpty { + writer.openBlock("traits: [", "],") { writer in + for traitID in traitIDs { + let trait = shape.traits[traitID]! + writer.write("\(traitID.rendered): \(trait.rendered),") + } + } + } + let members = (shape as? HasMembers)?.members ?? [] + if !members.isEmpty { + try writer.openBlock("members: [", "],") { writer in + for (index, member) in members.enumerated() { + try writeSchema(writer: writer, shape: member, index: index) + } + } + } + if let target = (shape as? MemberShape)?.target { + writer.write(try "target: \(target.schemaVarName),") + } + if let index { + writer.write("index: \(index),") + } + writer.unwrite(",") + } + } +} + +extension ShapeID { + + var rendered: String { + let namespaceLiteral = namespace.literal + let nameLiteral = name.literal + if let member { + let memberLiteral = member.literal + return ".init(\(namespaceLiteral), \(nameLiteral), \(memberLiteral))" + } else { + return ".init(\(namespaceLiteral), \(nameLiteral))" + } + } +} diff --git a/Sources/SmithyCodegenCore/SerializableStructs/SerializableStructsCodegen.swift b/Sources/SmithyCodegenCore/SerializableStructs/SerializableStructsCodegen.swift new file mode 100644 index 000000000..efba7f06e --- /dev/null +++ b/Sources/SmithyCodegenCore/SerializableStructs/SerializableStructsCodegen.swift @@ -0,0 +1,68 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +package struct SerializableStructsCodegen { + + package init() {} + + package func generate(ctx: GenerationContext) throws -> String { + let writer = SwiftWriter() + writer.write("import enum Smithy.Prelude") + writer.write("import class Smithy.Schema") + writer.write("import protocol SmithySerialization.SerializableStruct") + writer.write("import protocol SmithySerialization.ShapeSerializer") + writer.write("") + + for shape in ctx.model.shapes.values where shape.type == .structure || shape.type == .union { + let swiftType = try ctx.symbolProvider.swiftType(shape: shape) + try writer.openBlock("extension \(swiftType): SmithySerialization.SerializableStruct {", "}") { writer in + writer.write("") + writer.write("public static var schema: Smithy.Schema { \(try shape.schemaVarName) }") + writer.write("") + try writer.openBlock( + "public func serializeMembers(_ serializer: any SmithySerialization.ShapeSerializer) {", "}" + ) { writer in + for (index, member) in members(of: shape).enumerated() { + if shape.type == .structure { + let propertyName = try ctx.symbolProvider.propertyName(shapeID: member.id) + let properties = shape.hasTrait(.init("smithy.api", "error")) ? "properties." : "" + try writer.openBlock("if let value = self.\(properties)\(propertyName) {", "}") { writer in + try writeSerializeCall(writer: writer, shape: shape, member: member, index: index) + } + } else { // shape is a union + let enumCaseName = try ctx.symbolProvider.enumCaseName(shapeID: member.id) + try writer.openBlock("if case .\(enumCaseName)(let value) = self {", "}") { writer in + try writeSerializeCall(writer: writer, shape: shape, member: member, index: index) + } + } + } + } + } + writer.write("") + } + writer.unwrite("\n") + return writer.finalize() + } + + private func writeSerializeCall(writer: SwiftWriter, shape: Shape, member: MemberShape, index: Int) throws { + switch member.target.type { + case .list: + writer.write("// serialize list here") + case .map: + writer.write("// serialize map here") + default: + let methodName = try member.target.structConsumerMethod + let schemaVarName = try shape.schemaVarName + writer.write("serializer.\(methodName)(schema: \(schemaVarName).members[\(index)], value: value)") + } + } + + private func members(of shape: Shape) -> [MemberShape] { + guard let hasMembers = shape as? HasMembers else { return [] } + return hasMembers.members + } +} diff --git a/Sources/SmithyCodegenCore/SerializableStructs/Shape+StructConsumer.swift b/Sources/SmithyCodegenCore/SerializableStructs/Shape+StructConsumer.swift new file mode 100644 index 000000000..a3d9e1cff --- /dev/null +++ b/Sources/SmithyCodegenCore/SerializableStructs/Shape+StructConsumer.swift @@ -0,0 +1,52 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension Shape { + + var structConsumerMethod: String { + get throws { + switch type { + case .blob: + return "writeBlob" + case .boolean: + return "writeBoolean" + case .string, .enum: + return "writeString" + case .timestamp: + return "writeTimestamp" + case .byte: + return "writeByte" + case .short: + return "writeShort" + case .integer, .intEnum: + return "writeInteger" + case .long: + return "writeLong" + case .float: + return "writeFloat" + case .document: + return "writeDocument" + case .double: + return "writeDouble" + case .bigDecimal: + return "writeBigDecimal" + case .bigInteger: + return "writeBigInteger" + case .list, .set: + return "writeList" + case .map: + return "writeMap" + case .structure, .union: + return "writeStruct" + case .member, .service, .resource, .operation: + throw ModelError("Cannot serialize type \(type)") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift new file mode 100644 index 000000000..8440ab2a9 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/EnumShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy enums. +public class EnumShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .enum, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/HasMembers.swift b/Sources/SmithyCodegenCore/Shape/HasMembers.swift new file mode 100644 index 000000000..dd2a32e3d --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/HasMembers.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Protocol provided as a convenience to get members from Shapes that have them. +protocol HasMembers { + var members: [MemberShape] { get } +} diff --git a/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift new file mode 100644 index 000000000..51e03be42 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/IntEnumShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy intEnums. +public class IntEnumShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .intEnum, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ListShape.swift b/Sources/SmithyCodegenCore/Shape/ListShape.swift new file mode 100644 index 000000000..d5f6717e2 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ListShape.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy lists. +public class ListShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .list, traits: traits) + } + + public var member: MemberShape { + model.shapes[.init(id: id, member: "member")]! as! MemberShape + } + + public var members: [MemberShape] { + [member] + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/MapShape.swift b/Sources/SmithyCodegenCore/Shape/MapShape.swift new file mode 100644 index 000000000..1d6d0728c --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/MapShape.swift @@ -0,0 +1,36 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy maps. +public class MapShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .list, traits: traits) + } + + public var key: MemberShape { + model.shapes[.init(id: id, member: "key")]! as! MemberShape + } + + public var value: MemberShape { + model.shapes[.init(id: id, member: "value")]! as! MemberShape + } + + public var members: [MemberShape] { + return [key, value] + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/MemberShape.swift b/Sources/SmithyCodegenCore/Shape/MemberShape.swift new file mode 100644 index 000000000..31ab9c551 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/MemberShape.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +/// A ``Shape`` subclass specialized for Smithy members. +public class MemberShape: Shape { + let targetID: ShapeID + + init(id: ShapeID, traits: [ShapeID : Node], targetID: ShapeID) { + self.targetID = targetID + super.init(id: id, type: .member, traits: traits) + } + + public var target: Shape { + return model.shapes[targetID] ?? Shape.prelude[targetID]! + } +} diff --git a/Sources/SmithyCodegenCore/Shape/OperationShape.swift b/Sources/SmithyCodegenCore/Shape/OperationShape.swift new file mode 100644 index 000000000..53a582b2f --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/OperationShape.swift @@ -0,0 +1,46 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy operations. +class OperationShape: Shape { + let inputShapeID: ShapeID? + let outputShapeID: ShapeID? + + public init(id: ShapeID, traits: [ShapeID: Node], input: ShapeID?, output: ShapeID?) { + self.inputShapeID = input + self.outputShapeID = output + super.init(id: id, type: .operation, traits: traits) + } + + public var input: Shape { + if let inputShapeID { + return model.shapes[inputShapeID]!.adding(traits: [.init("smithy.api", "input"): [:]]) + } else { + let traits: [ShapeID: Node] = [ + .init("smithy.api", "input"): [:], + .init("swift.synthetic", "operationName"): .string(id.id), + ] + return Shape.unit.adding(traits: traits) + } + } + + public var output: Shape { + if let outputShapeID { + return model.shapes[outputShapeID]!.adding(traits: [.init("smithy.api", "output"): [:]]) + } else { + let traits: [ShapeID: Node] = [ + .init("smithy.api", "input"): [:], + .init("swift.synthetic", "operationName"): .string(id.id), + ] + return Shape.unit.adding(traits: traits) + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/ServiceShape.swift b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift new file mode 100644 index 000000000..179419e69 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ServiceShape.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +/// A ``Shape`` subclass specialized for Smithy services. +public class ServiceShape: Shape { + let errorIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID : Node], errorIDs: [ShapeID]) { + self.errorIDs = errorIDs + super.init(id: id, type: .service, traits: traits) + } + + public var errors: [Shape] { + errorIDs.compactMap { model.shapes[$0] } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift new file mode 100644 index 000000000..9d5635159 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/Shape+Prelude.swift @@ -0,0 +1,128 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Prelude +import struct Smithy.ShapeID + +extension Shape { + + static var prelude: [ShapeID: Shape] {[ + unit.id: unit, + boolean.id: boolean, + string.id: string, + integer.id: integer, + blob.id: blob, + timestamp.id: timestamp, + byte.id: byte, + short.id: short, + long.id: long, + float.id: float, + double.id: double, + document.id: document, + primitiveBoolean.id: primitiveBoolean, + primitiveInteger.id: primitiveInteger, + primitiveByte.id: primitiveByte, + primitiveLong.id: primitiveLong, + primitiveFloat.id: primitiveFloat, + primitiveDouble.id: primitiveDouble, + ]} + + static var unit: Shape { + let schema = Smithy.Prelude.unitSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var boolean: Shape { + let schema = Smithy.Prelude.booleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var string: Shape { + let schema = Smithy.Prelude.stringSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var integer: Shape { + let schema = Smithy.Prelude.integerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var blob: Shape { + let schema = Smithy.Prelude.blobSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var timestamp: Shape { + let schema = Smithy.Prelude.timestampSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var byte: Shape { + let schema = Smithy.Prelude.byteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var short: Shape { + let schema = Smithy.Prelude.shortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var long: Shape { + let schema = Smithy.Prelude.longSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var float: Shape { + let schema = Smithy.Prelude.floatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var double: Shape { + let schema = Smithy.Prelude.doubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var document: Shape { + let schema = Smithy.Prelude.documentSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveBoolean: Shape { + let schema = Smithy.Prelude.primitiveBooleanSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveInteger: Shape { + let schema = Smithy.Prelude.primitiveIntegerSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveByte: Shape { + let schema = Smithy.Prelude.primitiveByteSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveShort: Shape { + let schema = Smithy.Prelude.primitiveShortSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveLong: Shape { + let schema = Smithy.Prelude.primitiveLongSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveFloat: Shape { + let schema = Smithy.Prelude.primitiveFloatSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } + + static var primitiveDouble: Shape { + let schema = Smithy.Prelude.primitiveDoubleSchema + return Shape(id: schema.id, type: schema.type, traits: schema.traits) + } +} diff --git a/Sources/SmithyCodegenCore/Shape/Shape.swift b/Sources/SmithyCodegenCore/Shape/Shape.swift new file mode 100644 index 000000000..35f541bec --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/Shape.swift @@ -0,0 +1,78 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID +import enum Smithy.ShapeType +import enum Smithy.Node + +public class Shape { + public let id: ShapeID + public let type: ShapeType + public let traits: [ShapeID: Node] + weak var model: Model! + + public init(id: ShapeID, type: ShapeType, traits: [ShapeID: Node]) { + self.id = id + self.type = type + self.traits = traits + } + + public func hasTrait(_ traitID: ShapeID) -> Bool { + traits[traitID] != nil + } + + public func getTrait(_ traitID: ShapeID) -> Node? { + traits[traitID] + } + + public func adding(traits newTraits: [ShapeID: Node]) -> Shape { + let combinedTraits = traits.merging(newTraits) { _, new in new } + let new = Shape(id: id, type: type, traits: combinedTraits) + new.model = model + return new + } + + public var descendants: Set { + var c = Set() + descendants(&c) + return c + } + + private func descendants(_ descendants: inout Set) { + let shapes = candidates(for: self) + for shape in shapes { + if descendants.contains(shape) { continue } + descendants.insert(shape) + shape.descendants(&descendants) + } + } + + func candidates(for shape: Shape) -> [Shape] { + [] // default. May be overridden by Shape subclasses. + } +} + +extension Shape: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Shape: Equatable { + + public static func == (lhs: Shape, rhs: Shape) -> Bool { + lhs.id == rhs.id + } +} + +extension Shape: Comparable { + + public static func < (lhs: Shape, rhs: Shape) -> Bool { + lhs.id < rhs.id + } +} diff --git a/Sources/SmithyCodegenCore/Shape/StructureShape.swift b/Sources/SmithyCodegenCore/Shape/StructureShape.swift new file mode 100644 index 000000000..2f5f33340 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/StructureShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy structures. +public class StructureShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .structure, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/UnionShape.swift b/Sources/SmithyCodegenCore/Shape/UnionShape.swift new file mode 100644 index 000000000..7668563fb --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/UnionShape.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +/// A ``Shape`` subclass specialized for Smithy unions. +public class UnionShape: Shape, HasMembers { + let memberIDs: [ShapeID] + + public init(id: ShapeID, traits: [ShapeID: Node], memberIDs: [ShapeID]) { + self.memberIDs = memberIDs + super.init(id: id, type: .union, traits: traits) + } + + public var members: [MemberShape] { + return memberIDs.map { model.shapes[$0]! as! MemberShape } + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift b/Sources/SmithyCodegenCore/SwiftRendering/Node+Rendered.swift new file mode 100644 index 000000000..78b17977a --- /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..1bb57c035 --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/String+Utils.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +extension String { + + /// Escapes special characters in the string, then surrounds it in double quotes + /// to form a Swift string literal. + var literal: String { + let escaped = description + .replacingOccurrences(of: "\0", with: "\\0") + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\t", with: "\\t") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\'", with: "\\'") + return "\"\(escaped)\"" + } +} diff --git a/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift new file mode 100644 index 000000000..8907ee708 --- /dev/null +++ b/Sources/SmithyCodegenCore/SwiftRendering/SwiftWriter.swift @@ -0,0 +1,61 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class Foundation.Bundle +import struct Foundation.Data +import struct Foundation.URL + +class SwiftWriter { + + private var lines: [String] + + var indentLevel = 0 + + init(includeHeader: Bool = true) { + if includeHeader { + let defaultHeaderFileURL = Bundle.module.url(forResource: "DefaultSwiftHeader", withExtension: "txt")! + // swiftlint:disable:next force_try + let defaultHeader = try! String(data: Data(contentsOf: defaultHeaderFileURL), encoding: .utf8)! + self.lines = defaultHeader.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) } + } else { + self.lines = [] + } + } + + func indent() { + indentLevel += 4 + } + + func dedent() { + indentLevel -= 4 + } + + func write(_ line: String) { + lines.append(String(repeating: " ", count: indentLevel) + line) + } + + func unwrite(_ text: String) { + guard let lastIndex = lines.indices.last else { return } + if text == "\n" && lines[lastIndex] == "" { + _ = lines.removeLast() + } else if lines[lastIndex].hasSuffix(text) { + lines[lastIndex].removeLast(text.count) + } + } + + func openBlock(_ openWith: String, _ closeWith: String, contents: (SwiftWriter) throws -> Void) rethrows { + write(openWith) + indent() + try contents(self) + dedent() + write(closeWith) + } + + func finalize() -> String { + return lines.joined(separator: "\n").appending("\n") + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift new file mode 100644 index 000000000..4eca55b5b --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -0,0 +1,129 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.NSRange +import class Foundation.NSRegularExpression +import struct Smithy.ShapeID + +public struct SymbolProvider { + let service: ServiceShape + let model: Model + + init(service: ServiceShape, model: Model) { + self.service = service + self.model = model + } + + var serviceName: String { + get throws { + guard service.type == .service else { + throw SymbolProviderError("Called serviceName on non-service shape") + } + guard case .object(let serviceInfo) = service.getTrait(.init("aws.api", "service")) else { + throw SymbolProviderError("No service trait on service") + } + guard case .string(let sdkID) = serviceInfo["sdkId"] else { + throw SymbolProviderError("No sdkId on service trait") + } + return sdkID.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "Service", with: "") + } + } + + private var inputTraitID = ShapeID("smithy.api", "input") + private var outputTraitID = ShapeID("smithy.api", "output") + private var errorTraitID = ShapeID("smithy.api", "error") + private var operationNameTraitID = ShapeID("swift.synthetic", "operationName") + + public func swiftType(shape: Shape) throws -> String { + if case .string(let name) = shape.getTrait(operationNameTraitID), shape.hasTrait(inputTraitID) { + return "\(name)Input" + } else if shape.hasTrait(inputTraitID) { + guard let operation = model.shapes.values + .filter({ $0.type == .operation }) + .map({ $0 as! OperationShape }) + .first(where: { $0.inputShapeID == shape.id }) + else { throw SymbolProviderError("Operation for input \(shape.id) not found") } + return "\(operation.id.name)Input" + } else if + case .string(let name) = shape.getTrait(operationNameTraitID), shape.hasTrait(outputTraitID) { + return "\(name)Output" + } else if shape.hasTrait(outputTraitID) { + guard let operation = model.shapes.values + .filter({ $0.type == .operation }) + .map({ $0 as! OperationShape }) + .first(where: { $0.outputShapeID == shape.id }) + else { throw SymbolProviderError("Operation for output \(shape.id) not found") } + return "\(operation.id.name)Output" + } else if shape.hasTrait(errorTraitID) { + return shape.id.name + } else { + return try "\(serviceName)ClientTypes.\(shape.id.name)" + } + } + + public func propertyName(shapeID: ShapeID) throws -> String { + guard let member = shapeID.member else { throw SymbolProviderError("Shape ID has no member name") } + return member.toLowerCamelCase() + } + + public func enumCaseName(shapeID: ShapeID) throws -> String { + try propertyName(shapeID: shapeID).toLowerCamelCase().lowercased() + } +} + +private extension String { + + func toLowerCamelCase() -> String { + let words = splitOnWordBoundaries() // Split into words + let firstWord = words.first!.lowercased() // make first word lowercase + return firstWord + words.dropFirst().joined() // join lowercased first word to remainder + } + + func splitOnWordBoundaries() -> [String] { + // TODO: when nonsupporting platforms are dropped, convert this to Swift-native regex + // adapted from Java v2 SDK CodegenNamingUtils.splitOnWordBoundaries + var result = self + + // all non-alphanumeric characters: "acm-success"-> "acm success" + let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") + result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" + let underscoreRegex = try! NSRegularExpression(pattern: "_") + result = underscoreRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " _ ") + + // if a number has a standalone v or V in front of it, separate it out + let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") + result = smallVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 v$2") + + let largeVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})V([0-9]+)") + result = largeVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 V$2") + + // add a space between camelCased words + let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") + result = camelCaseSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // add a space after acronyms + let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") + result = acronymSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // add space after a number in the middle of a word + let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") + result = spaceAfterNumberRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // remove extra spaces - multiple consecutive ones or those and the beginning/end of words + let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") + result = removeExtraSpaceRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + .trimmingCharacters(in: .whitespaces) + + return result.components(separatedBy: " ") + } + + var range: NSRange { + NSRange(location: 0, length: count) + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift new file mode 100644 index 000000000..2d17de6bb --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SymbolProviderError: Error { + let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithySerialization/Consumer.swift b/Sources/SmithySerialization/Consumer.swift new file mode 100644 index 000000000..1296821af --- /dev/null +++ b/Sources/SmithySerialization/Consumer.swift @@ -0,0 +1,8 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias Consumer = (Serializer) throws -> Void diff --git a/Sources/SmithySerialization/MapSerializer.swift b/Sources/SmithySerialization/MapSerializer.swift new file mode 100644 index 000000000..2832e873f --- /dev/null +++ b/Sources/SmithySerialization/MapSerializer.swift @@ -0,0 +1,12 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class Smithy.Schema + +public protocol MapSerializer { + func writeEntry(keySchema: Schema, key: String, valueSerializer: Consumer) +} diff --git a/Sources/SmithySerialization/Schema/SchemaTraits.swift b/Sources/SmithySerialization/Schema/SchemaTraits.swift new file mode 100644 index 000000000..4b34c2abe --- /dev/null +++ b/Sources/SmithySerialization/Schema/SchemaTraits.swift @@ -0,0 +1,22 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// The trait IDs that should be copied into schemas. Other traits are omitted for brevity. +/// +/// This list can be expanded as features are added to Smithy/SDK that use them. +public let permittedTraitIDs: Set = [ + "smithy.api#sparse", + "smithy.api#input", + "smithy.api#output", + "smithy.api#error", + "smithy.api#enumValue", + "smithy.api#jsonName", + "smithy.api#required", + "smithy.api#default", + "smithy.api#timestampFormat", + "smithy.api#sensitive", +] diff --git a/Sources/SmithySerialization/SerializableShape.swift b/Sources/SmithySerialization/SerializableShape.swift new file mode 100644 index 000000000..8a15b5ff8 --- /dev/null +++ b/Sources/SmithySerialization/SerializableShape.swift @@ -0,0 +1,13 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class Smithy.Schema + +public protocol SerializableShape { + static var schema: Smithy.Schema { get } + func serialize(_ serializer: any ShapeSerializer) +} diff --git a/Sources/SmithySerialization/SerializableStruct.swift b/Sources/SmithySerialization/SerializableStruct.swift new file mode 100644 index 000000000..02a3c97ad --- /dev/null +++ b/Sources/SmithySerialization/SerializableStruct.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol SerializableStruct: SerializableShape { + func serializeMembers(_ serializer: any ShapeSerializer) +} + +public extension SerializableStruct { + + func serialize(_ serializer: any ShapeSerializer) { + serializer.writeStruct(schema: Self.schema, value: self) + } +} diff --git a/Sources/SmithySerialization/ShapeSerializer.swift b/Sources/SmithySerialization/ShapeSerializer.swift new file mode 100644 index 000000000..ec1f6a7da --- /dev/null +++ b/Sources/SmithySerialization/ShapeSerializer.swift @@ -0,0 +1,53 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import enum Smithy.ByteStream +import protocol Smithy.SmithyDocument +import class Smithy.Schema + +public protocol ShapeSerializer { + func writeStruct(schema: Schema, value: any SerializableStruct) + func writeList(schema: Schema, size: Int, consumer: Consumer) + func writeMap(schema: Schema, size: Int, consumer: Consumer) + func writeBoolean(schema: Schema, value: Bool) + func writeByte(schema: Schema, value: Int8) + func writeShort(schema: Schema, value: Int16) + func writeInteger(schema: Schema, value: Int) + func writeLong(schema: Schema, value: Int) + func writeFloat(schema: Schema, value: Float) + func writeDouble(schema: Schema, value: Double) + func writeBigInteger(schema: Schema, value: Int64) + func writeBigDecimal(schema: Schema, value: Double) + func writeString(schema: Schema, value: String) + func writeBlob(schema: Schema, value: Data) + func writeTimestamp(schema: Schema, value: Date) + func writeDocument(schema: Schema, value: any SmithyDocument) + func writeNull(schema: Schema) + func writeDataStream(schema: Schema, value: ByteStream) + func writeEventStream(schema: Schema, value: AsyncThrowingStream) +} + +public extension ShapeSerializer { + + func writeString(schema: Schema, value: T) where T.RawValue == String { + writeString(schema: schema, value: value.rawValue) + } + + func writeInteger(schema: Schema, value: T) where T.RawValue == Int { + writeInteger(schema: schema, value: value.rawValue) + } + + func writeDataStream(schema: Schema, value: ByteStream) { + // by default, do nothing + } + + func writeEventStream(schema: Schema, value: AsyncThrowingStream) { + // by default, do nothing + } +} diff --git a/Sources/SmithySerialization/StringSerializer.swift b/Sources/SmithySerialization/StringSerializer.swift new file mode 100644 index 000000000..23cd53b18 --- /dev/null +++ b/Sources/SmithySerialization/StringSerializer.swift @@ -0,0 +1,122 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +import class Foundation.ISO8601DateFormatter +import class Smithy.Schema +import struct Smithy.ShapeID +import protocol Smithy.SmithyDocument + +public class StringSerializer: ShapeSerializer { + private var _string = "" + let suffix = ", " + let redacted = "[REDACTED]" + + public init() {} + + public func writeStruct(schema: Smithy.Schema, value: any SerializableStruct) { + addNameAndValue(schema) { + let typeName = type(of: value) + let serializer = StringSerializer() + value.serializeMembers(serializer) + serializer.removeTrailingSuffix() + return "\(typeName)(\(serializer.string()))" + } + } + + public func string() -> String { + removeTrailingSuffix() + return _string + } + + public func writeList(schema: Smithy.Schema, size: Int, consumer: (any ShapeSerializer) throws -> Void) { + addNameAndValue(schema) { "" } + } + + public func writeMap(schema: Smithy.Schema, size: Int, consumer: (any MapSerializer) throws -> Void) { + addNameAndValue(schema) { "" } + } + + public func writeBoolean(schema: Smithy.Schema, value: Bool) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeByte(schema: Smithy.Schema, value: Int8) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeShort(schema: Smithy.Schema, value: Int16) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeInteger(schema: Smithy.Schema, value: Int) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeLong(schema: Smithy.Schema, value: Int) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeFloat(schema: Smithy.Schema, value: Float) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeDouble(schema: Smithy.Schema, value: Double) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeBigInteger(schema: Smithy.Schema, value: Int64) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeBigDecimal(schema: Smithy.Schema, value: Double) { + addNameAndValue(schema) { "\(value)" } + } + + public func writeString(schema: Smithy.Schema, value: String) { + addNameAndValue(schema) { "\"\(value)\"" } + } + + public func writeBlob(schema: Smithy.Schema, value: Data) { + addNameAndValue(schema) { "<\(value.count) bytes>" } + } + + public func writeTimestamp(schema: Smithy.Schema, value: Date) { + addNameAndValue(schema) { df.string(from: value) } + } + + public func writeDocument(schema: Smithy.Schema, value: any Smithy.SmithyDocument) { + addNameAndValue(schema) { "" } + } + + public func writeNull(schema: Smithy.Schema) { + addNameAndValue(schema) { "nil" } + } + + private func addNameAndValue(_ schema: Schema, _ value: () -> String) { + if schema.type == .member, let name = schema.id.member { + _string += "\(name): " + } + _string += schema.isSensitive ? redacted : value() + _string += suffix + } + + private func removeTrailingSuffix() { + if _string.hasSuffix(suffix) { _string.removeLast(suffix.count) } + } +} + +private let df = ISO8601DateFormatter() + +extension Smithy.Schema { + + var isSensitive: Bool { + let sensitive = ShapeID("smithy.api", "sensitive") + return traits[sensitive] ?? target?.traits[sensitive] != nil + } +} diff --git a/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift b/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift index 69761212c..0dde0b521 100644 --- a/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift +++ b/Sources/SmithySwiftNIO/SwiftNIOHTTPClient.swift @@ -6,6 +6,9 @@ // import AsyncHTTPClient +import struct Foundation.Date +import struct Foundation.URLComponents +import struct Foundation.URLQueryItem import NIOCore import NIOHTTP1 import NIOPosix @@ -15,9 +18,6 @@ import SmithyHTTPAPI import SmithyHTTPClientAPI import SmithyStreams import SmithyTelemetryAPI -import struct Foundation.Date -import struct Foundation.URLComponents -import struct Foundation.URLQueryItem /// AsyncHTTPClient-based HTTP client implementation that conforms to SmithyHTTPAPI.HTTPClient /// This implementation is thread-safe and supports concurrent request execution. diff --git a/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift new file mode 100644 index 000000000..069cfca7a --- /dev/null +++ b/Tests/SmithyCodegenCoreTests/SmithyCodegenCoreTests.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SmithyCodegenCore + +// No tests diff --git a/Tests/SmithyTests/ShapeIDTests.swift b/Tests/SmithyTests/ShapeIDTests.swift index d4b28de16..51b35b9f1 100644 --- a/Tests/SmithyTests/ShapeIDTests.swift +++ b/Tests/SmithyTests/ShapeIDTests.swift @@ -10,13 +10,23 @@ import Smithy class ShapeIDTests: XCTestCase { - func test_description_noMember() { - let subject = ShapeID("smithy.test", "TestShape") - XCTAssertEqual(subject.description, "smithy.test#TestShape") + func test_init_createsShapeIDWithNamespace() throws { + let subject = try ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.namespace, "smithy.test") } - func test_description_withMember() { - let subject = ShapeID("smithy.test", "TestShape", "TestMember") - XCTAssertEqual(subject.description, "smithy.test#TestShape$TestMember") + func test_init_createsShapeIDWithName() throws { + let subject = try ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.name, "TestName") + } + + func test_init_createsShapeIDWithMember() throws { + let subject = try ShapeID("smithy.test#TestName$TestMember") + XCTAssertEqual(subject.member, "TestMember") + } + + func test_init_createsShapeIDWithoutMember() throws { + let subject = try ShapeID("smithy.test#TestName") + XCTAssertNil(subject.member) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt index 3ad36bb44..ed05e4a00 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt @@ -99,6 +99,9 @@ class DirectedSwiftCodegen( integrations.forEach { it.writeAdditionalFiles(context, ctx, writers) } } + LOGGER.info("[${service.id}] Generating Smithy model file info") + SmithyModelFileInfoGenerator(context).writeSmithyModelFileInfo() + LOGGER.info("[${service.id}] Generating package manifest file") PackageManifestGenerator(context).writePackageManifest(writers.dependencies) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt index 0662d8d7d..8f4a69d8a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt @@ -1,14 +1,14 @@ package software.amazon.smithy.swift.codegen import software.amazon.smithy.aws.traits.ServiceTrait -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.core.GenerationContext import software.amazon.smithy.swift.codegen.model.getTrait class SmithyModelFileInfoGenerator( - val ctx: ProtocolGenerator.GenerationContext, + val ctx: GenerationContext, ) { fun writeSmithyModelFileInfo() { - ctx.service.getTrait()?.let { serviceTrait -> + ctx.model.serviceShapes.firstOrNull()?.getTrait()?.let { serviceTrait -> val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" val modelFileName = serviceTrait @@ -17,7 +17,7 @@ class SmithyModelFileInfoGenerator( .replace(",", "") .replace(" ", "-") val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" - ctx.delegator.useFileWriter(filename) { writer -> + ctx.writerDelegator().useFileWriter(filename) { writer -> writer.write("{\"path\":\"$contents\"}") } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt index cfd98792a..31c19f640 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt @@ -209,18 +209,20 @@ abstract class HTTPBindingProtocolGenerator( private fun usesSchemaBasedSerialization(ctx: ProtocolGenerator.GenerationContext): Boolean = // This fun is temporary; it will be eliminated when all services/protocols are moved to schema-based - ctx.service.allTraits.keys - .any { it.name == "rpcv2Cbor" } + false +// ctx.service.allTraits.keys +// .any { it.name == "rpcv2Cbor" } override fun generateSchemas(ctx: ProtocolGenerator.GenerationContext) { if (!usesSchemaBasedSerialization(ctx)) return // temporary condition val nestedShapes = resolveShapesNeedingSchema(ctx) .filter { it.type != ShapeType.MEMBER } // Member schemas are only rendered in-line - nestedShapes.forEach { renderSchemas(ctx, it) } + .sorted() + nestedShapes.forEach { renderSchema(ctx, it) } } - private fun renderSchemas( + private fun renderSchema( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { @@ -404,10 +406,7 @@ abstract class HTTPBindingProtocolGenerator( private fun resolveShapesNeedingSchema(ctx: ProtocolGenerator.GenerationContext): Set { val topLevelInputMembers = getHttpBindingOperations(ctx) - .flatMap { - val inputShape = ctx.model.expectShape(it.input.get()) - inputShape.members() - }.map { ctx.model.expectShape(it.target) } + .map { ctx.model.expectShape(it.input.get()) } .toSet() val topLevelOutputMembers = diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt index dd666794b..843821d46 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt @@ -49,7 +49,11 @@ class SchemaGenerator( } if (shape.members().isNotEmpty()) { writer.openBlock("members: [", "],") { - shape.members().withIndex().forEach { renderSchemaStruct(it.value, it.index) } + shape + .members() + .sorted() + .withIndex() + .forEach { renderSchemaStruct(it.value, it.index) } } } targetShape(shape)?.let { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt index 91a446af6..dd0ee07c0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaTraits.kt @@ -3,10 +3,12 @@ package software.amazon.smithy.swift.codegen.integration.serde.schema val permittedTraitIDs: Set = setOf( "smithy.api#sparse", + "smithy.api#input", + "smithy.api#output", + "smithy.api#error", "smithy.api#enumValue", "smithy.api#jsonName", "smithy.api#required", "smithy.api#default", "smithy.api#timestampFormat", - "smithy.api#httpPayload", )