diff --git a/Package.swift b/Package.swift index 828214760..e0474d213 100644 --- a/Package.swift +++ b/Package.swift @@ -308,7 +308,10 @@ let package = Package( ] ), .target( - name: "SmithyCodegenCore" + name: "SmithyCodegenCore", + dependencies: [ + "Smithy", + ] ), .testTarget( name: "ClientRuntimeTests", diff --git a/Sources/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/ShapeID.swift b/Sources/Smithy/Schema/ShapeID.swift index 647bbfeb8..a429b0fed 100644 --- a/Sources/Smithy/Schema/ShapeID.swift +++ b/Sources/Smithy/Schema/ShapeID.swift @@ -5,10 +5,13 @@ // SPDX-License-Identifier: Apache-2.0 // -/// Represents a single Smithy shape ID. +/// 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: Sendable, Hashable { +public struct ShapeID: Hashable, Sendable { public let namespace: String public let name: String public let member: String? @@ -27,16 +30,61 @@ public struct ShapeID: Sendable, Hashable { self.name = name self.member = member } -} -extension ShapeID: CustomStringConvertible { + 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 $") + } + } - /// Returns the absolute Shape ID in a single, printable string. - public var description: String { - if let member = self.member { + public init(id: ShapeID, member: String) { + self.namespace = id.namespace + self.name = id.name + self.member = member + } + + public var id: String { + if let member { return "\(namespace)#\(name)$\(member)" } else { return "\(namespace)#\(name)" } } } + +extension ShapeID: Comparable { + + public static func < (lhs: ShapeID, rhs: ShapeID) -> Bool { + return lhs.id.lowercased() < rhs.id.lowercased() + } +} + +extension ShapeID: CustomStringConvertible { + + /// Returns the absolute Shape ID in a single, printable string. + public var description: String { id } +} + +public struct ShapeIDError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/Smithy/Schema/ShapeType.swift b/Sources/Smithy/ShapeType.swift similarity index 100% rename from Sources/Smithy/Schema/ShapeType.swift rename to Sources/Smithy/ShapeType.swift diff --git a/Sources/SmithyCodegenCore/AST/ASTMember.swift b/Sources/SmithyCodegenCore/AST/ASTMember.swift index 5d3a30c35..6602103b2 100644 --- a/Sources/SmithyCodegenCore/AST/ASTMember.swift +++ b/Sources/SmithyCodegenCore/AST/ASTMember.swift @@ -5,8 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#ast-member struct ASTMember: Decodable { let target: String - let traits: [String: ASTNode]? + let traits: [String: Node]? } diff --git a/Sources/SmithyCodegenCore/AST/ASTModel.swift b/Sources/SmithyCodegenCore/AST/ASTModel.swift index 07de8bf89..c3c13d8ee 100644 --- a/Sources/SmithyCodegenCore/AST/ASTModel.swift +++ b/Sources/SmithyCodegenCore/AST/ASTModel.swift @@ -5,9 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#top-level-properties struct ASTModel: Decodable { let smithy: String - let metadata: ASTNode? + let metadata: Node? let shapes: [String: ASTShape] } diff --git a/Sources/SmithyCodegenCore/AST/ASTShape.swift b/Sources/SmithyCodegenCore/AST/ASTShape.swift index abeb8d425..a2ffd146b 100644 --- a/Sources/SmithyCodegenCore/AST/ASTShape.swift +++ b/Sources/SmithyCodegenCore/AST/ASTShape.swift @@ -5,11 +5,13 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum Smithy.Node + // See https://smithy.io/2.0/spec/json-ast.html#json-ast // This Swift type captures fields for all AST shape types struct ASTShape: Decodable { let type: ASTType - let traits: [String: ASTNode]? + let traits: [String: Node]? let member: ASTMember? let key: ASTMember? let value: ASTMember? diff --git a/Sources/SmithyCodegenCore/AST/ASTNode.swift b/Sources/SmithyCodegenCore/AST/Node+AST.swift similarity index 55% rename from Sources/SmithyCodegenCore/AST/ASTNode.swift rename to Sources/SmithyCodegenCore/AST/Node+AST.swift index a3d6fd58b..72f2e3239 100644 --- a/Sources/SmithyCodegenCore/AST/ASTNode.swift +++ b/Sources/SmithyCodegenCore/AST/Node+AST.swift @@ -5,24 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 // -/// Contains the value of a Smithy Node, as used in a JSON AST. -/// -/// Smithy node data is basically the same as the data that can be stored in JSON. -/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list. -/// -/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values -enum ASTNode { - case object([String: ASTNode]) - case list([ASTNode]) - case string(String) - case number(Double) - case boolean(Bool) - case null -} +import enum Smithy.Node -extension ASTNode: Decodable { +extension Node: Decodable { - init(from decoder: any Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null @@ -34,9 +21,9 @@ extension ASTNode: Decodable { self = .number(double) } else if let string = try? container.decode(String.self) { self = .string(string) - } else if let array = try? container.decode([ASTNode].self) { + } else if let array = try? container.decode([Node].self) { self = .list(array) - } else if let dictionary = try? container.decode([String: ASTNode].self) { + } else if let dictionary = try? container.decode([String: Node].self) { self = .object(dictionary) } else { throw ASTError("Undecodable value in AST node") diff --git a/Sources/SmithyCodegenCore/CodeGenerator.swift b/Sources/SmithyCodegenCore/CodeGenerator.swift index 24f7b056c..51d3eba50 100644 --- a/Sources/SmithyCodegenCore/CodeGenerator.swift +++ b/Sources/SmithyCodegenCore/CodeGenerator.swift @@ -27,8 +27,14 @@ public struct CodeGenerator { let modelData = try Data(contentsOf: modelFileURL) let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData) - // In the future, AST will be used to create a Model. - // Model will be used to generate code. + // Create the model from the AST + let model = try Model(astModel: astModel) + + // Create a generation context from the model + _ = try GenerationContext(model: model) + + // Generation context will be used here in the future + // to generate needed files. // This code simply writes an empty schemas file, since it is expected to exist after the // code generator plugin runs. diff --git a/Sources/SmithyCodegenCore/GenerationContext.swift b/Sources/SmithyCodegenCore/GenerationContext.swift new file mode 100644 index 000000000..4e3d7269e --- /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..74264c1df --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model+AST.swift @@ -0,0 +1,164 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +extension Model { + + /// Creates a Smithy model from a JSON AST model. + /// + /// Compared to the AST model, this model has custom shape types, members are included in the main body of shapes + /// along with other shape types, and all Shape IDs are fully-qualified + /// (i.e. members have the enclosing shape's namespace & name, along with their own member name.) + /// - Parameter astModel: The JSON AST model to be created. + convenience init(astModel: ASTModel) throws { + // Get all of the members from the AST model, create pairs of ShapeID & MemberShape + let idToMemberShapePairs = try astModel.shapes + .flatMap { try Self.memberShapePairs(id: $0.key, astShape: $0.value) } + let memberShapes = Dictionary(uniqueKeysWithValues: idToMemberShapePairs) + + // Get all of the non-members from the AST model, create pairs of ShapeID & various shape subclasses + let idToShapePairs = try astModel.shapes + .map { try Self.shapePair(id: $0.key, astShape: $0.value, memberShapes: memberShapes) } + + // Combine all shapes (member & nonmember) into one large Dict for inclusion in the model + let shapes = Dictionary(uniqueKeysWithValues: idToShapePairs + idToMemberShapePairs) + + // Initialize the properties of self + self.init(version: astModel.smithy, metadata: astModel.metadata, shapes: shapes) + + // self is now initialized, set all of the Shapes with references back to this model + self.shapes.values.forEach { $0.model = self } + + // Verify that there is exactly one Service + let services = self.shapes.values.filter { $0.type == .service } + guard services.count == 1 else { throw ModelError("Model has \(services.count) services") } + } + + private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, MemberShape)] { + var baseMembers = (astShape.members ?? [:]) + + // If this AST shape is an array, add a member for its element + if let member = astShape.member { + baseMembers["member"] = member + } + + // If this AST shape is a map, add members for its key & value + if let key = astShape.key { + baseMembers["key"] = key + } + if let value = astShape.value { + baseMembers["value"] = value + } + + // Map the AST members to ShapeID-to-MemberShape pairs & return the list of pairs + return try baseMembers.map { astMember in + // Create a ShapeID for this member + let memberID = ShapeID(id: try ShapeID(id), member: astMember.key) + + // Create traits for this member + let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value) } + let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? []) + + // Create a Shape ID for this member's target + let targetID = try ShapeID(astMember.value.target) + + // Create the ShapeID-to-MemberShape pair + return (memberID, MemberShape(id: memberID, traits: traits, targetID: targetID)) + } + } + + private static func shapePair( + id: String, astShape: ASTShape, memberShapes: [ShapeID: MemberShape] + ) throws -> (ShapeID, Shape) { + // Create the ShapeID for this shape from the AST shape's string ID. + let shapeID = try ShapeID(id) + + // Create model traits from the AST traits. + let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value) } ?? [] + let traits = Dictionary(uniqueKeysWithValues: idToTraitPairs) + + // Based on the AST shape type, create the appropriate Shape type. + switch astShape.type { + case .service: + let shape = ServiceShape( + id: shapeID, + traits: traits, + errorIDs: try astShape.errors?.map { try $0.id } ?? [] + ) + return (shapeID, shape) + case .operation: + let shape = OperationShape( + id: shapeID, + traits: traits, + input: try astShape.input?.id, + output: try astShape.output?.id + ) + return (shapeID, shape) + case .structure: + let shape = StructureShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .union: + let shape = UnionShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .enum: + let shape = EnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .intEnum: + let shape = IntEnumShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .list, .set: + let shape = ListShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + case .map: + let shape = MapShape( + id: shapeID, + traits: traits, + memberIDs: memberIDs(for: shapeID, memberShapes: memberShapes) + ) + return (shapeID, shape) + default: + let shape = Shape( + id: shapeID, + type: try astShape.type.modelType, + traits: traits + ) + + // Return the ShapeID-to-Shape pair. + return (shapeID, shape) + } + } + + private static func memberIDs(for shapeID: ShapeID, memberShapes: [ShapeID: MemberShape]) -> [ShapeID] { + // Given all the member shapes in this model, select the ones for the passed shape ID + // and return their IDs in sorted order. + memberShapes.keys.filter { + $0.namespace == shapeID.namespace && $0.name == shapeID.name && $0.member != nil + }.sorted() + } +} diff --git a/Sources/SmithyCodegenCore/Model/Model.swift b/Sources/SmithyCodegenCore/Model/Model.swift new file mode 100644 index 000000000..2ee603c47 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/Model.swift @@ -0,0 +1,28 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.Node +import struct Smithy.ShapeID + +public class Model { + public let version: String + public let metadata: Node? + public let shapes: [ShapeID: Shape] + + init(version: String, metadata: Node?, shapes: [ShapeID: Shape]) { + self.version = version + self.metadata = metadata + self.shapes = shapes + } + + func expectShape(id: ShapeID) throws -> Shape { + guard let shape = shapes[id] else { + throw ModelError("ShapeID \(id) was expected in model but not found") + } + return shape + } +} diff --git a/Sources/SmithyCodegenCore/Model/ModelError.swift b/Sources/SmithyCodegenCore/Model/ModelError.swift new file mode 100644 index 000000000..97d45fdb5 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ModelError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct ModelError: Error { + public let localizedDescription: String + + init(_ localizedDescription: String) { + self.localizedDescription = localizedDescription + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift new file mode 100644 index 000000000..e6d67a6a7 --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeID+AST.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.ShapeID + +extension ASTReference { + + var id: ShapeID { + get throws { + return try ShapeID(target) + } + } +} diff --git a/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift new file mode 100644 index 000000000..3f8332c5b --- /dev/null +++ b/Sources/SmithyCodegenCore/Model/ShapeType+AST.swift @@ -0,0 +1,68 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import enum Smithy.ShapeType + +extension ASTType { + + var modelType: ShapeType { + get throws { + switch self { + case .blob: + return .blob + case .boolean: + return .boolean + case .string: + return .string + case .timestamp: + return .timestamp + case .byte: + return .byte + case .short: + return .short + case .integer: + return .integer + case .long: + return .long + case .float: + return .float + case .document: + return .document + case .double: + return .double + case .bigDecimal: + return .bigDecimal + case .bigInteger: + return .bigInteger + case .`enum`: + return .`enum` + case .intEnum: + return .intEnum + case .list: + return .list + case .set: + return .set + case .map: + return .map + case .structure: + return .structure + case .union: + return .union + case .member: + return .member + case .service: + return .service + case .resource: + return .resource + case .operation: + return .operation + case .apply: + throw ModelError("\"apply\" AST shapes not implemented") + } + } + } +} diff --git a/Sources/SmithyCodegenCore/Shape/EnumShape.swift b/Sources/SmithyCodegenCore/Shape/EnumShape.swift new file mode 100644 index 000000000..b21948702 --- /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 } // swiftlint:disable:this force_cast + } + + 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..43703dc00 --- /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 } // swiftlint:disable:this force_cast + } + + 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..4dd331cf5 --- /dev/null +++ b/Sources/SmithyCodegenCore/Shape/ListShape.swift @@ -0,0 +1,33 @@ +// +// 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 { + // swiftlint:disable:next force_cast + 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..fa6d9bc36 --- /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 // swiftlint:disable:this force_cast + } + + public var value: MemberShape { + model.shapes[.init(id: id, member: "value")]! as! MemberShape // swiftlint:disable:this force_cast + } + + 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..29abb70ca --- /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..101257cfd --- /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..d955faf05 --- /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 enum Smithy.Node +import struct Smithy.ShapeID +import enum Smithy.ShapeType + +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..122b795d8 --- /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 } // swiftlint:disable:this force_cast + } + + 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..f6b7ba3e5 --- /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 } // swiftlint:disable:this force_cast + } + + override func candidates(for shape: Shape) -> [Shape] { + members.map { $0.target } + } +} diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift new file mode 100644 index 000000000..9ce726e89 --- /dev/null +++ b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProvider.swift @@ -0,0 +1,133 @@ +// +// 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 }) // swiftlint:disable:this force_cast + .first(where: { $0.inputShapeID == shape.id }) + else { throw SymbolProviderError("Operation for input \(shape.id) not found") } + return "\(operation.id.name)Input" + } else if + case .string(let name) = shape.getTrait(operationNameTraitID), shape.hasTrait(outputTraitID) { + return "\(name)Output" + } else if shape.hasTrait(outputTraitID) { + guard let operation = model.shapes.values + .filter({ $0.type == .operation }) + .map({ $0 as! OperationShape }) // swiftlint:disable:this force_cast + .first(where: { $0.outputShapeID == shape.id }) + else { throw SymbolProviderError("Operation for output \(shape.id) not found") } + return "\(operation.id.name)Output" + } else if shape.hasTrait(errorTraitID) { + return shape.id.name + } else { + return try "\(serviceName)ClientTypes.\(shape.id.name)" + } + } + + public func 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" + result = nonAlphaNumericRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // if there is an underscore, split on it: "acm_success" -> "acm", "_", "success" + result = underscoreRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " _ ") + + // if a number has a standalone v or V in front of it, separate it out + result = smallVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 v$2") + result = largeVRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 V$2") + + // add a space between camelCased words + result = camelCaseSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + + // add a space after acronyms + result = acronymSplitRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // add space after a number in the middle of a word + result = spaceAfterNumberRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: "$1 $2") + + // remove extra spaces - multiple consecutive ones or those and the beginning/end of words + result = removeExtraSpaceRegex.stringByReplacingMatches(in: result, range: result.range, withTemplate: " ") + .trimmingCharacters(in: .whitespaces) + + return result.components(separatedBy: " ") + } + + var range: NSRange { + NSRange(location: 0, length: count) + } +} + +// Regexes used in splitOnWordBoundaries() above. +// force_try linter rule is disabled since these are just created from static strings. +// swiftlint:disable force_try +private let nonAlphaNumericRegex = try! NSRegularExpression(pattern: "[^A-Za-z0-9+_]") +private let underscoreRegex = try! NSRegularExpression(pattern: "_") +private let smallVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})v([0-9]+)") +private let largeVRegex = try! NSRegularExpression(pattern: "([^a-z]{2,})V([0-9]+)") +private let camelCaseSplitRegex = try! NSRegularExpression(pattern: "(?<=[a-z])(?=[A-Z]([a-zA-Z]|[0-9]))") +private let acronymSplitRegex = try! NSRegularExpression(pattern: "([A-Z]+)([A-Z][a-z])") +private let spaceAfterNumberRegex = try! NSRegularExpression(pattern: "([0-9])([a-zA-Z])") +private let removeExtraSpaceRegex = try! NSRegularExpression(pattern: "\\s+") +// swiftlint:enable force_try diff --git a/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift b/Sources/SmithyCodegenCore/SymbolProvider/SymbolProviderError.swift 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/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.