diff --git a/Sources/Smithy/Document/Implementations/BigDecimalDocument.swift b/Sources/Smithy/Document/Implementations/BigDecimalDocument.swift index 69a8b7803..b5381fbdf 100644 --- a/Sources/Smithy/Document/Implementations/BigDecimalDocument.swift +++ b/Sources/Smithy/Document/Implementations/BigDecimalDocument.swift @@ -35,8 +35,8 @@ public struct BigDecimalDocument: SmithyDocument { return int } - public func asLong() throws -> Int64 { - guard let long = Int64(exactly: value) else { + public func asLong() throws -> Int { + guard let long = Int(exactly: value) else { throw DocumentError.numberOverflow("BigDecimal \(value) overflows long") } return long diff --git a/Sources/Smithy/Document/ShapeType.swift b/Sources/Smithy/Document/ShapeType.swift index 5b3de70a1..640b5c917 100644 --- a/Sources/Smithy/Document/ShapeType.swift +++ b/Sources/Smithy/Document/ShapeType.swift @@ -8,6 +8,14 @@ /// Reproduces the cases in Smithy `ShapeType`. /// https://github.com/smithy-lang/smithy/blob/main/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java public enum ShapeType { + + public enum Category { + case simple + case aggregate + case service + case member + } + case blob case boolean case string @@ -32,4 +40,18 @@ public enum ShapeType { case service case resource case operation + + public var category: Category { + switch self { + case .blob, .boolean, .string, .timestamp, .byte, .short, .integer, .long, + .float, .document, .double, .bigDecimal, .bigInteger, .enum, .intEnum: + return .simple + case .list, .set, .map, .structure, .union: + return .aggregate + case .service, .resource, .operation: + return .service + case .member: + return .member + } + } } diff --git a/Sources/SmithyJSON/Reader/Reader+ShapeDeserializer.swift b/Sources/SmithyJSON/Reader/Reader+ShapeDeserializer.swift new file mode 100644 index 000000000..6a8bad058 --- /dev/null +++ b/Sources/SmithyJSON/Reader/Reader+ShapeDeserializer.swift @@ -0,0 +1,259 @@ +// +// 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 struct Foundation.TimeInterval +import struct Smithy.Document +import protocol Smithy.SmithyDocument +import enum SmithyReadWrite.ReaderError +@_spi(SmithyReadWrite) import protocol SmithyReadWrite.ShapeDeserializer +@_spi(SmithyReadWrite) import protocol SmithyReadWrite.SchemaProtocol +@_spi(SmithyReadWrite) import struct SmithyReadWrite.Schema +@_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat + +@_spi(SmithyReadWrite) +extension Reader: SmithyReadWrite.ShapeDeserializer { + + public func readStructure(schema: SmithyReadWrite.Schema) throws -> Target? { + let resolvedReader = try resolvedReader(schema: schema) + let structureSchema = resolvedTargetSchema(schema: schema) + guard let factory = structureSchema.factory else { + throw ReaderError.invalidSchema("Missing factory for structure or union: \(structureSchema.id)") + } + guard resolvedReader.hasContent, resolvedReader.jsonNode == .object else { + return resolvedDefault(schema: schema) != nil ? factory() : nil + } + var value = factory() + try structureSchema.members.forEach { memberContainer in + try memberContainer.performRead(base: &value, key: "", reader: resolvedReader) + } + return value + } + + public func readList(schema: Schema<[T]>) throws -> [T]? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, resolvedReader.jsonNode == .array else { + return resolvedDefault(schema: schema) != nil ? [] : nil + } + let listSchema = resolvedTargetSchema(schema: schema) + guard let memberContainer = listSchema.members.first( + where: { $0.member.memberSchema().memberName == "member" } + ) else { + throw ReaderError.requiredValueNotPresent + } + var value = [T]() + for child in resolvedReader.children { + try memberContainer.performRead(base: &value, key: "", reader: child) + } + return value + } + + public func readMap(schema: Schema<[String: T]>) throws -> [String: T]? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, resolvedReader.jsonNode == .object else { + return resolvedDefault(schema: schema) != nil ? [:] : nil + } + let mapSchema = resolvedTargetSchema(schema: schema) + guard let valueContainer = mapSchema.members.first( + where: { $0.member.memberSchema().memberName == "value" } + ) else { + throw ReaderError.requiredValueNotPresent + } + var value = [String: T]() + for child in resolvedReader.children { + if !mapSchema.isSparse && child.jsonNode == .null { continue } + try valueContainer.performRead(base: &value, key: child.nodeInfo.name, reader: child) + } + return value + } + + public func readEnum(schema: Schema) throws -> T? where T.RawValue == String { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .string = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema).map { T(rawValue: try $0.asString())! } + } + guard let rawValue: String = try resolvedReader.readIfPresent() else { return nil } + return T(rawValue: rawValue) + } + + public func readIntEnum(schema: Schema) throws -> T? where T.RawValue == Int { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .number = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema).map { T(rawValue: try $0.asInteger())! } + } + guard let rawValue: Int = try resolvedReader.readIfPresent() else { return nil } + return T(rawValue: rawValue) + } + + public func readString(schema: Schema) throws -> String? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .string = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema)?.asString() + } + return try resolvedReader.readIfPresent() + } + + public func readBoolean(schema: SmithyReadWrite.Schema) throws -> Bool? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .bool = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema)?.asBoolean() + } + return try resolvedReader.readIfPresent() + } + + public func readByte(schema: SmithyReadWrite.Schema) throws -> Int8? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .number = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema)?.asByte() + } + return try resolvedReader.readIfPresent() + } + + public func readShort(schema: SmithyReadWrite.Schema) throws -> Int16? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .number = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema)?.asShort() + } + return try resolvedReader.readIfPresent() + } + + public func readInteger(schema: SmithyReadWrite.Schema) throws -> Int? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .number = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema)?.asInteger() + } + return try resolvedReader.readIfPresent() + } + + public func readLong(schema: SmithyReadWrite.Schema) throws -> Int? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .number = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema)?.asInteger() + } + return try resolvedReader.readIfPresent() + } + + public func readFloat(schema: SmithyReadWrite.Schema) throws -> Float? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent else { + return try resolvedDefault(schema: schema)?.asFloat() + } + return try resolvedReader.readIfPresent() + } + + public func readDouble(schema: SmithyReadWrite.Schema) throws -> Double? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent else { + return try resolvedDefault(schema: schema)?.asDouble() + } + return try resolvedReader.readIfPresent() + } + + public func readBigInteger(schema: SmithyReadWrite.Schema) throws -> Int64? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .number = resolvedReader.jsonNode else { + return try resolvedDefault(schema: schema)?.asBigInteger() + } + let int: Int? = try resolvedReader.readIfPresent() + return int.map { Int64($0) } + } + + public func readBigDecimal(schema: SmithyReadWrite.Schema) throws -> Double? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent else { + return try resolvedDefault(schema: schema)?.asDouble() + } + return try resolvedReader.readIfPresent() + } + + public func readBlob(schema: SmithyReadWrite.Schema) throws -> Data? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .string = resolvedReader.jsonNode else { + guard let base64String = try resolvedDefault(schema: schema)?.asString() else { return nil } + return Data(base64Encoded: base64String) + } + return try resolvedReader.readIfPresent() + } + + public func readTimestamp(schema: SmithyReadWrite.Schema) throws -> Date? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent else { + guard let defaultValue = resolvedDefault(schema: schema) else { return nil } + switch defaultValue.type { + case .float: + let interval = try TimeInterval(defaultValue.asFloat()) + return Date(timeIntervalSince1970: interval) + case .double: + return try Date(timeIntervalSince1970: defaultValue.asDouble()) + case .timestamp: + return try defaultValue.asTimestamp() + default: + throw ReaderError.invalidSchema("Unsupported timestamp default type: \(defaultValue.type)") + } + } + let memberSchema = schema.type == .member ? schema : nil + let timestampSchema = schema.targetSchema() ?? schema + let resolvedTimestampFormat = memberSchema?.timestampFormat ?? timestampSchema.timestampFormat + return try resolvedReader.readTimestampIfPresent(format: resolvedTimestampFormat ?? .epochSeconds) + } + + public func readDocument(schema: SmithyReadWrite.Schema) throws -> Document? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent else { + return resolvedDefault(schema: schema).map { Document($0) } + } + return try resolvedReader.readIfPresent() + } + + public func readNull(schema: any SmithyReadWrite.SchemaProtocol) throws -> Bool? { + let resolvedReader = try resolvedReader(schema: schema) + guard resolvedReader.hasContent, case .null = resolvedReader.jsonNode else { + return false + } + return try resolvedReader.readIfPresent() + } + + private func resolvedReader(schema: any SchemaProtocol) throws -> Reader { + if schema.httpPayload { + return self + } else if schema.containerType == .map || schema.containerType == .list || schema.containerType == .set { + return self + } else if schema.type == .member { + let resolvedName = try resolvedName(memberSchema: schema) + return self[NodeInfo(resolvedName)] + } else { + return self + } + } + + private func resolvedDefault(schema: Schema) -> (any SmithyDocument)? { + if schema.type == .member { + return schema.defaultValue ?? schema.targetSchema()?.defaultValue + } else { + return schema.defaultValue + } + } + + private func resolvedTargetSchema(schema: Schema) -> Schema { + schema.targetSchema() ?? schema + } + + private func resolvedName(memberSchema: any SchemaProtocol) throws -> String { + if respectsJSONName { + guard let resolvedName = memberSchema.jsonName ?? memberSchema.memberName else { + throw ReaderError.requiredValueNotPresent + } + return resolvedName + } else { + guard let resolvedName = memberSchema.memberName else { + throw ReaderError.requiredValueNotPresent + } + return resolvedName + } + } +} diff --git a/Sources/SmithyJSON/Reader/Reader.swift b/Sources/SmithyJSON/Reader/Reader.swift index 9836bd14b..52ba6409e 100644 --- a/Sources/SmithyJSON/Reader/Reader.swift +++ b/Sources/SmithyJSON/Reader/Reader.swift @@ -6,7 +6,6 @@ // @_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyReader -import protocol Smithy.SmithyDocument import struct Smithy.Document import typealias SmithyReadWrite.ReadingClosure import enum SmithyReadWrite.ReaderError @@ -26,6 +25,9 @@ public final class Reader: SmithyReader { public let nodeInfo: NodeInfo let jsonNode: JSONNode? + public var respectsJSONName = false { + didSet { children.forEach { $0.respectsJSONName = respectsJSONName } } + } public internal(set) var children = [Reader]() public internal(set) weak var parent: Reader? public var hasContent: Bool { jsonNode != nil && jsonNode != .null } @@ -33,6 +35,7 @@ public final class Reader: SmithyReader { init(nodeInfo: NodeInfo, jsonObject: Any?, parent: Reader? = nil) throws { self.nodeInfo = nodeInfo self.jsonNode = try Self.jsonNode(for: jsonObject) + self.respectsJSONName = parent?.respectsJSONName ?? false self.parent = parent self.children = try Self.children(from: jsonObject, parent: self) } @@ -40,6 +43,7 @@ public final class Reader: SmithyReader { init(nodeInfo: NodeInfo, parent: Reader?) { self.nodeInfo = nodeInfo self.jsonNode = nil + self.respectsJSONName = parent?.respectsJSONName ?? false self.parent = parent } diff --git a/Sources/SmithyReadWrite/Schema/Prelude.swift b/Sources/SmithyReadWrite/Schema/Prelude.swift new file mode 100644 index 000000000..102d13c56 --- /dev/null +++ b/Sources/SmithyReadWrite/Schema/Prelude.swift @@ -0,0 +1,105 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +@_spi(SmithyDocumentImpl) import Smithy + +@_spi(SmithyReadWrite) +public var unitSchema: Schema { + Schema(id: "smithy.api#Unit", type: .structure, factory: { Unit() }) +} + +@_spi(SmithyReadWrite) +public var booleanSchema: Schema { + Schema(id: "smithy.api#Boolean", type: .boolean) +} + +@_spi(SmithyReadWrite) +public var stringSchema: Schema { + Schema(id: "smithy.api#String", type: .string) +} + +@_spi(SmithyReadWrite) +public var integerSchema: Schema { + Schema(id: "smithy.api#Integer", type: .integer) +} + +@_spi(SmithyReadWrite) +public var blobSchema: Schema { + Schema(id: "smithy.api#Blob", type: .blob) +} + +@_spi(SmithyReadWrite) +public var timestampSchema: Schema { + Schema(id: "smithy.api#Timestamp", type: .timestamp) +} + +@_spi(SmithyReadWrite) +public var byteSchema: Schema { + Schema(id: "smithy.api#Byte", type: .byte) +} + +@_spi(SmithyReadWrite) +public var shortSchema: Schema { + Schema(id: "smithy.api#Short", type: .short) +} + +@_spi(SmithyReadWrite) +public var longSchema: Schema { + Schema(id: "smithy.api#Long", type: .long) +} + +@_spi(SmithyReadWrite) +public var floatSchema: Schema { + Schema(id: "smithy.api#Float", type: .float) +} + +@_spi(SmithyReadWrite) +public var doubleSchema: Schema { + Schema(id: "smithy.api#Double", type: .double) +} + +@_spi(SmithyReadWrite) +public var documentSchema: Schema { + Schema(id: "smithy.api#PrimitiveDocument", type: .document) +} + +@_spi(SmithyReadWrite) +public var primitiveBooleanSchema: Schema { + Schema(id: "smithy.api#PrimitiveBoolean", type: .boolean, defaultValue: BooleanDocument(value: false)) +} + +@_spi(SmithyReadWrite) +public var primitiveIntegerSchema: Schema { + Schema(id: "smithy.api#PrimitiveInteger", type: .integer, defaultValue: IntegerDocument(value: 0)) +} + +@_spi(SmithyReadWrite) +public var primitiveByteSchema: Schema { + Schema(id: "smithy.api#PrimitiveByte", type: .byte, defaultValue: ByteDocument(value: 0)) +} + +@_spi(SmithyReadWrite) +public var primitiveShortSchema: Schema { + Schema(id: "smithy.api#PrimitiveShort", type: .short, defaultValue: ShortDocument(value: 0)) +} + +@_spi(SmithyReadWrite) +public var primitiveLongSchema: Schema { + Schema(id: "smithy.api#PrimitiveLong", type: .long, defaultValue: LongDocument(value: 0)) +} + +@_spi(SmithyReadWrite) +public var primitiveFloatSchema: Schema { + Schema(id: "smithy.api#PrimitiveFloat", type: .float, defaultValue: FloatDocument(value: 0.0)) +} + +@_spi(SmithyReadWrite) +public var primitiveDoubleSchema: Schema { + Schema(id: "smithy.api#PrimitiveDouble", type: .double, defaultValue: DoubleDocument(value: 0.0)) +} diff --git a/Sources/SmithyReadWrite/Schema/Schema.swift b/Sources/SmithyReadWrite/Schema/Schema.swift new file mode 100644 index 000000000..5746dc49f --- /dev/null +++ b/Sources/SmithyReadWrite/Schema/Schema.swift @@ -0,0 +1,152 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date +@_spi(SmithyDocumentImpl) import Smithy +@_spi(SmithyTimestamps) import SmithyTimestamps + +@_spi(SmithyReadWrite) +public protocol SchemaProtocol { + var id: String { get } + var type: ShapeType { get } + var defaultValue: (any SmithyDocument)? { get } + var jsonName: String? { get } + var httpPayload: Bool { get } + var enumValue: (any SmithyDocument)? { get } + var memberName: String? { get } + var containerType: ShapeType? { get } + var isRequired: Bool { get } +} + +public extension SchemaProtocol { + + var lastResortDefaultValue: any SmithyDocument { + get throws { + switch type { + case .structure, .union, .map: return StringMapDocument(value: [:]) + case .string, .enum: return StringDocument(value: "") + case .integer, .intEnum: return IntegerDocument(value: 0) + case .boolean: return BooleanDocument(value: false) + case .blob: return BlobDocument(value: Data()) + case .timestamp: return TimestampDocument(value: Date(timeIntervalSince1970: 0.0)) + case .bigDecimal: return BigDecimalDocument(value: 0.0) + case .bigInteger: return BigIntegerDocument(value: 0) + case .byte: return ByteDocument(value: 0) + case .document: return NullDocument() + case .list, .set: return ListDocument(value: []) + case .short: return ShortDocument(value: 0) + case .long: return LongDocument(value: 0) + case .float: return FloatDocument(value: 0.0) + case .double: return DoubleDocument(value: 0.0) + case .member, .service, .resource, .operation: + throw ReaderError.invalidSchema("Last resort not defined for type \(type)") + } + } + } +} + +@_spi(SmithyReadWrite) +public protocol MemberProtocol { + associatedtype Base + var memberSchema: () -> (any SchemaProtocol) { get } + func performRead(base: inout Base, key: String, reader: any ShapeDeserializer) throws + func performWrite(base: Base, writer: any SmithyWriter) throws +} + +@_spi(SmithyReadWrite) +public struct MemberContainer { + public let member: any MemberProtocol + + public init(member: any MemberProtocol) { + self.member = member + } + + public func performRead(base: inout Base, key: String, reader: any ShapeDeserializer) throws { + try member.performRead(base: &base, key: key, reader: reader) + } + + public func performWrite(base: Base, writer: any SmithyWriter) throws { + try member.performWrite(base: base, writer: writer) + } +} + +@_spi(SmithyReadWrite) +public struct Schema: SchemaProtocol { + + public struct Member: MemberProtocol { + public var memberSchema: () -> (any SchemaProtocol) { { memberSchemaSpecific() } } + public let memberSchemaSpecific: () -> Schema + let readBlock: (inout Base, String, any ShapeDeserializer) throws -> Void + let writeBlock: (Base, any SmithyWriter) throws -> Void + + public init( + memberSchema: @escaping () -> Schema, + readBlock: @escaping (inout Base, String, any ShapeDeserializer) throws -> Void = { _, _, _ in }, + writeBlock: @escaping (Base, any SmithyWriter) throws -> Void = { _, _ in } + ) { + self.memberSchemaSpecific = memberSchema + self.readBlock = readBlock + self.writeBlock = writeBlock + } + + public func performRead(base: inout Base, key: String, reader: any ShapeDeserializer) throws { + try readBlock(&base, key, reader) + } + + public func performWrite(base: Base, writer: any SmithyWriter) throws { + try writeBlock(base, writer) + } + } + + public let id: String + public let type: ShapeType + public let factory: (() -> Base)? + public let members: [MemberContainer] + public let targetSchema: () -> Schema? + public let memberName: String? + public let containerType: ShapeType? + public let jsonName: String? + public let httpPayload: Bool + public let enumValue: (any SmithyDocument)? + public let timestampFormat: SmithyTimestamps.TimestampFormat? + public let isSparse: Bool + public let isRequired: Bool + public let defaultValue: (any Smithy.SmithyDocument)? + + public init( + id: String, + type: ShapeType, + factory: (() -> Base)? = nil, + members: [MemberContainer] = [], + targetSchema: @escaping () -> Schema? = { nil }, + memberName: String? = nil, + containerType: ShapeType? = nil, + jsonName: String? = nil, + httpPayload: Bool = false, + enumValue: (any SmithyDocument)? = nil, + timestampFormat: SmithyTimestamps.TimestampFormat? = nil, + isSparse: Bool = false, + isRequired: Bool = false, + defaultValue: (any SmithyDocument)? = nil + ) { + self.id = id + self.type = type + self.factory = factory + self.members = members + self.targetSchema = targetSchema + self.memberName = memberName + self.containerType = containerType + self.jsonName = jsonName + self.httpPayload = httpPayload + self.enumValue = enumValue + self.timestampFormat = timestampFormat + self.isSparse = isSparse + self.isRequired = isRequired + self.defaultValue = defaultValue + } +} diff --git a/Sources/SmithyReadWrite/Schema/Unit.swift b/Sources/SmithyReadWrite/Schema/Unit.swift new file mode 100644 index 000000000..bd0c8e887 --- /dev/null +++ b/Sources/SmithyReadWrite/Schema/Unit.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) import Smithy + +@_spi(SmithyReadWrite) +public struct Unit: Sendable, Equatable { + + public static func write(value: Unit, to writer: Writer) throws { + try writer.write(Document(StringMapDocument(value: [:]))) + } + + public init() {} +} diff --git a/Sources/SmithyReadWrite/ShapeDeserializer.swift b/Sources/SmithyReadWrite/ShapeDeserializer.swift new file mode 100644 index 000000000..1dd3a6b34 --- /dev/null +++ b/Sources/SmithyReadWrite/ShapeDeserializer.swift @@ -0,0 +1,195 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Smithy.Document +@_spi(SmithyDocumentImpl) import struct Smithy.NullDocument +import struct Foundation.Data +import struct Foundation.Date + +@_spi(SmithyReadWrite) +public protocol ShapeDeserializer: SmithyReader { + func readStructure(schema: Schema) throws -> T? + func readList(schema: Schema<[T]>) throws -> [T]? + func readMap(schema: Schema<[String: T]>) throws -> [String: T]? + func readBoolean(schema: Schema) throws -> Bool? + func readByte(schema: Schema) throws -> Int8? + func readShort(schema: Schema) throws -> Int16? + func readInteger(schema: Schema) throws -> Int? + func readLong(schema: Schema) throws -> Int? + func readFloat(schema: Schema) throws -> Float? + func readDouble(schema: Schema) throws -> Double? + func readBigInteger(schema: Schema) throws -> Int64? + func readBigDecimal(schema: Schema) throws -> Double? + func readString(schema: Schema) throws -> String? + func readBlob(schema: Schema) throws -> Data? + func readTimestamp(schema: Schema) throws -> Date? + func readDocument(schema: Schema) throws -> Document? + func readNull(schema: SchemaProtocol) throws -> Bool? + func readEnum(schema: Schema) throws -> T? where T.RawValue == String + func readIntEnum(schema: Schema) throws -> T? where T.RawValue == Int +} + +@_spi(SmithyReadWrite) +public extension ShapeDeserializer { + + func readEnumNonNull(schema: Schema) throws -> T where T.RawValue == String { + guard let value: T = try readEnum(schema: schema) else { + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readIntEnumNonNull(schema: Schema) throws -> T where T.RawValue == Int { + guard let value: T = try readIntEnum(schema: schema) else { + throw ReaderError.requiredValueNotPresent + } + return value + } +} + +@_spi(SmithyReadWrite) +public extension ShapeDeserializer { + + func readStructureNonNull(schema: Schema) throws -> T { + guard let value = try readStructure(schema: schema) else { + guard let factory = schema.factory else { + throw SmithyReadWrite.ReaderError.invalidSchema("Missing factory for structure or union: \(schema.id)") + } + if schema.isRequired { return factory() } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readListNonNull(schema: Schema<[T]>) throws -> [T] { + guard let value = try readList(schema: schema) else { + if schema.isRequired { return [] } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readMapNonNull(schema: Schema<[String: T]>) throws -> [String: T] { + guard let value = try readMap(schema: schema) else { + if schema.isRequired { return [:] } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readBooleanNonNull(schema: Schema) throws -> Bool { + guard let value = try readBoolean(schema: schema) else { + if schema.isRequired { return false } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readByteNonNull(schema: Schema) throws -> Int8 { + guard let value = try readByte(schema: schema) else { + if schema.isRequired { return 0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readShortNonNull(schema: Schema) throws -> Int16 { + guard let value = try readShort(schema: schema) else { + if schema.isRequired { return 0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readIntegerNonNull(schema: Schema) throws -> Int { + guard let value = try readInteger(schema: schema) else { + if schema.isRequired { return 0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readLongNonNull(schema: Schema) throws -> Int { + guard let value = try readLong(schema: schema) else { + if schema.isRequired { return 0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readFloatNonNull(schema: Schema) throws -> Float { + guard let value = try readFloat(schema: schema) else { + if schema.isRequired { return 0.0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readDoubleNonNull(schema: Schema) throws -> Double { + guard let value = try readDouble(schema: schema) else { + if schema.isRequired { return 0.0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readBigIntegerNonNull(schema: Schema) throws -> Int64 { + guard let value = try readBigInteger(schema: schema) else { + if schema.isRequired { return 0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readBigDecimalNonNull(schema: Schema) throws -> Double { + guard let value = try readBigDecimal(schema: schema) else { + if schema.isRequired { return 0.0 } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readStringNonNull(schema: Schema) throws -> String { + guard let value = try readString(schema: schema) else { + if schema.isRequired { return "" } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readBlobNonNull(schema: Schema) throws -> Data { + guard let value = try readBlob(schema: schema) else { + if schema.isRequired { return Data() } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readTimestampNonNull(schema: Schema) throws -> Date { + guard let value = try readTimestamp(schema: schema) else { + if schema.isRequired { return Date(timeIntervalSince1970: 0.0) } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readDocumentNonNull(schema: Schema) throws -> Document { + guard let value = try readDocument(schema: schema) else { + if schema.isRequired { return Document(NullDocument()) } + throw ReaderError.requiredValueNotPresent + } + return value + } + + func readNullNonNull(schema: SchemaProtocol) throws -> Bool { + guard let value = try readNull(schema: schema) else { + if schema.isRequired { return false } + throw ReaderError.requiredValueNotPresent + } + return value + } +} diff --git a/Sources/SmithyReadWrite/SmithyReader.swift b/Sources/SmithyReadWrite/SmithyReader.swift index 7fc95afe1..ad8ad7741 100644 --- a/Sources/SmithyReadWrite/SmithyReader.swift +++ b/Sources/SmithyReadWrite/SmithyReader.swift @@ -209,4 +209,5 @@ public extension SmithyReader { public enum ReaderError: Error { case requiredValueNotPresent case invalidType(String) + case invalidSchema(String) } diff --git a/Sources/SmithyReadWrite/SmithyWriter.swift b/Sources/SmithyReadWrite/SmithyWriter.swift index e27d4132f..c890063e6 100644 --- a/Sources/SmithyReadWrite/SmithyWriter.swift +++ b/Sources/SmithyReadWrite/SmithyWriter.swift @@ -28,7 +28,7 @@ public protocol SmithyWriter: AnyObject { func write(_ value: Int16?) throws func write(_ value: UInt8?) throws func write(_ value: Data?) throws - func write(_ value: SmithyDocument?) throws + func write(_ value: (any SmithyDocument)?) throws func writeTimestamp(_ value: Date?, format: TimestampFormat) throws func write(_ value: T?) throws where T.RawValue == Int func write(_ value: T?) throws where T.RawValue == String diff --git a/Sources/SmithyReadWrite/WritingClosure.swift b/Sources/SmithyReadWrite/WritingClosure.swift index fcc3fe20a..9a7a95100 100644 --- a/Sources/SmithyReadWrite/WritingClosure.swift +++ b/Sources/SmithyReadWrite/WritingClosure.swift @@ -8,7 +8,7 @@ import struct Foundation.Data import struct Foundation.Date @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat -import protocol Smithy.SmithyDocument +import struct Smithy.Document @_spi(SmithyReadWrite) public typealias WritingClosure = (T, Writer) throws -> Void @@ -103,7 +103,7 @@ public enum WritingClosures { try writer.write(value) } - public static func writeDocument(value: SmithyDocument?, to writer: Writer) throws { + public static func writeDocument(value: Document?, to writer: Writer) throws { try writer.write(value) } } 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 a68c9c159..37c459ef7 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 @@ -80,6 +80,7 @@ class DirectedSwiftCodegen(val context: PluginContext) : generateMessageMarshallable(ctx) generateMessageUnmarshallable(ctx) generateCodableConformanceForNestedTypes(ctx) + generateSchemas(ctx) initializeMiddleware(ctx) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EnumGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EnumGenerator.kt index ce41d4878..a11b98268 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EnumGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EnumGenerator.kt @@ -142,14 +142,18 @@ class EnumGenerator( private fun renderEnum() { writer.writeShapeDocs(shape) writer.writeAvailableAttribute(null, shape) - writer.openBlock( - "public enum \$enum.name:L: \$N, \$N, \$N, \$N, \$N {", - "}", + val conformances = writer.format( + "\$N, \$N, \$N, \$N, \$N", SwiftTypes.Protocols.Sendable, SwiftTypes.Protocols.Equatable, SwiftTypes.Protocols.RawRepresentable, SwiftTypes.Protocols.CaseIterable, - SwiftTypes.Protocols.Hashable + SwiftTypes.Protocols.Hashable, + ) + writer.openBlock( + "public enum \$enum.name:L: \$L {", + "}", + conformances, ) { createEnumWriterContexts() // add the sdkUnknown case which will always be last diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/IntEnumGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/IntEnumGenerator.kt index 45b27b3d7..bf6584f54 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/IntEnumGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/IntEnumGenerator.kt @@ -41,14 +41,18 @@ class IntEnumGenerator( private fun renderEnum() { writer.writeShapeDocs(shape) writer.writeAvailableAttribute(null, shape) - writer.openBlock( - "public enum \$enum.name:L: \$N, \$N, \$N, \$N, \$N {", - "}", + val conformances = writer.format( + "\$N, \$N, \$N, \$N, \$N", SwiftTypes.Protocols.Sendable, SwiftTypes.Protocols.Equatable, SwiftTypes.Protocols.RawRepresentable, SwiftTypes.Protocols.CaseIterable, - SwiftTypes.Protocols.Hashable + SwiftTypes.Protocols.Hashable, + ) + writer.openBlock( + "public enum \$enum.name:L: \$L {", + "}", + conformances, ) { createEnumWriterContexts() // add the sdkUnknown case which will always be last diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt index e5b7ec7ed..fc4a72e64 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt @@ -87,7 +87,7 @@ class ShapeValueGenerator( private fun unionDecl(writer: SwiftWriter, shape: UnionShape, block: () -> Unit) { val symbol = symbolProvider.toSymbol(shape) - writer.writeInline("\$N.", symbol).call { block() }.write(")") + writer.writeInline("\$N.", symbol).call { block() } } private fun documentDecl(writer: SwiftWriter, node: Node) { @@ -231,8 +231,13 @@ class ShapeValueGenerator( CodegenException("unknown member ${currShape.id}.${keyNode.value}") } memberShape = generator.model.expectShape(member.target) - writer.writeInline("\$L(", lowerCase(keyNode.value)) - generator.writeShapeValueInline(writer, memberShape, valueNode) + if (member.target.toString() != "smithy.api#Unit") { + writer.writeInline("\$L(", lowerCase(keyNode.value)) + generator.writeShapeValueInline(writer, memberShape, valueNode) + writer.writeInline(")") + } else { + writer.writeInline("\$L", lowerCase(keyNode.value)) + } } else -> throw CodegenException("unexpected shape type " + currShape.type) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt index 7f8d24f25..1d273b635 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt @@ -117,16 +117,19 @@ class StructureGenerator( writer.writeShapeDocs(shape) writer.writeAvailableAttribute(model, shape) val equatableConformance = writer.format(", \$N", SwiftTypes.Protocols.Equatable).takeIf { shape.hasTrait() } ?: "" - writer.openBlock("public struct \$struct.name:L: \$N$equatableConformance {", SwiftTypes.Protocols.Sendable) - .call { generateStructMembers() } - .write("") - .call { generateInitializerForStructure(false) } - .closeBlock("}") + writer.openBlock( + "public struct \$struct.name:L: \$N$equatableConformance {", "}", + SwiftTypes.Protocols.Sendable, + ) { + generateStructMembers() + writer.write("") + generateInitializerForStructure(false) + } } private fun generateStructMembers() { membersSortedByName.forEach { - var (memberName, memberSymbol) = memberShapeDataContainer.getOrElse(it) { return@forEach } + val (memberName, memberSymbol) = memberShapeDataContainer.getOrElse(it) { return@forEach } writer.writeMemberDocs(model, it) val indirect = it.hasTrait() var indirectOrNot = "" @@ -149,7 +152,7 @@ class StructureGenerator( val (memberName, memberSymbol) = memberShapeDataContainer.getOrElse(member) { Pair(null, null) } if (memberName == null || memberSymbol == null) continue val terminator = if (index == membersSortedByName.size - 1) "" else "," - writer.write("\$L: \$D$terminator", memberName, memberSymbol) + writer.write("\$L: \$D\$L", memberName, memberSymbol, terminator) } } writer.write(") {") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt index 4942e639b..b2db06531 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftSymbolProvider.kt @@ -174,6 +174,17 @@ class SwiftSymbolProvider(private val model: Model, val swiftSettings: SwiftSett if (shape.hasTrait() && service != null && !shape.hasTrait()) { builder.namespace(service.nestedNamespaceType(this).name, ".") } + if (shape.id.namespace == "smithy.api") { + when (shape.id.name) { + "Unit" -> { + builder.addDependency(SwiftDependency.SMITHY_READ_WRITE) + builder.namespace(SwiftDependency.SMITHY_READ_WRITE.target, ".") + builder.name("Unit") + builder.putProperty("spiNames", listOf("SmithyReadWrite")) + } + else -> throw Exception("Unhandled prelude type converted to Symbol") + } + } return builder.build() } @@ -243,7 +254,7 @@ class SwiftSymbolProvider(private val model: Model, val swiftSettings: SwiftSett override fun documentShape(shape: DocumentShape): Symbol { return createSymbolBuilder(shape, "Document", "Smithy", SwiftDeclaration.STRUCT, true) - .addDependency(SwiftDependency.SMITHY_READ_WRITE) + .addDependency(SwiftDependency.SMITHY) .build() } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt index 1cd21c3b2..10c38e2d4 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt @@ -88,7 +88,11 @@ class UnionGenerator( writer.writeMemberDocs(model, member) val enumCaseName = symbolProvider.toMemberName(member) val enumCaseAssociatedType = symbolProvider.toSymbol(member) - writer.write("case \$L(\$L)", enumCaseName, enumCaseAssociatedType) + if (member.target.toString() != "smithy.api#Unit") { + writer.write("case \$L(\$N)", enumCaseName, enumCaseAssociatedType) + } else { + writer.write("case \$L", enumCaseName) + } } // add the sdkUnknown case which will always be last writer.write("case sdkUnknown(\$N)", SwiftTypes.String) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt index ed0ede05a..36b076fa4 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt @@ -57,7 +57,7 @@ class EndpointParamsGenerator( val memberName = param.name.toString().toLowerCamelCase() val memberSymbol = param.toSymbol() val terminator = if (index != parameters.lastIndex) "," else "" - writer.write("$memberName: \$D$terminator", memberSymbol) + writer.write("\$L: \$D\$L", memberName, memberSymbol, terminator) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/events/MessageUnmarshallableGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/events/MessageUnmarshallableGenerator.kt index 81bcb64e5..3169f016a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/events/MessageUnmarshallableGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/events/MessageUnmarshallableGenerator.kt @@ -11,6 +11,10 @@ import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.HTTPProtocolCustomizable import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.serde.readwrite.ReadingClosureUtils +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.WireProtocol +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.responseWireProtocol +import software.amazon.smithy.swift.codegen.integration.serde.schema.readMethodName +import software.amazon.smithy.swift.codegen.integration.serde.schema.schemaVar import software.amazon.smithy.swift.codegen.integration.serde.struct.readerSymbol import software.amazon.smithy.swift.codegen.model.eventStreamErrors import software.amazon.smithy.swift.codegen.model.eventStreamEvents @@ -204,7 +208,25 @@ class MessageUnmarshallableGenerator( } } - private fun renderReadToValue(writer: SwiftWriter, memberShape: MemberShape) { + private fun renderReadToValue(writer: SwiftWriter, member: MemberShape) { + if (ctx.service.responseWireProtocol == WireProtocol.JSON) { + renderReadWithSchemaToValue(writer, member) + } else { + renderReadWithSerdeToValue(writer, member) + } + } + + private fun renderReadWithSchemaToValue(writer: SwiftWriter, memberShape: MemberShape) { + val target = ctx.model.expectShape(memberShape.target) + writer.write( + "let value = try \$N.from(data: message.payload).\$LNonNull(schema: \$L)", + ctx.service.readerSymbol, + target.readMethodName, + target.schemaVar(writer), + ) + } + + private fun renderReadWithSerdeToValue(writer: SwiftWriter, memberShape: MemberShape) { val readingClosure = ReadingClosureUtils(ctx, writer).readingClosure(memberShape) writer.write( "let value = try \$N.readFrom(message.payload, with: \$L)", 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 80d5b39e9..fca80c6ed 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 @@ -23,7 +23,6 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait -import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.model.traits.HttpHeaderTrait import software.amazon.smithy.model.traits.HttpLabelTrait import software.amazon.smithy.model.traits.HttpPayloadTrait @@ -37,6 +36,7 @@ import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.customtraits.NeedsReaderTrait import software.amazon.smithy.swift.codegen.customtraits.NeedsWriterTrait +import software.amazon.smithy.swift.codegen.customtraits.NestedTrait import software.amazon.smithy.swift.codegen.events.MessageMarshallableGenerator import software.amazon.smithy.swift.codegen.events.MessageUnmarshallableGenerator import software.amazon.smithy.swift.codegen.integration.httpResponse.HTTPResponseGenerator @@ -57,6 +57,9 @@ import software.amazon.smithy.swift.codegen.integration.middlewares.SignerMiddle import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpHeaderProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpQueryItemProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpUrlPathProvider +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.WireProtocol +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.responseWireProtocol +import software.amazon.smithy.swift.codegen.integration.serde.schema.SchemaGenerator import software.amazon.smithy.swift.codegen.integration.serde.struct.StructDecodeGenerator import software.amazon.smithy.swift.codegen.integration.serde.struct.StructEncodeGenerator import software.amazon.smithy.swift.codegen.integration.serde.union.UnionDecodeGenerator @@ -72,9 +75,11 @@ import software.amazon.smithy.swift.codegen.model.targetOrSelf import software.amazon.smithy.swift.codegen.supportsStreamingAndIsRPC import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.utils.ModelFileUtils +import software.amazon.smithy.swift.codegen.utils.SchemaFileUtils import software.amazon.smithy.utils.OptionalUtils import java.util.Optional import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull private val Shape.isStreaming: Boolean get() = hasTrait() && isUnionShape @@ -207,11 +212,42 @@ abstract class HTTPBindingProtocolGenerator( } } + override fun generateSchemas(ctx: ProtocolGenerator.GenerationContext) { + if (ctx.service.responseWireProtocol != WireProtocol.JSON) { return } + val nestedShapes = resolveShapesNeedingSchema(ctx) + .filter { !it.hasTrait() } + .filter { + !( + it.asMemberShape().getOrNull()?.let { + ctx.model.expectShape(it.target).hasTrait() + } ?: false + ) + } + for (shape in nestedShapes) { + renderSchemas(ctx, shape) + } + } + + private fun renderSchemas(ctx: ProtocolGenerator.GenerationContext, shape: Shape) { + val symbol: Symbol = ctx.symbolProvider.toSymbol(shape) + val symbolName = symbol.name + val filename = SchemaFileUtils.filename(ctx.settings, "${shape.id.name}+Schema") + val encodeSymbol = Symbol.builder() + .definitionFile(filename) + .name(symbolName) + .build() + ctx.delegator.useShapeWriter(encodeSymbol) { writer -> + SchemaGenerator(ctx, writer).renderSchema(shape) + } + } + fun renderCodableExtension( ctx: ProtocolGenerator.GenerationContext, shape: Shape, ) { if (!shape.hasTrait() && !shape.hasTrait()) { return } + val shapeUsesSchemaBasedRead = ctx.service.responseWireProtocol == WireProtocol.JSON && shape.hasTrait() + if (shapeUsesSchemaBasedRead && !shape.hasTrait()) { return } val symbol: Symbol = ctx.symbolProvider.toSymbol(shape) val symbolName = symbol.name val filename = ModelFileUtils.filename(ctx.settings, "$symbolName+ReadWrite") @@ -226,12 +262,11 @@ abstract class HTTPBindingProtocolGenerator( is StructureShape -> { // get all members sorted by name and filter out either all members with other traits OR members with the payload trait val httpBodyMembers = members.filter { it.isInHttpBody() } - val path = "properties.".takeIf { shape.hasTrait() } ?: "" if (shape.hasTrait()) { writer.write("") renderStructEncode(ctx, shape, mapOf(), httpBodyMembers, writer) } - if (shape.hasTrait()) { + if (shape.hasTrait() && !shapeUsesSchemaBasedRead) { writer.write("") renderStructDecode(ctx, shape, mapOf(), httpBodyMembers, writer) } @@ -241,7 +276,7 @@ abstract class HTTPBindingProtocolGenerator( writer.write("") UnionEncodeGenerator(ctx, shape, members, writer).render() } - if (shape.hasTrait()) { + if (shape.hasTrait() && !shapeUsesSchemaBasedRead) { writer.write("") UnionDecodeGenerator(ctx, shape, members, writer).render() } @@ -350,6 +385,66 @@ abstract class HTTPBindingProtocolGenerator( return resolved } + private fun resolveShapesNeedingSchema(ctx: ProtocolGenerator.GenerationContext): Set { + val topLevelOutputMembers = getHttpBindingOperations(ctx) + .map { ctx.model.expectShape(it.output.get()) } + .toSet() + + val topLevelErrorMembers = getHttpBindingOperations(ctx) + .flatMap { it.errors } + .map { ctx.model.expectShape(it) } + .toSet() + + val topLevelServiceErrorMembers = ctx.service.errors + .map { ctx.model.expectShape(it) } + .toSet() + + // Input members excluded from schema generation until schema-based deserialization is implemented + +// val topLevelInputMembers = getHttpBindingOperations(ctx).flatMap { +// val inputShape = ctx.model.expectShape(it.input.get()) +// inputShape.members() +// } +// .map { ctx.model.expectShape(it.target) } +// .toSet() + + val allTopLevelMembers = + topLevelOutputMembers + .union(topLevelErrorMembers) + .union(topLevelServiceErrorMembers) +// .union(topLevelInputMembers) + + return walkNestedShapesRequiringSchema(ctx, allTopLevelMembers) + } + + private fun walkNestedShapesRequiringSchema(ctx: ProtocolGenerator.GenerationContext, shapes: Set): Set { + val resolved = mutableSetOf() + val walker = Walker(ctx.model) + + // walk all the shapes in the set and find all other + // structs/unions (or collections thereof) in the graph from that shape + shapes.forEach { shape -> + walker.iterateShapes(shape) { relationship -> + when (relationship.relationshipType) { + RelationshipType.MEMBER_TARGET, + RelationshipType.STRUCTURE_MEMBER, + RelationshipType.LIST_MEMBER, + RelationshipType.SET_MEMBER, + RelationshipType.MAP_KEY, + RelationshipType.MAP_VALUE, + RelationshipType.UNION_MEMBER, + -> true + else -> false + } + }.forEach { + // Don't generate schemas for Smithy built-in / "prelude" shapes. + // Those are included in runtime. + if (it.id.namespace != "smithy.api") { resolved.add(it) } + } + } + return resolved + } + // Checks for @requiresLength trait // Returns true if the operation: // - has a streaming member with @httpPayload trait diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt index eecd0c585..0527c8797 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/ProtocolGenerator.kt @@ -117,6 +117,8 @@ interface ProtocolGenerator { */ fun generateCodableConformanceForNestedTypes(ctx: GenerationContext) + fun generateSchemas(ctx: GenerationContext) + /** * * Generate unit tests for the protocol diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorGenerator.kt index bc2edb967..571a64786 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorGenerator.kt @@ -13,6 +13,8 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.integration.HTTPProtocolCustomizable import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.AWSProtocol +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.awsProtocol import software.amazon.smithy.swift.codegen.integration.serde.struct.readerSymbol import software.amazon.smithy.swift.codegen.model.getTrait import software.amazon.smithy.swift.codegen.model.hasTrait @@ -90,6 +92,9 @@ class HTTPResponseBindingErrorGenerator( writer.addImport(SwiftSymbol.make("ClientRuntime", null, SwiftDependency.CLIENT_RUNTIME, emptyList(), listOf("SmithyReadWrite"))) writer.write("let data = try await httpResponse.data()") writer.write("let responseReader = try \$N.from(data: data)", ctx.service.readerSymbol) + if (ctx.service.awsProtocol == AWSProtocol.REST_JSON_1) { + writer.write("responseReader.respectsJSONName = true") + } val noErrorWrapping = ctx.service.getTrait()?.isNoErrorWrapping ?: false if (ctx.service.hasTrait()) { writer.write("let errorDetails = httpResponse.headers.value(for: \"x-amzn-query-error\")") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorInitGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorInitGenerator.kt index 15a1ceba7..62e7fd3d1 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorInitGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingErrorInitGenerator.kt @@ -13,6 +13,8 @@ import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.SectionId import software.amazon.smithy.swift.codegen.integration.httpResponse.bindingTraits.HTTPResponseTraitPayload import software.amazon.smithy.swift.codegen.integration.httpResponse.bindingTraits.HTTPResponseTraitResponseCode +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.AWSProtocol +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.awsProtocol import software.amazon.smithy.swift.codegen.integration.serde.struct.readerSymbol import software.amazon.smithy.swift.codegen.utils.ModelFileUtils @@ -53,6 +55,9 @@ class HTTPResponseBindingErrorInitGenerator( if (needsReader) { writer.addImport(ctx.service.readerSymbol) writer.write("let reader = baseError.errorBodyReader") + if (ctx.service.awsProtocol == AWSProtocol.REST_JSON_1) { + writer.write("reader.respectsJSONName = true") + } } if (needsResponse) { writer.write("let httpResponse = baseError.httpResponse") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt index d680f39b0..c3819eb21 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HTTPResponseBindingOutputGenerator.kt @@ -66,6 +66,9 @@ class HTTPResponseBindingOutputGenerator( writer.write("let data = try await httpResponse.data()") writer.write("let responseReader = try \$N.from(data: data)", ctx.service.readerSymbol) writer.write("let reader = \$L", reader(ctx, op, writer)) + if (ctx.service.awsProtocol == AWSProtocol.REST_JSON_1) { + writer.write("reader.respectsJSONName = true") + } } writer.write("var value = \$N()", outputSymbol) HTTPResponseHeaders(ctx, false, headerBindings, defaultTimestampFormat, writer).render() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeDecodeGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeDecodeGenerator.kt index e0bbde818..8504df524 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeDecodeGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeDecodeGenerator.kt @@ -44,7 +44,10 @@ import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.serde.json.TimestampUtils import software.amazon.smithy.swift.codegen.integration.serde.readwrite.NodeInfoUtils import software.amazon.smithy.swift.codegen.integration.serde.readwrite.ReadingClosureUtils +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.WireProtocol import software.amazon.smithy.swift.codegen.integration.serde.readwrite.responseWireProtocol +import software.amazon.smithy.swift.codegen.integration.serde.schema.readMethodName +import software.amazon.smithy.swift.codegen.integration.serde.schema.schemaVar import software.amazon.smithy.swift.codegen.model.expectTrait import software.amazon.smithy.swift.codegen.model.getTrait import software.amazon.smithy.swift.codegen.model.hasTrait @@ -72,8 +75,10 @@ open class MemberShapeDecodeGenerator( else -> renderMemberExp(member, isPayload) } val memberName = ctx.symbolProvider.toMemberName(member) - if (decodingUnion) { + if (decodingUnion && member.target.toString() != "smithy.api#Unit") { writer.write("return .\$L(\$L)", memberName, readExp) + } else if (decodingUnion) { + writer.write("return .\$L", memberName) } else if (shapeContainingMembers.isError) { writer.write("value.properties.\$L = \$L", memberName, readExp) } else { @@ -82,6 +87,15 @@ open class MemberShapeDecodeGenerator( } private fun renderStructOrUnionExp(memberShape: MemberShape, isPayload: Boolean): String { + if (useSBS) { + val target = ctx.model.expectShape(memberShape.target) + return writer.format( + "try \$L.readStructure\$L(schema: \$L)", + reader(memberShape, isPayload), + "NonNull".takeIf { decodingUnion || (memberShape.hasTrait() || memberShape.hasTrait() || target.hasTrait()) } ?: "", + memberShape.schemaVar(writer), + ) + } val readingClosure = readingClosureUtils.readingClosure(memberShape) return writer.format( "try \$L.\$L(with: \$L)", @@ -92,6 +106,15 @@ open class MemberShapeDecodeGenerator( } private fun renderListExp(memberShape: MemberShape, listShape: ListShape): String { + if (useSBS) { + val target = ctx.model.expectShape(memberShape.target) + return writer.format( + "try \$L.readList\$L(schema: \$L)", + reader(memberShape, false), + "NonNull".takeIf { decodingUnion || (memberShape.hasTrait() || memberShape.hasTrait() || target.hasTrait()) } ?: "", + memberShape.schemaVar(writer), + ) + } val isSparse = listShape.hasTrait() val memberReadingClosure = readingClosureUtils.readingClosure(listShape.member, isSparse) val memberNodeInfo = nodeInfoUtils.nodeInfo(listShape.member) @@ -108,6 +131,15 @@ open class MemberShapeDecodeGenerator( } private fun renderMapExp(memberShape: MemberShape, mapShape: MapShape): String { + if (useSBS) { + val target = ctx.model.expectShape(memberShape.target) + return writer.format( + "try \$L.readMap\$L(schema: \$L)", + reader(memberShape, false), + "NonNull".takeIf { decodingUnion || (memberShape.hasTrait() || memberShape.hasTrait() || target.hasTrait()) } ?: "", + memberShape.schemaVar(writer), + ) + } val isSparse = mapShape.hasTrait() val valueReadingClosure = ReadingClosureUtils(ctx, writer).readingClosure(mapShape.value, isSparse) val keyNodeInfo = nodeInfoUtils.nodeInfo(mapShape.key) @@ -126,6 +158,9 @@ open class MemberShapeDecodeGenerator( } private fun renderTimestampExp(memberShape: MemberShape, timestampShape: TimestampShape): String { + if (useSBS) { + return renderMemberExp(memberShape, isPayload = false) + } val memberTimestampFormatTrait = memberShape.getTrait() val swiftTimestampFormatCase = TimestampUtils.timestampFormat(ctx, memberTimestampFormatTrait, timestampShape) return writer.format( @@ -139,6 +174,16 @@ open class MemberShapeDecodeGenerator( } private fun renderMemberExp(memberShape: MemberShape, isPayload: Boolean): String { + if (useSBS) { + val target = ctx.model.expectShape(memberShape.target) + return writer.format( + "try \$L.\$L\$L(schema: \$L)", + reader(memberShape, isPayload), + target.readMethodName, + "NonNull".takeIf { decodingUnion || (memberShape.hasTrait() || memberShape.hasTrait() || target.hasTrait()) } ?: "", + memberShape.schemaVar(writer), + ) + } return writer.format( "try \$L.\$L()\$L", reader(memberShape, isPayload), @@ -154,7 +199,7 @@ open class MemberShapeDecodeGenerator( private fun reader(memberShape: MemberShape, isPayload: Boolean): String { val nodeInfo = nodeInfoUtils.nodeInfo(memberShape) - return "reader".takeIf { isPayload } ?: writer.format("reader[\$L]", nodeInfo) + return "reader".takeIf { isPayload || useSBS } ?: writer.format("reader[\$L]", nodeInfo) } private fun default(memberShape: MemberShape): String { @@ -297,4 +342,6 @@ open class MemberShapeDecodeGenerator( } private var decodingUnion: Boolean = shapeContainingMembers.isUnionShape + + private val useSBS: Boolean = ctx.service.responseWireProtocol == WireProtocol.JSON } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeEncodeGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeEncodeGenerator.kt index 671ec4572..1fde133fa 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeEncodeGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/member/MemberShapeEncodeGenerator.kt @@ -23,6 +23,7 @@ import software.amazon.smithy.swift.codegen.integration.serde.readwrite.awsProto import software.amazon.smithy.swift.codegen.integration.serde.readwrite.requestWireProtocol import software.amazon.smithy.swift.codegen.model.getTrait import software.amazon.smithy.swift.codegen.model.hasTrait +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyReadWriteTypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTimestampsTypes abstract class MemberShapeEncodeGenerator( @@ -37,6 +38,10 @@ abstract class MemberShapeEncodeGenerator( private val nodeInfoUtils = NodeInfoUtils(ctx, writer, ctx.service.requestWireProtocol) fun writeMember(memberShape: MemberShape, unionMember: Boolean, errorMember: Boolean) { + if (unionMember && memberShape.target.toString() == "smithy.api#Unit") { + writeUnitMember(memberShape) + return + } val prefix1 = "value.".takeIf { !unionMember } ?: "" val prefix2 = "properties.".takeIf { errorMember } ?: "" val prefix = prefix1 + prefix2 @@ -61,6 +66,16 @@ abstract class MemberShapeEncodeGenerator( } } + private fun writeUnitMember(memberShape: MemberShape) { + val propertyKey = nodeInfoUtils.nodeInfo(memberShape) + writer.write( + "try writer[\$L].write(\$N(), with: \$N.write(value:to:))", + propertyKey, + SmithyReadWriteTypes.Unit, + SmithyReadWriteTypes.Unit, + ) + } + private fun writeStructureOrUnionMember(memberShape: MemberShape, prefix: String) { val memberName = ctx.symbolProvider.toMemberName(memberShape) val propertyKey = nodeInfoUtils.nodeInfo(memberShape) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt new file mode 100644 index 000000000..043840ee9 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaGenerator.kt @@ -0,0 +1,220 @@ +package software.amazon.smithy.swift.codegen.integration.serde.schema + +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.NodeType +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeType +import software.amazon.smithy.model.traits.DefaultTrait +import software.amazon.smithy.model.traits.EnumValueTrait +import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.model.traits.HttpPayloadTrait +import software.amazon.smithy.model.traits.JsonNameTrait +import software.amazon.smithy.model.traits.RequiredTrait +import software.amazon.smithy.model.traits.SparseTrait +import software.amazon.smithy.model.traits.StreamingTrait +import software.amazon.smithy.model.traits.TimestampFormatTrait +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.isInHttpBody +import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.model.hasTrait +import software.amazon.smithy.swift.codegen.model.isEnum +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyReadWriteTypes +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTimestampsTypes +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes +import kotlin.jvm.optionals.getOrNull + +class SchemaGenerator( + val ctx: ProtocolGenerator.GenerationContext, + val writer: SwiftWriter, +) { + + fun renderSchema(shape: Shape) { + writer.openBlock( + "var \$L: \$N<\$N> {", + "}", + shape.schemaVar(writer), + SmithyReadWriteTypes.Schema, + ctx.symbolProvider.toSymbol(shape), + ) { + renderSchemaStruct(shape) + } + } + + private fun renderSchemaStruct(shape: Shape) { + val shapeSymbol = ctx.symbolProvider.toSymbol(shape) + writer.openBlock( + "\$N<\$N>(", + ")", + SmithyReadWriteTypes.Schema, + shapeSymbol, + ) { + writer.write("id: \$S,", shape.id.toString()) + writer.write("type: .\$L,", shape.type) + if (shape.type == ShapeType.STRUCTURE) { + writer.write("factory: { .init() },") + } else if (shape.type == ShapeType.UNION) { + writer.write("factory: { .sdkUnknown(\"\") },") + } + if (shape.members().isNotEmpty() && !shape.isEnum && !shape.isIntEnumShape) { + writer.openBlock("members: [", "],") { + shape.members() + .filter { it.isInHttpBody() } + .filter { !ctx.model.expectShape(it.target).hasTrait() } + .forEach { member -> + writer.openBlock(".init(member:", "),") { + writer.openBlock( + "\$N<\$N>.Member<\$N>(", + ")", + SmithyReadWriteTypes.Schema, + shapeSymbol, + ctx.symbolProvider.toSymbol(ctx.model.expectShape(member.target)), + ) { + writer.write("memberSchema: { \$L },", member.schemaVar(writer)) + writeSetterGetter(writer, shape, member) + writer.unwrite(",\n") + writer.write("") + } + } + } + writer.unwrite(",\n") + writer.write("") + } + } + targetShape(shape)?.let { writer.write("targetSchema: { \$L },", it.schemaVar(writer)) } + shape.id.member.getOrNull()?.let { writer.write("memberName: \$S,", it) } + memberShape(shape)?.let { writer.write("containerType: .\$L,", ctx.model.expectShape(it.container).type) } + jsonName(shape)?.let { writer.write("jsonName: \$S,", it) } + if (shape.hasTrait()) { writer.write("httpPayload: true,") } + enumValue(shape)?.let { node -> + when (node.type) { + NodeType.STRING -> writer.write("enumValue: \$N(value: \$S)", SmithyTypes.StringDocument, node) + NodeType.NUMBER -> writer.write("enumValue: \$N(value: \$L)", SmithyTypes.IntegerDocument, node) + else -> throw Exception("Unsupported node type") + } + } + timestampFormat(shape)?.let { + writer.addImport(SmithyTimestampsTypes.TimestampFormat) + writer.write("timestampFormat: .\$L,", it.swiftEnumCase) + } + if (shape.hasTrait()) { + writer.write("isSparse: true,") + } + if (isRequired(shape)) { + writer.write("isRequired: true,") + } + defaultValue(shape)?.let { writer.write("defaultValue: \$L,", it) } + writer.unwrite(",\n") + writer.write("") + } + } + + private fun writeSetterGetter(writer: SwiftWriter, shape: Shape, member: MemberShape) { + val target = ctx.model.expectShape(member.target) + val readMethodName = target.readMethodName + val memberIsRequired = member.isRequired || + (member.hasTrait() || target.hasTrait()) || + (shape.isMapShape && member.memberName == "value" && !shape.hasTrait()) || + (shape.isListShape && !shape.hasTrait()) || + shape.isUnionShape + val readMethodExtension = "NonNull".takeIf { memberIsRequired } ?: "" + when (shape.type) { + ShapeType.STRUCTURE -> { + val path = "properties.".takeIf { shape.hasTrait() } ?: "" + writer.write( + "readBlock: { value, _, reader in value.\$L\$L = try reader.\$L\$L(schema: \$L) },", + path, + ctx.symbolProvider.toMemberName(member), + readMethodName, + readMethodExtension, + member.schemaVar(writer), + ) + } + ShapeType.UNION -> { + if (member.target.toString() != "smithy.api#Unit") { + writer.write( + "readBlock: { value, _, reader in try reader.\$L(schema: \$L).map { value = .\$L(\$\$0) } },", + readMethodName, + member.schemaVar(writer), + ctx.symbolProvider.toMemberName(member), + ) + } else { + writer.write( + "readBlock: { value, _, reader in try reader.\$L(schema: \$L).map { _ in value = .\$L } },", + readMethodName, + member.schemaVar(writer), + ctx.symbolProvider.toMemberName(member), + ) + } + } + ShapeType.SET, ShapeType.LIST -> { + writer.write( + "readBlock: { value, _, reader in try value.append(reader.\$L\$L(schema: \$L)) },", + readMethodName, + readMethodExtension, + member.schemaVar(writer), + ) + } + ShapeType.MAP -> { + if (member.memberName != "key") { + writer.write( + "readBlock: { value, key, reader in try value[key] = reader.\$L\$L(schema: \$L) },", + readMethodName, + readMethodExtension, + member.schemaVar(writer), + ) + } + } + else -> {} + } + } + + private fun targetShape(shape: Shape): Shape? { + return memberShape(shape)?.let { ctx.model.expectShape(it.target) } + } + + private fun memberShape(shape: Shape): MemberShape? { + return shape.asMemberShape().getOrNull() + } + + private fun enumValue(shape: Shape): Node? { + return shape.getTrait()?.toNode() + } + + private fun jsonName(shape: Shape): String? { + return shape.getTrait()?.value + } + + private fun isRequired(shape: Shape): Boolean { + return shape.hasTrait() + } + + private fun defaultValue(shape: Shape): String? { + return shape.getTrait()?.let { + val node = it.toNode() + when (node.type) { + NodeType.STRING -> writer.format("\$N(value: \$S)", SmithyTypes.StringDocument, node.toString()) + NodeType.BOOLEAN -> writer.format("\$N(value: \$L)", SmithyTypes.BooleanDocument, node.toString()) + NodeType.NUMBER -> writer.format("\$N(value: \$L)", SmithyTypes.DoubleDocument, node.toString()) + NodeType.ARRAY -> writer.format("\$N(value: [])", SmithyTypes.ListDocument) + NodeType.OBJECT -> writer.format("\$N(value: [:])", SmithyTypes.StringMapDocument) + NodeType.NULL -> writer.format("\$N()", SmithyTypes.NullDocument) + } + } + } + + private fun timestampFormat(shape: Shape): TimestampFormatTrait.Format? { + return shape.getTrait()?.format + } + + private val TimestampFormatTrait.Format.swiftEnumCase: String + get() { + return when (this) { + TimestampFormatTrait.Format.EPOCH_SECONDS -> "epochSeconds" + TimestampFormatTrait.Format.DATE_TIME -> "dateTime" + TimestampFormatTrait.Format.HTTP_DATE -> "httpDate" + else -> throw Exception("Unknown TimestampFormat") + } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt new file mode 100644 index 000000000..4bb6838bd --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/schema/SchemaShapeUtils.kt @@ -0,0 +1,74 @@ +package software.amazon.smithy.swift.codegen.integration.serde.schema + +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.ShapeType +import software.amazon.smithy.model.traits.EnumTrait +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.model.hasTrait +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyReadWriteTypes +import kotlin.jvm.optionals.getOrNull + +fun Shape.schemaVar(writer: SwiftWriter): String { + return if (this.id.namespace == "smithy.api") { + this.id.preludeSchemaVarName(writer) + } else { + this.id.schemaVarName() + } +} + +private fun ShapeId.preludeSchemaVarName(writer: SwiftWriter): String { + return when (this.name) { + "Unit" -> writer.format("\$N", SmithyReadWriteTypes.unitSchema) + "String" -> writer.format("\$N", SmithyReadWriteTypes.stringSchema) + "Blob" -> writer.format("\$N", SmithyReadWriteTypes.blobSchema) + "Integer" -> writer.format("\$N", SmithyReadWriteTypes.integerSchema) + "Timestamp" -> writer.format("\$N", SmithyReadWriteTypes.timestampSchema) + "Boolean" -> writer.format("\$N", SmithyReadWriteTypes.booleanSchema) + "Float" -> writer.format("\$N", SmithyReadWriteTypes.floatSchema) + "Double" -> writer.format("\$N", SmithyReadWriteTypes.doubleSchema) + "Long" -> writer.format("\$N", SmithyReadWriteTypes.longSchema) + "Short" -> writer.format("\$N", SmithyReadWriteTypes.shortSchema) + "Byte" -> writer.format("\$N", SmithyReadWriteTypes.byteSchema) + "PrimitiveInteger" -> writer.format("\$N", SmithyReadWriteTypes.primitiveIntegerSchema) + "PrimitiveBoolean" -> writer.format("\$N", SmithyReadWriteTypes.primitiveBooleanSchema) + "PrimitiveFloat" -> writer.format("\$N", SmithyReadWriteTypes.primitiveFloatSchema) + "PrimitiveDouble" -> writer.format("\$N", SmithyReadWriteTypes.primitiveDoubleSchema) + "PrimitiveLong" -> writer.format("\$N", SmithyReadWriteTypes.primitiveLongSchema) + "PrimitiveShort" -> writer.format("\$N", SmithyReadWriteTypes.primitiveShortSchema) + "PrimitiveByte" -> writer.format("\$N", SmithyReadWriteTypes.primitiveByteSchema) + "Document" -> writer.format("\$N", SmithyReadWriteTypes.documentSchema) + else -> throw Exception("Unhandled prelude type converted to schemaVar: ${this.name}") + } +} + +private fun ShapeId.schemaVarName(): String { + val namespacePortion = this.namespace.replace(".", "_") + val namePortion = this.name + val memberPortion = this.member.getOrNull()?.let { "__member_$it" } ?: "" + return "schema__namespace_${namespacePortion}__name_${namePortion}$memberPortion" +} + +val Shape.readMethodName: String + get() = when (type) { + ShapeType.BLOB -> "readBlob" + ShapeType.BOOLEAN -> "readBoolean" + ShapeType.STRING -> "readEnum".takeIf { hasTrait() } ?: "readString" + ShapeType.ENUM -> "readEnum" + ShapeType.TIMESTAMP -> "readTimestamp" + ShapeType.BYTE -> "readByte" + ShapeType.SHORT -> "readShort" + ShapeType.INTEGER -> "readInteger" + ShapeType.INT_ENUM -> "readIntEnum" + ShapeType.LONG -> "readLong" + ShapeType.FLOAT -> "readFloat" + ShapeType.DOCUMENT -> "readDocument" + ShapeType.DOUBLE -> "readDouble" + ShapeType.BIG_DECIMAL -> "readBigDecimal" + ShapeType.BIG_INTEGER -> "readBigInteger" + ShapeType.LIST, ShapeType.SET -> "readList" + ShapeType.MAP -> "readMap" + ShapeType.STRUCTURE, ShapeType.UNION -> "readStructure" + ShapeType.MEMBER, ShapeType.SERVICE, ShapeType.RESOURCE, ShapeType.OPERATION, null -> + throw Exception("Unsupported member target type: $type") + } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/union/UnionEncodeGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/union/UnionEncodeGenerator.kt index 186970952..470a5f0ce 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/union/UnionEncodeGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/union/UnionEncodeGenerator.kt @@ -30,10 +30,17 @@ class UnionEncodeGenerator( val membersSortedByName: List = members.sortedBy { it.memberName } membersSortedByName.forEach { member -> val memberName = ctx.symbolProvider.toMemberName(member) - writer.write("case let .\$L(\$L):", memberName, memberName) - writer.indent() - writeMember(member, true, false) - writer.dedent() + if (member.target.toString() != "smithy.api#Unit") { + writer.write("case let .\$L(\$L):", memberName, memberName) + writer.indent() + writeMember(member, true, false) + writer.dedent() + } else { + writer.write("case .\$L:", memberName) + writer.indent() + writeMember(member, true, false) + writer.dedent() + } } writer.write("case let .sdkUnknown(sdkUnknown):") writer.indent() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/EquatableConformanceTransformer.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/EquatableConformanceTransformer.kt index 141db35e2..d37cfdaef 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/EquatableConformanceTransformer.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/EquatableConformanceTransformer.kt @@ -35,7 +35,7 @@ object EquatableConformanceTransformer { // Get all shapes nested within member shapes used as pagination tokens. val shapesToAddEquatableConformanceTraitTo = mutableSetOf() for (tokenMemberShape in paginationTokenMembers) { - val nestedShapes = model.getNestedShapes(tokenMemberShape) + val nestedShapes = model.getNestedShapes(tokenMemberShape).filter { it.id.namespace != "smithy.api" } shapesToAddEquatableConformanceTraitTo.addAll(nestedShapes) } // Add @equatableConformance custom trait to all structure and union shapes diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NeedsReaderWriterTransformer.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NeedsReaderWriterTransformer.kt index aa8f1d8f1..a123cdf80 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NeedsReaderWriterTransformer.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NeedsReaderWriterTransformer.kt @@ -32,7 +32,9 @@ object NeedsReaderWriterTransformer { private fun transform(model: Model, structure: StructureShape, trait: Trait): Model { // Get all shapes nested under the passed trait - var allShapesNeedingTrait = model.getNestedShapes(structure).filter { !it.hasTrait(trait.toShapeId()) } + var allShapesNeedingTrait = model.getNestedShapes(structure) + .filter { it.id.namespace != "smithy.api" } + .filter { !it.hasTrait(trait.toShapeId()) } if (allShapesNeedingTrait.isEmpty()) { return model } // If it's a struct or union, tag it with the trait. Else leave it unchanged. diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NestedShapeTransformer.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NestedShapeTransformer.kt index f79d8470a..1a7f1abd1 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NestedShapeTransformer.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/NestedShapeTransformer.kt @@ -27,7 +27,7 @@ object NestedShapeTransformer { private fun transformInner(model: Model, service: ServiceShape): Model? { // find all the shapes in this models shapes that have are nested shapes (not a top level input or output) - val allShapesNeedingNested = model.getNestedShapes(service).filter { !it.hasTrait() } + val allShapesNeedingNested = model.getNestedShapes(service).filter { !it.hasTrait() && it.id.namespace != "smithy.api" } if (allShapesNeedingNested.isEmpty()) { return null diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/TestEquatableConformanceTransformer.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/TestEquatableConformanceTransformer.kt index 2c387ded0..63063c3b9 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/TestEquatableConformanceTransformer.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/TestEquatableConformanceTransformer.kt @@ -32,7 +32,7 @@ object TestEquatableConformanceTransformer { // Transform the model by adding the TestEquatableConformanceTrait to shapes that need it // In a later SwiftIntegration, the conformance will be code-generated return ModelTransformer.create().mapShapes(model) { shape -> - if (needsTestEquatableConformance.contains(shape.id)) { + if (needsTestEquatableConformance.contains(shape.id) && shape.id.namespace != "smithy.api") { // If the shape is a structure or union, add the TestEquatableConformanceTrait to it // All other shape types don't need to have Equatable generated for them when (shape.type) { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftintegrations/TestEquatableConformanceIntegration.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftintegrations/TestEquatableConformanceIntegration.kt index 29f90adc2..b71fea88f 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftintegrations/TestEquatableConformanceIntegration.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftintegrations/TestEquatableConformanceIntegration.kt @@ -70,9 +70,16 @@ class TestEquatableConformanceIntegration : SwiftIntegration { writer.openBlock("switch (lhs, rhs) {", "}") { shape.members().forEach { member -> val enumCaseName = ctx.symbolProvider.toMemberName(member) - writer.write("case (.\$L(let lhs), .\$L(let rhs)):", enumCaseName, enumCaseName) - writer.indent { - writer.write("return lhs == rhs") + if (member.target.toString() != "smithy.api#Unit") { + writer.write("case (.\$L(let lhs), .\$L(let rhs)):", enumCaseName, enumCaseName) + writer.indent { + writer.write("return lhs == rhs") + } + } else { + writer.write("case (.\$L, .\$L):", enumCaseName, enumCaseName) + writer.indent { + writer.write("return true") + } } } writer.write("default: return false") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyJSONTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyJSONTypes.kt index 3d4a743eb..c94ed2a28 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyJSONTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyJSONTypes.kt @@ -10,7 +10,7 @@ import software.amazon.smithy.swift.codegen.SwiftDependency object SmithyJSONTypes { val Writer = runtimeSymbol("Writer", SwiftDeclaration.CLASS, listOf(SmithyReadWriteTypes.SmithyWriter)) - val Reader = runtimeSymbol("Reader", SwiftDeclaration.CLASS, listOf(SmithyReadWriteTypes.SmithyReader)) + val Reader = runtimeSymbol("Reader", SwiftDeclaration.CLASS, listOf(SmithyReadWriteTypes.SmithyReader, SmithyReadWriteTypes.ShapeDeserializer)) } private fun runtimeSymbol( diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt index 9d09c1822..0aff05036 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyReadWriteTypes.kt @@ -27,6 +27,28 @@ object SmithyReadWriteTypes { val WritingClosures = runtimeSymbol("WritingClosures", SwiftDeclaration.ENUM) val ReadingClosureBox = runtimeSymbol("ReadingClosureBox", SwiftDeclaration.STRUCT) val WritingClosureBox = runtimeSymbol("WritingClosureBox", SwiftDeclaration.STRUCT) + val Schema = runtimeSymbol("Schema", SwiftDeclaration.STRUCT) + val ShapeDeserializer = runtimeSymbol("ShapeDeserializer", SwiftDeclaration.PROTOCOL) + val Unit = runtimeSymbol("Unit", SwiftDeclaration.STRUCT) + val unitSchema = runtimeSymbol("unitSchema", SwiftDeclaration.VAR) + val stringSchema = runtimeSymbol("stringSchema", SwiftDeclaration.VAR) + val blobSchema = runtimeSymbol("blobSchema", SwiftDeclaration.VAR) + val integerSchema = runtimeSymbol("integerSchema", SwiftDeclaration.VAR) + val timestampSchema = runtimeSymbol("timestampSchema", SwiftDeclaration.VAR) + val booleanSchema = runtimeSymbol("booleanSchema", SwiftDeclaration.VAR) + val floatSchema = runtimeSymbol("floatSchema", SwiftDeclaration.VAR) + val doubleSchema = runtimeSymbol("doubleSchema", SwiftDeclaration.VAR) + val longSchema = runtimeSymbol("longSchema", SwiftDeclaration.VAR) + val shortSchema = runtimeSymbol("shortSchema", SwiftDeclaration.VAR) + val byteSchema = runtimeSymbol("byteSchema", SwiftDeclaration.VAR) + val primitiveBooleanSchema = runtimeSymbol("primitiveBooleanSchema", SwiftDeclaration.VAR) + val primitiveFloatSchema = runtimeSymbol("primitiveFloatSchema", SwiftDeclaration.VAR) + val primitiveDoubleSchema = runtimeSymbol("primitiveDoubleSchema", SwiftDeclaration.VAR) + val primitiveLongSchema = runtimeSymbol("primitiveLongSchema", SwiftDeclaration.VAR) + val primitiveIntegerSchema = runtimeSymbol("primitiveIntegerSchema", SwiftDeclaration.VAR) + val primitiveShortSchema = runtimeSymbol("primitiveShortSchema", SwiftDeclaration.VAR) + val primitiveByteSchema = runtimeSymbol("primitiveByteSchema", SwiftDeclaration.VAR) + val documentSchema = runtimeSymbol("documentSchema", SwiftDeclaration.VAR) } private fun runtimeSymbol( diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt index 713ac236d..eae9f4ab5 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyTypes.kt @@ -21,12 +21,32 @@ object SmithyTypes { val LogAgent = runtimeSymbol("LogAgent", SwiftDeclaration.PROTOCOL) val RequestMessageSerializer = runtimeSymbol("RequestMessageSerializer", SwiftDeclaration.PROTOCOL) val URIQueryItem = runtimeSymbol("URIQueryItem", SwiftDeclaration.STRUCT) + val BigDecimalDocument = runtimeSymbol("BigDecimalDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val BigIntegerDocument = runtimeSymbol("BigIntegerDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val BlobDocument = runtimeSymbol("BlobDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val BooleanDocument = runtimeSymbol("BooleanDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val ByteDocument = runtimeSymbol("ByteDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val DoubleDocument = runtimeSymbol("DoubleDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val FloatDocument = runtimeSymbol("FloatDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val IntegerDocument = runtimeSymbol("IntegerDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val ListDocument = runtimeSymbol("ListDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val LongDocument = runtimeSymbol("LongDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val NullDocument = runtimeSymbol("NullDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val ShortDocument = runtimeSymbol("ShortDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val StringDocument = runtimeSymbol("StringDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val StringMapDocument = runtimeSymbol("StringMapDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) + val TimestampDocument = runtimeSymbol("TimestampDocument", SwiftDeclaration.STRUCT, listOf(), listOf("SmithyDocumentImpl")) } -private fun runtimeSymbol(name: String, declaration: SwiftDeclaration? = null): Symbol = SwiftSymbol.make( +private fun runtimeSymbol( + name: String, + declaration: SwiftDeclaration?, + additionalImports: List = emptyList(), + spiName: List = emptyList(), +): Symbol = SwiftSymbol.make( name, declaration, - SwiftDependency.SMITHY, - emptyList(), - emptyList(), + SwiftDependency.SMITHY.takeIf { additionalImports.isEmpty() }, + additionalImports, + spiName, ) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt index 9045179fd..7df4a5ce7 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.swift.codegen.SwiftDeclaration import software.amazon.smithy.swift.codegen.SwiftDependency object SwiftTypes { + val Void = builtInSymbol("Void", SwiftDeclaration.STRUCT) val String = builtInSymbol("String", SwiftDeclaration.STRUCT) val Int = builtInSymbol("Int", SwiftDeclaration.STRUCT) val Int8 = builtInSymbol("Int8", SwiftDeclaration.STRUCT) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt new file mode 100644 index 000000000..378ad5cc5 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/utils/SchemaFileUtils.kt @@ -0,0 +1,16 @@ +package software.amazon.smithy.swift.codegen.utils + +import software.amazon.smithy.swift.codegen.SwiftSettings + +class SchemaFileUtils { + + companion object { + fun filename(settings: SwiftSettings, filename: String): String { + return if (settings.mergeModels) { + "Sources/${settings.moduleName}/Schemas.swift" + } else { + "Sources/${settings.moduleName}/schemas/$filename.swift" + } + } + } +} diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/HttpProtocolUnitTestResponseGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/HttpProtocolUnitTestResponseGeneratorTests.kt index 1706b32c0..8d7370ec6 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/HttpProtocolUnitTestResponseGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/HttpProtocolUnitTestResponseGeneratorTests.kt @@ -167,7 +167,6 @@ open class HttpProtocolUnitTestResponseGeneratorTests { let expected = JsonUnionsOutput( contents: MyUnion.stringvalue("foo") - ) XCTAssertEqual(actual, expected) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructDecodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructDecodeGenerationTests.kt index 39259ad75..77909d2b2 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructDecodeGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructDecodeGenerationTests.kt @@ -59,16 +59,6 @@ extension ExampleClientTypes.Nested4 { try writer["member1"].write(value.member1) try writer["stringMap"].writeMap(value.stringMap, valueWritingClosure: SmithyReadWrite.listWritingClosure(memberWritingClosure: SmithyReadWrite.WritingClosures.writeString(value:to:), memberNodeInfo: "member", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) } - - static func read(from reader: SmithyJSON.Reader) throws -> ExampleClientTypes.Nested4 { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - var value = ExampleClientTypes.Nested4() - value.member1 = try reader["member1"].readIfPresent() - value.intList = try reader["intList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readInt(from:), memberNodeInfo: "member", isFlattened: false) - value.intMap = try reader["intMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.ReadingClosures.readInt(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - value.stringMap = try reader["stringMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - return value - } } """ contents.shouldContainOnlyOnce(expectedContents) @@ -90,15 +80,6 @@ extension ExampleClientTypes.RecursiveShapesInputOutputNested1 { try writer["foo"].write(value.foo) try writer["nested"].write(value.nested, with: ExampleClientTypes.RecursiveShapesInputOutputNested2.write(value:to:)) } - - static func read(from reader: SmithyJSON.Reader) throws -> ExampleClientTypes.RecursiveShapesInputOutputNested1 { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - var value = ExampleClientTypes.RecursiveShapesInputOutputNested1() - value.foo = try reader["foo"].readIfPresent() - value.nested = try reader["nested"].readIfPresent(with: ExampleClientTypes.RecursiveShapesInputOutputNested2.read(from:)) - return value - } -} """ contents.shouldContainOnlyOnce(expectedContents) } @@ -119,14 +100,6 @@ extension ExampleClientTypes.RecursiveShapesInputOutputNested2 { try writer["bar"].write(value.bar) try writer["recursiveMember"].write(value.recursiveMember, with: ExampleClientTypes.RecursiveShapesInputOutputNested1.write(value:to:)) } - - static func read(from reader: SmithyJSON.Reader) throws -> ExampleClientTypes.RecursiveShapesInputOutputNested2 { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - var value = ExampleClientTypes.RecursiveShapesInputOutputNested2() - value.bar = try reader["bar"].readIfPresent() - value.recursiveMember = try reader["recursiveMember"].readIfPresent(with: ExampleClientTypes.RecursiveShapesInputOutputNested1.read(from:)) - return value - } } """ contents.shouldContainOnlyOnce(expectedContents) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructEncodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructEncodeGenerationTests.kt index 165fbcfa1..76cc6a001 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructEncodeGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/StructEncodeGenerationTests.kt @@ -83,10 +83,10 @@ extension Nested4 { static func read(from reader: SmithyJSON.Reader) throws -> Nested4 { guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } var value = Nested4() - value.member1 = try reader["member1"].readIfPresent() - value.intList = try reader["intList"].readListIfPresent(memberReadingClosure: SmithyReadWrite.ReadingClosures.readInt(from:), memberNodeInfo: "member", isFlattened: false) - value.intMap = try reader["intMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.ReadingClosures.readInt(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) - value.stringMap = try reader["stringMap"].readMapIfPresent(valueReadingClosure: SmithyReadWrite.listReadingClosure(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false) + value.member1 = try reader.readInteger(schema: schema__namespace_com_test__name_Nested4__member_member1) + value.intList = try reader.readList(schema: schema__namespace_com_test__name_Nested4__member_intList) + value.intMap = try reader.readMap(schema: schema__namespace_com_test__name_Nested4__member_intMap) + value.stringMap = try reader.readMap(schema: schema__namespace_com_test__name_Nested4__member_stringMap) return value } } @@ -184,8 +184,8 @@ extension RecursiveShapesInputOutputNested1 { static func read(from reader: SmithyJSON.Reader) throws -> RecursiveShapesInputOutputNested1 { guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } var value = RecursiveShapesInputOutputNested1() - value.foo = try reader["foo"].readIfPresent() - value.nested = try reader["nested"].readIfPresent(with: RecursiveShapesInputOutputNested2.read(from:)) + value.foo = try reader.readString(schema: schema__namespace_com_test__name_RecursiveShapesInputOutputNested1__member_foo) + value.nested = try reader.readStructure(schema: schema__namespace_com_test__name_RecursiveShapesInputOutputNested1__member_nested) return value } } @@ -213,8 +213,8 @@ extension RecursiveShapesInputOutputNested2 { static func read(from reader: SmithyJSON.Reader) throws -> RecursiveShapesInputOutputNested2 { guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } var value = RecursiveShapesInputOutputNested2() - value.bar = try reader["bar"].readIfPresent() - value.recursiveMember = try reader["recursiveMember"].readIfPresent(with: RecursiveShapesInputOutputNested1.read(from:)) + value.bar = try reader.readString(schema: schema__namespace_com_test__name_RecursiveShapesInputOutputNested2__member_bar) + value.recursiveMember = try reader.readStructure(schema: schema__namespace_com_test__name_RecursiveShapesInputOutputNested2__member_recursiveMember) return value } } diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionDecodeGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionDecodeGeneratorTests.kt index eb21f6999..f4b93210d 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionDecodeGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionDecodeGeneratorTests.kt @@ -74,35 +74,6 @@ extension ExampleClientTypes.MyUnion { try writer["sdkUnknown"].write(sdkUnknown) } } - - static func read(from reader: SmithyJSON.Reader) throws -> ExampleClientTypes.MyUnion { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - let name = reader.children.filter { ${'$'}0.hasContent && ${'$'}0.nodeInfo.name != "__type" }.first?.nodeInfo.name - switch name { - case "stringValue": - return .stringvalue(try reader["stringValue"].read()) - case "booleanValue": - return .booleanvalue(try reader["booleanValue"].read()) - case "numberValue": - return .numbervalue(try reader["numberValue"].read()) - case "blobValue": - return .blobvalue(try reader["blobValue"].read()) - case "timestampValue": - return .timestampvalue(try reader["timestampValue"].readTimestamp(format: SmithyTimestamps.TimestampFormat.epochSeconds)) - case "inheritedTimestamp": - return .inheritedtimestamp(try reader["inheritedTimestamp"].readTimestamp(format: SmithyTimestamps.TimestampFormat.httpDate)) - case "enumValue": - return .enumvalue(try reader["enumValue"].read()) - case "listValue": - return .listvalue(try reader["listValue"].readList(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false)) - case "mapValue": - return .mapvalue(try reader["mapValue"].readMap(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false)) - case "structureValue": - return .structurevalue(try reader["structureValue"].read(with: ExampleClientTypes.GreetingWithErrorsOutput.read(from:))) - default: - return .sdkUnknown(name ?? "") - } - } } """ contents.shouldContainOnlyOnce(expectedContents) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionEncodeGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionEncodeGeneratorTests.kt index f59b4ee31..0992b8afd 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionEncodeGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/basicshapes/UnionEncodeGeneratorTests.kt @@ -102,35 +102,6 @@ extension ExampleClientTypes.MyUnion { try writer["sdkUnknown"].write(sdkUnknown) } } - - static func read(from reader: SmithyJSON.Reader) throws -> ExampleClientTypes.MyUnion { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - let name = reader.children.filter { ${'$'}0.hasContent && ${'$'}0.nodeInfo.name != "__type" }.first?.nodeInfo.name - switch name { - case "stringValue": - return .stringvalue(try reader["stringValue"].read()) - case "booleanValue": - return .booleanvalue(try reader["booleanValue"].read()) - case "numberValue": - return .numbervalue(try reader["numberValue"].read()) - case "blobValue": - return .blobvalue(try reader["blobValue"].read()) - case "timestampValue": - return .timestampvalue(try reader["timestampValue"].readTimestamp(format: SmithyTimestamps.TimestampFormat.epochSeconds)) - case "inheritedTimestamp": - return .inheritedtimestamp(try reader["inheritedTimestamp"].readTimestamp(format: SmithyTimestamps.TimestampFormat.httpDate)) - case "enumValue": - return .enumvalue(try reader["enumValue"].read()) - case "listValue": - return .listvalue(try reader["listValue"].readList(memberReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), memberNodeInfo: "member", isFlattened: false)) - case "mapValue": - return .mapvalue(try reader["mapValue"].readMap(valueReadingClosure: SmithyReadWrite.ReadingClosures.readString(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false)) - case "structureValue": - return .structurevalue(try reader["structureValue"].read(with: ExampleClientTypes.GreetingWithErrorsOutput.read(from:))) - default: - return .sdkUnknown(name ?? "") - } - } } """ contents.shouldContainOnlyOnce(expectedContents) @@ -154,19 +125,6 @@ extension ExampleClientTypes.IndirectEnum { try writer["sdkUnknown"].write(sdkUnknown) } } - - static func read(from reader: SmithyJSON.Reader) throws -> ExampleClientTypes.IndirectEnum { - guard reader.hasContent else { throw SmithyReadWrite.ReaderError.requiredValueNotPresent } - let name = reader.children.filter { ${'$'}0.hasContent && ${'$'}0.nodeInfo.name != "__type" }.first?.nodeInfo.name - switch name { - case "some": - return .some(try reader["some"].read(with: ExampleClientTypes.IndirectEnum.read(from:))) - case "other": - return .other(try reader["other"].read()) - default: - return .sdkUnknown(name ?? "") - } - } } """ contents.shouldContainOnlyOnce(expectedContents) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/awsjson11/OutputResponseDeserializerTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/awsjson11/OutputResponseDeserializerTests.kt index 7a446c4b1..ff8c6d1fc 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/awsjson11/OutputResponseDeserializerTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/protocolspecificserde/awsjson11/OutputResponseDeserializerTests.kt @@ -50,8 +50,8 @@ extension SimpleStructureOutput { let responseReader = try SmithyJSON.Reader.from(data: data) let reader = responseReader var value = SimpleStructureOutput() - value.name = try reader["name"].readIfPresent() - value.number = try reader["number"].readIfPresent() + value.name = try reader.readString(schema: schema__namespace_smithy_swift_synthetic__name_SimpleStructureOutput__member_name) + value.number = try reader.readInteger(schema: schema__namespace_smithy_swift_synthetic__name_SimpleStructureOutput__member_number) return value } } diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/EventStreamTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/EventStreamTests.kt index 84b22a3f9..b04bf5108 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/EventStreamTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/EventStreamTests.kt @@ -123,12 +123,12 @@ extension EventStreamTestClientTypes.TestStream { return .messagewithstring(event) case "MessageWithStruct": var event = EventStreamTestClientTypes.MessageWithStruct() - let value = try SmithyJSON.Reader.readFrom(message.payload, with: EventStreamTestClientTypes.TestStruct.read(from:)) + let value = try SmithyJSON.Reader.from(data: message.payload).readStructureNonNull(schema: schema__namespace_aws_protocoltests_restjson__name_TestStruct) event.someStruct = value return .messagewithstruct(event) case "MessageWithUnion": var event = EventStreamTestClientTypes.MessageWithUnion() - let value = try SmithyJSON.Reader.readFrom(message.payload, with: EventStreamTestClientTypes.TestUnion.read(from:)) + let value = try SmithyJSON.Reader.from(data: message.payload).readStructureNonNull(schema: schema__namespace_aws_protocoltests_restjson__name_TestUnion) event.someUnion = value return .messagewithunion(event) case "MessageWithHeaders": @@ -166,14 +166,14 @@ extension EventStreamTestClientTypes.TestStream { event.payload = message.payload return .messagewithheaderandpayload(event) case "MessageWithNoHeaderPayloadTraits": - let value = try SmithyJSON.Reader.readFrom(message.payload, with: EventStreamTestClientTypes.MessageWithNoHeaderPayloadTraits.read(from:)) + let value = try SmithyJSON.Reader.from(data: message.payload).readStructureNonNull(schema: schema__namespace_aws_protocoltests_restjson__name_MessageWithNoHeaderPayloadTraits) return .messagewithnoheaderpayloadtraits(value) case "MessageWithUnboundPayloadTraits": var event = EventStreamTestClientTypes.MessageWithUnboundPayloadTraits() if case .string(let value) = message.headers.value(name: "header") { event.header = value } - let value = try SmithyJSON.Reader.readFrom(message.payload, with: SmithyReadWrite.ReadingClosures.readString(from:)) + let value = try SmithyJSON.Reader.from(data: message.payload).readStringNonNull(schema: SmithyReadWrite.stringSchema) event.unboundString = value return .messagewithunboundpayloadtraits(event) default: @@ -183,7 +183,7 @@ extension EventStreamTestClientTypes.TestStream { let makeError: (SmithyEventStreamsAPI.Message, SmithyEventStreamsAPI.MessageType.ExceptionParams) throws -> Swift.Error = { message, params in switch params.exceptionType { case "SomeError": - let value = try SmithyJSON.Reader.readFrom(message.payload, with: SomeError.read(from:)) + let value = try SmithyJSON.Reader.from(data: message.payload).readStructureNonNull(schema: schema__namespace_aws_protocoltests_restjson__name_SomeError) return value default: let httpResponse = SmithyHTTPAPI.HTTPResponse(body: .data(message.payload), statusCode: .ok) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/HTTPBindingProtocolGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/HTTPBindingProtocolGeneratorTests.kt index 954053e0b..b9a0e0cf4 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/HTTPBindingProtocolGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/HTTPBindingProtocolGeneratorTests.kt @@ -73,8 +73,9 @@ extension ExplicitStructOutput { let data = try await httpResponse.data() let responseReader = try SmithyJSON.Reader.from(data: data) let reader = responseReader + reader.respectsJSONName = true var value = ExplicitStructOutput() - value.payload1 = try reader.readIfPresent(with: Nested2.read(from:)) + value.payload1 = try reader.readStructure(schema: schema__namespace_smithy_swift_synthetic__name_ExplicitStructOutput__member_payload1) return value } } diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/responseflow/HttpResponseBindingIgnoreQuery.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/responseflow/HttpResponseBindingIgnoreQuery.kt index a2044b8b0..3086e443f 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/responseflow/HttpResponseBindingIgnoreQuery.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/responseflow/HttpResponseBindingIgnoreQuery.kt @@ -27,8 +27,9 @@ extension IgnoreQueryParamsInResponseOutput { let data = try await httpResponse.data() let responseReader = try SmithyJSON.Reader.from(data: data) let reader = responseReader + reader.respectsJSONName = true var value = IgnoreQueryParamsInResponseOutput() - value.baz = try reader["baz"].readIfPresent() + value.baz = try reader.readString(schema: schema__namespace_smithy_swift_synthetic__name_IgnoreQueryParamsInResponseOutput__member_baz) return value } }