diff --git a/Package.swift b/Package.swift index b0da600c0..0c51c42d1 100644 --- a/Package.swift +++ b/Package.swift @@ -243,7 +243,7 @@ let package = Package( ), .testTarget( name: "SmithyJSONTests", - dependencies: ["SmithyJSON", "ClientRuntime"] + dependencies: ["SmithyJSON", "ClientRuntime", "SmithyTestUtil"] ), .testTarget( name: "SmithyFormURLTests", diff --git a/Sources/Smithy/Document/DocumentError.swift b/Sources/Smithy/Document/DocumentError.swift new file mode 100644 index 000000000..47c16d92d --- /dev/null +++ b/Sources/Smithy/Document/DocumentError.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public enum DocumentError: Error { + case invalidJSONData + case typeMismatch(String) + case numberOverflow(String) + case invalidBase64(String) + case invalidDateFormat(String) +} diff --git a/Sources/Smithy/Document/Implementations/BigDecimalDocument.swift b/Sources/Smithy/Document/Implementations/BigDecimalDocument.swift new file mode 100644 index 000000000..69a8b7803 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/BigDecimalDocument.swift @@ -0,0 +1,63 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct BigDecimalDocument: SmithyDocument { + public var type: ShapeType { .bigDecimal } + let value: Double + + public init(value: Double) { + self.value = value + } + + public func asByte() throws -> Int8 { + guard let byte = Int8(exactly: value) else { + throw DocumentError.numberOverflow("BigDecimal \(value) overflows byte") + } + return byte + } + + public func asShort() throws -> Int16 { + guard let short = Int16(exactly: value) else { + throw DocumentError.numberOverflow("BigDecimal \(value) overflows short") + } + return short + } + + public func asInteger() throws -> Int { + guard let int = Int(exactly: value) else { + throw DocumentError.numberOverflow("BigDecimal \(value) overflows int") + } + return int + } + + public func asLong() throws -> Int64 { + guard let long = Int64(exactly: value) else { + throw DocumentError.numberOverflow("BigDecimal \(value) overflows long") + } + return long + } + + public func asFloat() throws -> Float { + guard let float = Float(exactly: value) else { + throw DocumentError.numberOverflow("BigDecimal \(value) overflows float") + } + return float + } + + public func asDouble() throws -> Double { + value + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + value + } +} diff --git a/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift b/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift new file mode 100644 index 000000000..6c6d80e4c --- /dev/null +++ b/Sources/Smithy/Document/Implementations/BigIntegerDocument.swift @@ -0,0 +1,54 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct BigIntegerDocument: SmithyDocument { + public var type: ShapeType { .bigInteger } + let value: Int + + public init(value: Int) { + self.value = value + } + + public func asByte() throws -> Int8 { + guard let byte = Int8(exactly: value) else { + throw DocumentError.numberOverflow("BigInteger \(value) overflows byte") + } + return byte + } + + public func asShort() throws -> Int16 { + guard let short = Int16(exactly: value) else { + throw DocumentError.numberOverflow("BigInteger \(value) overflows short") + } + return short + } + + public func asInteger() throws -> Int { + value + } + + public func asLong() throws -> Int64 { + Int64(value) + } + + public func asFloat() throws -> Float { + Float(value) + } + + public func asDouble() throws -> Double { + Double(value) + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + Double(value) + } +} diff --git a/Sources/Smithy/Document/Implementations/BlobDocument.swift b/Sources/Smithy/Document/Implementations/BlobDocument.swift new file mode 100644 index 000000000..7397fe925 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/BlobDocument.swift @@ -0,0 +1,22 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data + +@_spi(SmithyDocumentImpl) +public struct BlobDocument: SmithyDocument { + public var type: ShapeType { .blob } + let value: Data + + public init(value: Data) { + self.value = value + } + + public func asBlob() throws -> Data { + value + } +} diff --git a/Sources/Smithy/Document/Implementations/BooleanDocument.swift b/Sources/Smithy/Document/Implementations/BooleanDocument.swift new file mode 100644 index 000000000..8f727842b --- /dev/null +++ b/Sources/Smithy/Document/Implementations/BooleanDocument.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct BooleanDocument: SmithyDocument { + public var type: ShapeType { .boolean } + let value: Bool + + public init(value: Bool) { + self.value = value + } + + public func asBoolean() throws -> Bool { + value + } +} diff --git a/Sources/Smithy/Document/Implementations/ByteDocument.swift b/Sources/Smithy/Document/Implementations/ByteDocument.swift new file mode 100644 index 000000000..1d9e97008 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/ByteDocument.swift @@ -0,0 +1,48 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct ByteDocument: SmithyDocument { + public var type: ShapeType { .byte } + let value: Int8 + + public init(value: Int8) { + self.value = value + } + + public func asByte() throws -> Int8 { + value + } + + public func asShort() throws -> Int16 { + Int16(value) + } + + public func asInteger() throws -> Int { + Int(value) + } + + public func asLong() throws -> Int64 { + Int64(value) + } + + public func asFloat() throws -> Float { + Float(value) + } + + public func asDouble() throws -> Double { + Double(value) + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + Double(value) + } +} diff --git a/Sources/Smithy/Document/Implementations/Document.swift b/Sources/Smithy/Document/Implementations/Document.swift new file mode 100644 index 000000000..9e4b16d0d --- /dev/null +++ b/Sources/Smithy/Document/Implementations/Document.swift @@ -0,0 +1,101 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date + +/// A type-erased container for some value conforming to the `SmithyDocument` protocol. +public struct Document: SmithyDocument { + let document: SmithyDocument + + public init(_ document: SmithyDocument) { + self.document = document + } +} + +// Since protocol-bound values cannot conform to Self-referencing +// protocols like `Equatable`, the `Document` container provides the +// `Equatable` conformance. +extension Document: Equatable { + + public static func ==(_ lhs: Document, _ rhs: Document) -> Bool { + isEqual(lhs.document, rhs.document) + } +} + +// All of these implementations of `SmithyDocument` methods simply delegate +// to the inner document. +public extension Document { + + var type: Smithy.ShapeType { + document.type + } + + func asBoolean() throws -> Bool { + try document.asBoolean() + } + + func asString() throws -> String { + try document.asString() + } + + func asList() throws -> [SmithyDocument] { + try document.asList() + } + + func asStringMap() throws -> [String: SmithyDocument] { + try document.asStringMap() + } + + func size() -> Int { + document.size() + } + + func asByte() throws -> Int8 { + try document.asByte() + } + + func asShort() throws -> Int16 { + try document.asShort() + } + + func asInteger() throws -> Int { + try document.asInteger() + } + + func asLong() throws -> Int64 { + try document.asLong() + } + + func asFloat() throws -> Float { + try document.asFloat() + } + + func asDouble() throws -> Double { + try document.asDouble() + } + + func asBigInteger() throws -> Int64 { + try document.asBigInteger() + } + + func asBigDecimal() throws -> Double { + try document.asBigDecimal() + } + + func asBlob() throws -> Data { + try document.asBlob() + } + + func asTimestamp() throws -> Date { + try document.asTimestamp() + } + + func getMember(_ memberName: String) throws -> SmithyDocument? { + try document.getMember(memberName) + } +} diff --git a/Sources/Smithy/Document/Implementations/DoubleDocument.swift b/Sources/Smithy/Document/Implementations/DoubleDocument.swift new file mode 100644 index 000000000..201065a4e --- /dev/null +++ b/Sources/Smithy/Document/Implementations/DoubleDocument.swift @@ -0,0 +1,63 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct DoubleDocument: SmithyDocument { + public var type: ShapeType { .double } + let value: Double + + public init(value: Double) { + self.value = value + } + + public func asByte() throws -> Int8 { + guard let byte = Int8(exactly: value) else { + throw DocumentError.numberOverflow("Double \(value) overflows byte") + } + return byte + } + + public func asShort() throws -> Int16 { + guard let short = Int16(exactly: value) else { + throw DocumentError.numberOverflow("Double \(value) overflows short") + } + return short + } + + public func asInteger() throws -> Int { + guard let int = Int(exactly: value) else { + throw DocumentError.numberOverflow("Double \(value) overflows int") + } + return int + } + + public func asLong() throws -> Int64 { + guard let long = Int64(exactly: value) else { + throw DocumentError.numberOverflow("Double \(value) overflows long") + } + return long + } + + public func asFloat() throws -> Float { + guard let float = Float(exactly: value) else { + throw DocumentError.numberOverflow("Double \(value) overflows float") + } + return float + } + + public func asDouble() throws -> Double { + value + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + value + } +} diff --git a/Sources/Smithy/Document/Implementations/FloatDocument.swift b/Sources/Smithy/Document/Implementations/FloatDocument.swift new file mode 100644 index 000000000..0ef7d6a20 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/FloatDocument.swift @@ -0,0 +1,60 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct FloatDocument: SmithyDocument { + public var type: ShapeType { .float } + let value: Float + + public init(value: Float) { + self.value = value + } + + public func asByte() throws -> Int8 { + guard let byte = Int8(exactly: value) else { + throw DocumentError.numberOverflow("Float \(value) overflows byte") + } + return byte + } + + public func asShort() throws -> Int16 { + guard let short = Int16(exactly: value) else { + throw DocumentError.numberOverflow("Float \(value) overflows short") + } + return short + } + + public func asInteger() throws -> Int { + guard let int = Int(exactly: value) else { + throw DocumentError.numberOverflow("Float \(value) overflows int") + } + return int + } + + public func asLong() throws -> Int64 { + guard let long = Int64(exactly: value) else { + throw DocumentError.numberOverflow("Float \(value) overflows long") + } + return long + } + + public func asFloat() throws -> Float { + value + } + + public func asDouble() throws -> Double { + Double(value) + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + Double(value) + } +} diff --git a/Sources/Smithy/Document/Implementations/IntegerDocument.swift b/Sources/Smithy/Document/Implementations/IntegerDocument.swift new file mode 100644 index 000000000..8175ae3fa --- /dev/null +++ b/Sources/Smithy/Document/Implementations/IntegerDocument.swift @@ -0,0 +1,54 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct IntegerDocument: SmithyDocument { + public var type: ShapeType { .integer } + let value: Int + + public init(value: Int) { + self.value = value + } + + public func asByte() throws -> Int8 { + guard let byte = Int8(exactly: value) else { + throw DocumentError.numberOverflow("Int \(value) overflows byte") + } + return byte + } + + public func asShort() throws -> Int16 { + guard let short = Int16(exactly: value) else { + throw DocumentError.numberOverflow("Int \(value) overflows short") + } + return short + } + + public func asInteger() throws -> Int { + value + } + + public func asLong() throws -> Int64 { + Int64(value) + } + + public func asFloat() throws -> Float { + Float(value) + } + + public func asDouble() throws -> Double { + Double(value) + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + Double(value) + } +} diff --git a/Sources/Smithy/Document/Implementations/ListDocument.swift b/Sources/Smithy/Document/Implementations/ListDocument.swift new file mode 100644 index 000000000..d9dc6d987 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/ListDocument.swift @@ -0,0 +1,24 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct ListDocument: SmithyDocument { + public var type: ShapeType { .list } + let value: [SmithyDocument] + + public init(value: [SmithyDocument]) { + self.value = value + } + + public func asList() throws -> [SmithyDocument] { + return value + } + + public func size() -> Int { + value.count + } +} diff --git a/Sources/Smithy/Document/Implementations/LongDocument.swift b/Sources/Smithy/Document/Implementations/LongDocument.swift new file mode 100644 index 000000000..71362f526 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/LongDocument.swift @@ -0,0 +1,57 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct LongDocument: SmithyDocument { + public var type: ShapeType { .long } + let value: Int64 + + public init(value: Int64) { + self.value = value + } + + public func asByte() throws -> Int8 { + guard let byte = Int8(exactly: value) else { + throw DocumentError.numberOverflow("Long \(value) overflows byte") + } + return byte + } + + public func asShort() throws -> Int16 { + guard let short = Int16(exactly: value) else { + throw DocumentError.numberOverflow("Long \(value) overflows short") + } + return short + } + + public func asInteger() throws -> Int { + guard let int = Int(exactly: value) else { + throw DocumentError.numberOverflow("Long \(value) overflows int") + } + return int + } + + public func asLong() throws -> Int64 { + value + } + + public func asFloat() throws -> Float { + Float(value) + } + + public func asDouble() throws -> Double { + Double(value) + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + Double(value) + } +} diff --git a/Sources/Smithy/Document/Implementations/NullDocument.swift b/Sources/Smithy/Document/Implementations/NullDocument.swift new file mode 100644 index 000000000..b70b6abc1 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/NullDocument.swift @@ -0,0 +1,13 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct NullDocument: SmithyDocument { + public var type: ShapeType { .structure } // think of this as a Unit structure + + public init() {} +} diff --git a/Sources/Smithy/Document/Implementations/ShortDocument.swift b/Sources/Smithy/Document/Implementations/ShortDocument.swift new file mode 100644 index 000000000..2da3d079e --- /dev/null +++ b/Sources/Smithy/Document/Implementations/ShortDocument.swift @@ -0,0 +1,51 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct ShortDocument: SmithyDocument { + public var type: ShapeType { .short } + let value: Int16 + + public init(value: Int16) { + self.value = value + } + + public func asByte() throws -> Int8 { + guard let byte = Int8(exactly: value) else { + throw DocumentError.numberOverflow("Short \(value) overflows byte") + } + return byte + } + + public func asShort() throws -> Int16 { + value + } + + public func asInteger() throws -> Int { + Int(value) + } + + public func asLong() throws -> Int64 { + Int64(value) + } + + public func asFloat() throws -> Float { + Float(value) + } + + public func asDouble() throws -> Double { + Double(value) + } + + public func asBigInteger() throws -> Int64 { + Int64(value) + } + + public func asBigDecimal() throws -> Double { + Double(value) + } +} diff --git a/Sources/Smithy/Document/Implementations/StringDocument.swift b/Sources/Smithy/Document/Implementations/StringDocument.swift new file mode 100644 index 000000000..8f14afbd9 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/StringDocument.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct StringDocument: SmithyDocument { + public var type: ShapeType { .string } + let value: String + + public init(value: String) { + self.value = value + } + + public func asString() throws -> String { + value + } +} diff --git a/Sources/Smithy/Document/Implementations/StringMapDocument.swift b/Sources/Smithy/Document/Implementations/StringMapDocument.swift new file mode 100644 index 000000000..3501a27fd --- /dev/null +++ b/Sources/Smithy/Document/Implementations/StringMapDocument.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) +public struct StringMapDocument: SmithyDocument { + public var type: ShapeType { .map } + let value: [String: SmithyDocument] + + public init(value: [String: SmithyDocument]) { + self.value = value + } + + public func asStringMap() throws -> [String: SmithyDocument] { + return value + } + + public func size() -> Int { + value.count + } + + public func getMember(_ memberName: String) throws -> SmithyDocument? { + value[memberName] + } + + public static func ==(_ lhs: StringMapDocument, _ rhs: StringMapDocument) -> Bool { + false + } +} diff --git a/Sources/Smithy/Document/Implementations/TimestampDocument.swift b/Sources/Smithy/Document/Implementations/TimestampDocument.swift new file mode 100644 index 000000000..bbbf26352 --- /dev/null +++ b/Sources/Smithy/Document/Implementations/TimestampDocument.swift @@ -0,0 +1,22 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Date + +@_spi(SmithyDocumentImpl) +public struct TimestampDocument: SmithyDocument { + public var type: ShapeType { .timestamp } + let value: Date + + public init(value: Date) { + self.value = value + } + + public func asTimestamp() throws -> Date { + value + } +} diff --git a/Sources/Smithy/Document/ShapeType.swift b/Sources/Smithy/Document/ShapeType.swift new file mode 100644 index 000000000..5b3de70a1 --- /dev/null +++ b/Sources/Smithy/Document/ShapeType.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Reproduces the cases in Smithy `ShapeType`. +/// https://github.com/smithy-lang/smithy/blob/main/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +public enum ShapeType { + case blob + case boolean + case string + case timestamp + case byte + case short + case integer + case long + case float + case document + case double + case bigDecimal + case bigInteger + case `enum` + case intEnum + case list + case set + case map + case structure + case union + case member + case service + case resource + case operation +} diff --git a/Sources/Smithy/Document/SmithyDocument.swift b/Sources/Smithy/Document/SmithyDocument.swift new file mode 100644 index 000000000..c5e295017 --- /dev/null +++ b/Sources/Smithy/Document/SmithyDocument.swift @@ -0,0 +1,162 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.Date + +public protocol SmithyDocument { + + var type: ShapeType { get } + + // "as" methods throw if the document doesn't match the requested type. + func asBoolean() throws -> Bool + func asString() throws -> String + func asList() throws -> [SmithyDocument] + func asStringMap() throws -> [String: SmithyDocument] + + // Get the number of list elements or map entries. Returns -1 if the + // document is not a list or map. + func size() -> Int + + // Throw if not numeric. Numeric accessors should automatically coerce a value + // to the requested numeric type, but throw if the value would overflow. + func asByte() throws -> Int8 + func asShort() throws -> Int16 + func asInteger() throws -> Int + func asLong() throws -> Int64 + func asFloat() throws -> Float + func asDouble() throws -> Double + func asBigInteger() throws -> Int64 + func asBigDecimal() throws -> Double + + // Protocols that don't support blob serialization like JSON should + // automatically attempt to base64 decode a string and return it as a blob. + func asBlob() throws -> Data + + // Protocols like JSON that don't support timestamps should automatically + // convert values based on the timestamp format of the shape or the codec. + func asTimestamp() throws -> Date + + // Get a member by name, taking protocol details like jsonName into account. + func getMember(_ memberName: String) throws -> SmithyDocument? +} + +// Default implementations for a Document that either throw or (for size()) +// return a default value. +public extension SmithyDocument { + + func asBoolean() throws -> Bool { + throw DocumentError.typeMismatch("Expected boolean, got \(self)") + } + + func asString() throws -> String { + throw DocumentError.typeMismatch("Expected string, got \(self)") + } + + func asList() throws -> [SmithyDocument] { + throw DocumentError.typeMismatch("Expected list, got \(self)") + } + + func asStringMap() throws -> [String: SmithyDocument] { + throw DocumentError.typeMismatch("Expected map, got \(self)") + } + + func size() -> Int { + -1 + } + + func asByte() throws -> Int8 { + throw DocumentError.typeMismatch("Expected byte, got \(self)") + } + + func asShort() throws -> Int16 { + throw DocumentError.typeMismatch("Expected short, got \(self)") + } + + func asInteger() throws -> Int { + throw DocumentError.typeMismatch("Expected int, got \(self)") + } + + func asLong() throws -> Int64 { + throw DocumentError.typeMismatch("Expected long, got \(self)") + } + + func asFloat() throws -> Float { + throw DocumentError.typeMismatch("Expected float, got \(self)") + } + + func asDouble() throws -> Double { + throw DocumentError.typeMismatch("Expected double, got \(self)") + } + + func asBigInteger() throws -> Int64 { + throw DocumentError.typeMismatch("Expected BigInteger, got \(self)") + } + + func asBigDecimal() throws -> Double { + throw DocumentError.typeMismatch("Expected BigDecimal, got \(self)") + } + + func asBlob() throws -> Data { + throw DocumentError.typeMismatch("Expected blob, got \(self)") + } + + func asTimestamp() throws -> Date { + throw DocumentError.typeMismatch("Expected timestamp, got \(self)") + } + + func getMember(_ memberName: String) throws -> SmithyDocument? { + throw DocumentError.typeMismatch("Expected a map, structure, or union document, got \(self)") + } +} + +extension SmithyDocument { + + public static func isEqual(_ lhs: SmithyDocument, _ rhs: SmithyDocument) -> Bool { + switch (lhs.type, rhs.type) { + case (.blob, .blob): + return (try? lhs.asBlob() == rhs.asBlob()) ?? false + case (.boolean, .boolean): + return (try? lhs.asBoolean() == rhs.asBoolean()) ?? false + case (.string, .string): + return (try? lhs.asString() == rhs.asString()) ?? false + case (.timestamp, .timestamp): + return (try? lhs.asTimestamp() == rhs.asTimestamp()) ?? false + case (.byte, .byte): + return (try? lhs.asByte() == rhs.asByte()) ?? false + case (.short, .short): + return (try? lhs.asShort() == rhs.asShort()) ?? false + case (.integer, .integer): + return (try? lhs.asInteger() == rhs.asInteger()) ?? false + case (.long, .long): + return (try? lhs.asLong() == rhs.asLong()) ?? false + case (.float, .float): + return (try? lhs.asFloat() == rhs.asFloat()) ?? false + case (.double, .double): + return (try? lhs.asDouble() == rhs.asDouble()) ?? false + case (.bigDecimal, .bigDecimal): + return (try? lhs.asBigDecimal() == rhs.asBigDecimal()) ?? false + case (.bigInteger, .bigInteger): + return (try? lhs.asBigInteger() == rhs.asBigInteger()) ?? false + case (.list, .list): + guard let lhsList = try? lhs.asList(), let rhsList = try? rhs.asList() else { return false } + guard lhsList.count == rhsList.count else { return false } + return zip(lhsList, rhsList).allSatisfy { isEqual($0.0, $0.1) } + case (.map, .map): + guard let lhsMap = try? lhs.asStringMap(), let rhsMap = try? rhs.asStringMap() else { return false } + guard lhsMap.count == rhsMap.count else { return false } + return lhsMap.keys.allSatisfy { key in + guard let leftValue = lhsMap[key], let rightValue = rhsMap[key] else { return false } + return isEqual(leftValue, rightValue) + } + case (.structure, .structure): + return true // structure type is currently only used for null values + default: + return false + } + } +} diff --git a/Sources/SmithyFormURL/Writer.swift b/Sources/SmithyFormURL/Writer.swift index abd40c312..2d1f76e9b 100644 --- a/Sources/SmithyFormURL/Writer.swift +++ b/Sources/SmithyFormURL/Writer.swift @@ -6,7 +6,7 @@ // @_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyWriter -import enum SmithyReadWrite.Document +import protocol Smithy.SmithyDocument @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat @_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter import struct Foundation.Data @@ -127,7 +127,7 @@ public extension Writer { try write(value?.base64EncodedString()) } - func write(_ value: Document?) throws { + func write(_ value: SmithyDocument?) throws { // No operation. Smithy document not supported in FormURL } diff --git a/Sources/SmithyHTTPClient/SdkHttpRequestBuilder+HTTPRequestBase.swift b/Sources/SmithyHTTPClient/SdkHttpRequestBuilder+HTTPRequestBase.swift index f9d611243..75cf6fba1 100644 --- a/Sources/SmithyHTTPClient/SdkHttpRequestBuilder+HTTPRequestBase.swift +++ b/Sources/SmithyHTTPClient/SdkHttpRequestBuilder+HTTPRequestBase.swift @@ -29,7 +29,7 @@ extension HTTPRequestBuilder { replacingQueryItems(components.percentEncodedQueryItems?.map { URIQueryItem(name: $0.name, value: $0.value) } ?? [URIQueryItem]()) - } else if crtRequest as? HTTP2Request != nil { + } else if crtRequest is HTTP2Request { assertionFailure("HTTP2Request not supported") } else { assertionFailure("Unknown request type") diff --git a/Sources/SmithyJSON/Document+Init.swift b/Sources/SmithyJSON/Document+Init.swift new file mode 100644 index 000000000..f67483f99 --- /dev/null +++ b/Sources/SmithyJSON/Document+Init.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@_spi(SmithyDocumentImpl) import Smithy + +extension Document: ExpressibleByArrayLiteral { + + public init(arrayLiteral value: Document...) { + self.init(ListDocument(value: value)) + } +} + +extension Document: ExpressibleByBooleanLiteral { + + public init(booleanLiteral value: Bool) { + self.init(BooleanDocument(value: value)) + } +} + +extension Document: ExpressibleByDictionaryLiteral { + + public init(dictionaryLiteral elements: (String, Document)...) { + let value = elements.reduce([String: Document]()) { acc, curr in + var newValue = acc + newValue[curr.0] = curr.1 + return newValue + } + self.init(StringMapDocument(value: value)) + } +} + +extension Document: ExpressibleByFloatLiteral { + + public init(floatLiteral value: Float) { + self.init(FloatDocument(value: value)) + } +} + +extension Document: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: Int) { + self.init(IntegerDocument(value: value)) + } +} + +extension Document: ExpressibleByNilLiteral { + + public init(nilLiteral: ()) { + self.init(NullDocument()) + } +} + +extension Document: ExpressibleByStringLiteral { + + public init(stringLiteral value: String) { + self.init(StringDocument(value: value)) + } +} diff --git a/Sources/SmithyJSON/Document+JSONUtils.swift b/Sources/SmithyJSON/Document+JSONUtils.swift new file mode 100644 index 000000000..dd0386229 --- /dev/null +++ b/Sources/SmithyJSON/Document+JSONUtils.swift @@ -0,0 +1,58 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import func CoreFoundation.CFGetTypeID +import func CoreFoundation.CFBooleanGetTypeID +import struct Foundation.Data +import struct Foundation.Date +import class Foundation.DateFormatter +import class Foundation.JSONSerialization +import struct Foundation.Locale +import class Foundation.NSNull +import class Foundation.NSNumber +@_spi(SmithyDocumentImpl) import Smithy + +public extension Document { + + static func make(from data: Data) throws -> Document { + let jsonObject = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) + return try Self.make(from: jsonObject) + } + + /// Creates a Smithy `Document` from a Swift JSON object. + /// + /// The JSON object should obey the following: + /// - The top level object is an NSArray or NSDictionary. + /// - All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull. + /// - All dictionary keys are instances of NSString. + /// - Numbers are neither NaN nor infinity. + /// The JSON object + /// - Parameter jsonObject: The JSON object + /// - Returns: A Smithy `Document` containing the JSON. + static func make(from jsonObject: Any) throws -> Document { + let doc: SmithyDocument + if let object = jsonObject as? [String: Any] { + doc = StringMapDocument(value: try object.mapValues { try Self.make(from: $0) }) + } else if let array = jsonObject as? [Any] { + doc = ListDocument(value: try array.map { try Self.make(from: $0) }) + } else if let nsNumber = jsonObject as? NSNumber { + // Check if the NSNumber is a boolean, else treat it as double + if CFGetTypeID(nsNumber) == CFBooleanGetTypeID() { + doc = BooleanDocument(value: nsNumber.boolValue) + } else { + doc = DoubleDocument(value: nsNumber.doubleValue) + } + } else if let string = jsonObject as? String { + doc = StringDocument(value: string) + } else if jsonObject is NSNull { + doc = NullDocument() + } else { + throw DocumentError.invalidJSONData + } + return Document(doc) + } +} diff --git a/Sources/SmithyJSON/Reader/Reader.swift b/Sources/SmithyJSON/Reader/Reader.swift index 1138dc884..f70752696 100644 --- a/Sources/SmithyJSON/Reader/Reader.swift +++ b/Sources/SmithyJSON/Reader/Reader.swift @@ -6,7 +6,8 @@ // @_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyReader -import enum SmithyReadWrite.Document +import protocol Smithy.SmithyDocument +import struct Smithy.Document import typealias SmithyReadWrite.ReadingClosure import enum SmithyReadWrite.ReaderError @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat diff --git a/Sources/SmithyJSON/Writer/Writer.swift b/Sources/SmithyJSON/Writer/Writer.swift index 689362e11..b76f7c4f4 100644 --- a/Sources/SmithyJSON/Writer/Writer.swift +++ b/Sources/SmithyJSON/Writer/Writer.swift @@ -6,7 +6,8 @@ // @_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyWriter -import enum SmithyReadWrite.Document +@_spi(SmithyDocumentImpl) import protocol Smithy.SmithyDocument +import enum Smithy.DocumentError @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat @_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter import struct Foundation.Data @@ -85,27 +86,66 @@ public extension Writer { self.jsonNode = .number(NSNumber(value: value)) } + func write(_ value: Int64?) throws { + guard let value else { return } + self.jsonNode = .number(NSNumber(value: value)) + } + func write(_ value: Data?) throws { try write(value?.base64EncodedString()) } - func write(_ value: Document?) throws { + func write(_ value: SmithyDocument?) throws { guard let value else { return } - switch value { - case .object(let object): - try object.forEach { try self[.init($0.key)].write($0.value) } + switch value.type { + case .map: + let map = try value.asStringMap() + try map.forEach { try self[.init($0.key)].write($0.value) } self.jsonNode = .object - case .array(let array): - try array.enumerated().forEach { try self[.init("\($0.offset)")].write($0.element) } + case .list: + let list = try value.asList() + try list.enumerated().forEach { try self[.init("\($0.offset)")].write($0.element) } self.jsonNode = .array - case .boolean(let bool): + case .boolean: + let bool = try value.asBoolean() try write(bool) - case .number(let number): - try write(number) - case .string(let string): + case .double: + let double = try value.asDouble() + try write(double) + case .integer: + let int = try value.asInteger() + try write(int) + case .float: + let float = try value.asFloat() + try write(float) + case .long: + let long = try value.asLong() + try write(long) + case .short: + let short = try value.asShort() + try write(short) + case .byte: + let byte = try value.asByte() + try write(byte) + case .bigInteger: + let bigInteger = try value.asBigInteger() + try write(bigInteger) + case .bigDecimal: + let bigDecimal = try value.asBigDecimal() + try write(bigDecimal) + case .string: + let string = try value.asString() try write(string) - case .null: + case .blob: + let data = try value.asBlob() + try write(data) + case .timestamp: + let date = try value.asTimestamp() + try writeTimestamp(date, format: .dateTime) + case .structure: self.jsonNode = .null + default: + throw DocumentError.invalidJSONData } } diff --git a/Sources/SmithyReadWrite/Document.swift b/Sources/SmithyReadWrite/Document.swift deleted file mode 100644 index 57672eac9..000000000 --- a/Sources/SmithyReadWrite/Document.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import struct Foundation.Data -import class Foundation.JSONSerialization -import class Foundation.NSNull -import class Foundation.NSNumber -import func CoreFoundation.CFGetTypeID -import func CoreFoundation.CFBooleanGetTypeID - -public enum Document { - case array([Document]) - case boolean(Bool) - case number(Double) - case object([String: Document]) - case string(String) - case null -} - -extension Document: Equatable { } - -extension Document: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: Document...) { - self = .array(elements) - } -} - -extension Document: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: Bool) { - self = .boolean(value) - } -} - -extension Document: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, Document)...) { - let dictionary = elements.reduce([String: Document]()) { acc, curr in - var newValue = acc - newValue[curr.0] = curr.1 - return newValue - } - self = .object(dictionary) - } -} - -extension Document: ExpressibleByFloatLiteral { - public init(floatLiteral value: Double) { - self = .number(value) - } -} - -extension Document: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { - self = .number(Double(value)) - } -} - -extension Document: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { - self = .null - } -} - -extension Document: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self = .string(value) - } -} - -// extension to use subscribts to get the values from objects/arrays as normal -public extension Document { - - subscript(_ key: String) -> Document? { - guard case .object(let object) = self else { - return nil - } - return object[key] - } - - subscript(_ key: Int) -> Document? { - switch self { - case .array(let array): - return array[key] - case .object(let object): - return object["\(key)"] - default: - return nil - } - } -} - -extension Document { - - private var jsonObject: Any { - switch self { - case .array(let array): - return array.map { $0.jsonObject } - case .boolean(let bool): - return bool - case .number(let double): - return double - case .object(let object): - return object.mapValues { $0.jsonObject } - case .string(let string): - return string - case .null: - return NSNull() - } - } - - public static func make(from jsonObject: Any) throws -> Document { - if let object = jsonObject as? [String: Any] { - return .object(try object.mapValues { try Document.make(from: $0) }) - } else if let array = jsonObject as? [Any] { - return .array(try array.map { try Document.make(from: $0) }) - } else if let nsNumber = jsonObject as? NSNumber, CFGetTypeID(nsNumber) == CFBooleanGetTypeID() { - return .boolean(nsNumber.boolValue) - } else if let nsNumber = jsonObject as? NSNumber { - return .number(nsNumber.doubleValue) - } else if let string = jsonObject as? String { - return .string(string) - } else if jsonObject is NSNull { - return .null - } else { - throw SmithyDocumentError.invalidJSONData - } - } - - public static func make(from data: Data) throws -> Document { - let jsonObject = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) - return try Document.make(from: jsonObject) - } -} - -enum SmithyDocumentError: Error { - case invalidJSONData -} diff --git a/Sources/SmithyReadWrite/ReadingClosure.swift b/Sources/SmithyReadWrite/ReadingClosure.swift index 44e073ed3..f09498a75 100644 --- a/Sources/SmithyReadWrite/ReadingClosure.swift +++ b/Sources/SmithyReadWrite/ReadingClosure.swift @@ -7,6 +7,8 @@ import struct Foundation.Data import struct Foundation.Date +import struct Smithy.Document +import protocol Smithy.SmithyDocument @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat @_spi(SmithyReadWrite) @@ -180,11 +182,11 @@ public enum ReadingClosures { } public static func readDocument(from reader: Reader) throws -> Document { - try reader.read() + Document(try reader.read()) } public static func readDocument(from reader: Reader) throws -> Document? { - try reader.readIfPresent() + (try reader.readIfPresent()).map { Document($0) } } } diff --git a/Sources/SmithyReadWrite/SmithyReader.swift b/Sources/SmithyReadWrite/SmithyReader.swift index 657550040..7904a12c1 100644 --- a/Sources/SmithyReadWrite/SmithyReader.swift +++ b/Sources/SmithyReadWrite/SmithyReader.swift @@ -8,6 +8,7 @@ import struct Foundation.Data import struct Foundation.Date @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat +import struct Smithy.Document @_spi(SmithyReadWrite) public protocol SmithyReader: AnyObject { diff --git a/Sources/SmithyReadWrite/SmithyWriter.swift b/Sources/SmithyReadWrite/SmithyWriter.swift index a44ad64ce..62f992122 100644 --- a/Sources/SmithyReadWrite/SmithyWriter.swift +++ b/Sources/SmithyReadWrite/SmithyWriter.swift @@ -9,6 +9,7 @@ import struct Foundation.Data import struct Foundation.Date @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat import enum Smithy.ByteStream +import protocol Smithy.SmithyDocument @_spi(SmithyReadWrite) public protocol SmithyWriter: AnyObject { @@ -27,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: Document?) throws + func write(_ value: 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 be81bd89c..fcc3fe20a 100644 --- a/Sources/SmithyReadWrite/WritingClosure.swift +++ b/Sources/SmithyReadWrite/WritingClosure.swift @@ -8,6 +8,7 @@ import struct Foundation.Data import struct Foundation.Date @_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat +import protocol Smithy.SmithyDocument @_spi(SmithyReadWrite) public typealias WritingClosure = (T, Writer) throws -> Void @@ -102,7 +103,7 @@ public enum WritingClosures { try writer.write(value) } - public static func writeDocument(value: Document?, to writer: Writer) throws { + public static func writeDocument(value: SmithyDocument?, to writer: Writer) throws { try writer.write(value) } } diff --git a/Sources/SmithyXML/Reader/Reader.swift b/Sources/SmithyXML/Reader/Reader.swift index ef62555ae..85d3f3f73 100644 --- a/Sources/SmithyXML/Reader/Reader.swift +++ b/Sources/SmithyXML/Reader/Reader.swift @@ -6,7 +6,7 @@ // @_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyReader -import enum SmithyReadWrite.Document +import struct Smithy.Document @_spi(SmithyReadWrite) import typealias SmithyReadWrite.ReadingClosure import struct Foundation.Date import struct Foundation.Data diff --git a/Sources/SmithyXML/Writer/Writer.swift b/Sources/SmithyXML/Writer/Writer.swift index f9b5ec84e..867cfb538 100644 --- a/Sources/SmithyXML/Writer/Writer.swift +++ b/Sources/SmithyXML/Writer/Writer.swift @@ -6,7 +6,7 @@ // @_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyWriter -import enum SmithyReadWrite.Document +import protocol Smithy.SmithyDocument import struct Foundation.Date import struct Foundation.Data @_spi(SmithyReadWrite) import typealias SmithyReadWrite.WritingClosure @@ -116,7 +116,7 @@ public final class Writer: SmithyWriter { try write(value?.base64EncodedString()) } - public func write(_ value: Document?) throws { + public func write(_ value: SmithyDocument?) throws { // No operation. Smithy document not supported in XML } diff --git a/Tests/SmithyJSONTests/DocumentTests.swift b/Tests/SmithyJSONTests/DocumentTests.swift new file mode 100644 index 000000000..973b04a50 --- /dev/null +++ b/Tests/SmithyJSONTests/DocumentTests.swift @@ -0,0 +1,80 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import Smithy +@_spi(SmithyReadWrite) import SmithyReadWrite +import SmithyTestUtil +@testable @_spi(SmithyReadWrite) import SmithyJSON + +class DocumentTests: XCTestCase { + let json1: [String : Any] = [ + "list": [1, 2, 3], + "map": ["a": 1, "b": 2, "c": 3], + "string": "potato", + "integer": 1, + "decimal": 1.5, + "boolean": false, + "null": NSNull() + ] + lazy var json1Data: Data = { try! JSONSerialization.data(withJSONObject: json1) }() + lazy var json1Document = { try! Document.make(from: json1) }() + + lazy var json2: [String: Any] = { + var json2 = json1 + json2["string2"] = "tomato" + return json2 + }() + lazy var json2Data: Data = { try! JSONSerialization.data(withJSONObject: json2) }() + lazy var json2Document = { try! Document.make(from: json2) }() + + func test_encode_encodesJSON() throws { + + // Create a Smithy document from the JSON object + let document = try Document.make(from: json1) + + // Write the JSON to a JSON writer. + let writer = SmithyJSON.Writer(nodeInfo: "") + try writer.write(document) + let encodedJSONData = try writer.data() + + // Check that the written JSON is equal, using the JSON comparator. + try XCTAssert(JSONComparator.jsonData(json1Data, isEqualTo: encodedJSONData)) + } + + func test_decode_decodesJSON() throws { + + // Create a reader with the Smithy JSON data + let reader = try SmithyJSON.Reader.from(data: json1Data) + + // Decode a Document from the JSON + let decodedJSONDocument: Document = try reader.read() + + // Compare equality of the two documents + XCTAssertEqual(json1Document, decodedJSONDocument) + } + + func test_compare_comparesEqualJSON() throws { + let reader1 = try SmithyJSON.Reader.from(data: json1Data) + let decodedDoc1: Document = try reader1.read() + + let reader2 = try SmithyJSON.Reader.from(data: json1Data) + let decodedDoc2: Document = try reader2.read() + + XCTAssertEqual(decodedDoc1, decodedDoc2) + } + + func test_compare_comparesUnequalJSON() throws { + let reader1 = try SmithyJSON.Reader.from(data: json1Data) + let decodedDoc1: Document = try reader1.read() + + let reader2 = try SmithyJSON.Reader.from(data: json2Data) + let decodedDoc2: Document = try reader2.read() + + XCTAssertNotEqual(decodedDoc1, decodedDoc2) + } +} 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 b617c2e36..d3a4f13df 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 @@ -27,8 +27,8 @@ import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.StreamingTrait import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.toMemberNames -import software.amazon.smithy.swift.codegen.swiftmodules.SmithyReadWriteTypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyStreamsTypes +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes import software.amazon.smithy.swift.codegen.utils.toLowerCamelCase import software.amazon.smithy.utils.StringUtils.lowerCase @@ -90,7 +90,7 @@ class ShapeValueGenerator( private fun documentDecl(writer: SwiftWriter, node: Node) { writer.openBlock( "try \$N.make(from: Data(\"\"\"", "\"\"\".utf8))", - SmithyReadWriteTypes.Document, + SmithyTypes.Document, ) { writer.write(Node.prettyPrintJson(node)) } 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 80dcf6299..a1234bd82 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 @@ -60,7 +60,6 @@ import software.amazon.smithy.swift.codegen.model.getTrait import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.nestedNamespaceType import software.amazon.smithy.swift.codegen.swiftmodules.FoundationTypes -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 software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes @@ -243,7 +242,7 @@ class SwiftSymbolProvider(private val model: Model, val swiftSettings: SwiftSett } override fun documentShape(shape: DocumentShape): Symbol { - return createSymbolBuilder(shape, "Document", "SmithyReadWrite", SwiftDeclaration.ENUM, true) + return createSymbolBuilder(shape, "Document", "Smithy", SwiftDeclaration.STRUCT, true) .addDependency(SwiftDependency.SMITHY_READ_WRITE) .build() } @@ -363,15 +362,16 @@ class SwiftSymbolProvider(private val model: Model, val swiftSettings: SwiftSett // Document: default value can be set to null, true, false, string, numbers, an empty list, or an empty map. private fun handleDocumentDefaultValue(literal: String, node: Node, builder: Symbol.Builder): Symbol.Builder { var formatString = when { - node.isObjectNode -> "\$N.object([:])" - node.isArrayNode -> "\$N.array([])" - node.isBooleanNode -> "\$N.boolean($literal)" - node.isStringNode -> "\$N.string(\"$literal\")" - node.isNumberNode -> "\$N.number($literal)" + node.isObjectNode -> "[:]" + node.isArrayNode -> "[]" + node.isBooleanNode -> literal + node.isStringNode -> "\"$literal\"" + node.isNumberNode -> literal else -> return builder // no-op } return builder.defaultValueClosure { writer -> - writer.format(formatString, SmithyReadWriteTypes.Document) + writer.addImport(SwiftDependency.SMITHY_JSON.target) + writer.format(formatString) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HTTPResponseTraitWithHTTPPayload.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HTTPResponseTraitWithHTTPPayload.kt index 93d570a51..9de30beda 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HTTPResponseTraitWithHTTPPayload.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HTTPResponseTraitWithHTTPPayload.kt @@ -18,7 +18,7 @@ import software.amazon.smithy.swift.codegen.integration.serde.member.MemberShape import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isEnum import software.amazon.smithy.swift.codegen.swiftmodules.SmithyEventStreamsTypes -import software.amazon.smithy.swift.codegen.swiftmodules.SmithyReadWriteTypes +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes class HTTPResponseTraitWithHTTPPayload( @@ -38,7 +38,7 @@ class HTTPResponseTraitWithHTTPPayload( when (target.type) { ShapeType.DOCUMENT -> { writer.openBlock("if let data = try await httpResponse.body.readData() {", "}") { - writer.write("value.\$L = try \$N.make(from: data)", memberName, SmithyReadWriteTypes.Document) + writer.write("value.\$L = try \$N.make(from: data)", memberName, SmithyTypes.Document) } } ShapeType.STRING -> { 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 755db8473..e8a6cc2d6 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 @@ -38,6 +38,7 @@ 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.model.traits.XmlFlattenedTrait +import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.serde.json.TimestampUtils @@ -50,7 +51,6 @@ import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isError import software.amazon.smithy.swift.codegen.swiftEnumCaseName import software.amazon.smithy.swift.codegen.swiftmodules.FoundationTypes -import software.amazon.smithy.swift.codegen.swiftmodules.SmithyReadWriteTypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTimestampsTypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes @@ -253,20 +253,21 @@ open class MemberShapeDecodeGenerator( } private fun resolveDocumentDefault(useZeroValue: Boolean, node: Node): String { + writer.addImport(SwiftDependency.SMITHY_JSON.target) return when { - node.isObjectNode -> writer.format(" ?? \$N.object([:])", SmithyReadWriteTypes.Document) - node.isArrayNode -> writer.format(" ?? \$N.array([])", SmithyReadWriteTypes.Document) + node.isObjectNode -> writer.format(" ?? [:]") + node.isArrayNode -> writer.format(" ?? []") node.isStringNode -> { val resolvedValue = "".takeIf { useZeroValue } ?: node.expectStringNode().value - writer.format(" ?? \$N.string(\"$resolvedValue\")", SmithyReadWriteTypes.Document) + writer.format(" ?? \"$resolvedValue\"") } node.isBooleanNode -> { val resolvedValue = "false".takeIf { useZeroValue } ?: node.expectBooleanNode().value - writer.format(" ?? \$N.boolean($resolvedValue)", SmithyReadWriteTypes.Document) + writer.format(" ?? $resolvedValue") } node.isNumberNode -> { val resolvedValue = "0".takeIf { useZeroValue } ?: node.expectNumberNode().value - writer.format(" ?? \$N.number($resolvedValue)", SmithyReadWriteTypes.Document) + writer.format(" ?? $resolvedValue") } else -> "" // null node type means no default value but explicit } 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 87677cf75..939892200 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 @@ -13,7 +13,6 @@ import software.amazon.smithy.swift.codegen.SwiftDependency object SmithyReadWriteTypes { val SmithyReader = runtimeSymbol("SmithyReader", SwiftDeclaration.PROTOCOL) val SmithyWriter = runtimeSymbol("SmithyWriter", SwiftDeclaration.PROTOCOL) - val Document = runtimeSymbol("Document", SwiftDeclaration.ENUM, emptyList()) val ReaderError = runtimeSymbol("ReaderError", SwiftDeclaration.ENUM, emptyList()) val mapWritingClosure = runtimeSymbol("mapWritingClosure", SwiftDeclaration.FUNC) val listWritingClosure = runtimeSymbol("listWritingClosure", SwiftDeclaration.FUNC) 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 13270177c..713ac236d 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 @@ -17,6 +17,7 @@ object SmithyTypes { val ClientError = runtimeSymbol("ClientError", SwiftDeclaration.ENUM) val Context = runtimeSymbol("Context", SwiftDeclaration.CLASS) val ContextBuilder = runtimeSymbol("ContextBuilder", SwiftDeclaration.CLASS) + val Document = runtimeSymbol("Document", SwiftDeclaration.STRUCT) val LogAgent = runtimeSymbol("LogAgent", SwiftDeclaration.PROTOCOL) val RequestMessageSerializer = runtimeSymbol("RequestMessageSerializer", SwiftDeclaration.PROTOCOL) val URIQueryItem = runtimeSymbol("URIQueryItem", SwiftDeclaration.STRUCT) diff --git a/smithy-swift-codegen/src/test/kotlin/HTTPBindingProtocolGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HTTPBindingProtocolGeneratorTests.kt index feaa394fd..34c92f140 100644 --- a/smithy-swift-codegen/src/test/kotlin/HTTPBindingProtocolGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HTTPBindingProtocolGeneratorTests.kt @@ -108,7 +108,7 @@ extension InlineDocumentAsPayloadOutput { static func httpOutput(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> InlineDocumentAsPayloadOutput { var value = InlineDocumentAsPayloadOutput() if let data = try await httpResponse.body.readData() { - value.documentValue = try SmithyReadWrite.Document.make(from: data) + value.documentValue = try Smithy.Document.make(from: data) } return value } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt index 4ee3d3151..043c91175 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt @@ -258,7 +258,7 @@ open class HttpProtocolUnitTestResponseGeneratorTests { let actual: InlineDocumentOutput = try await InlineDocumentOutput.httpOutput(from:)(httpResponse) let expected = InlineDocumentOutput( - documentValue: try SmithyReadWrite.Document.make(from: Data(""${'"'} + documentValue: try Smithy.Document.make(from: Data(""${'"'} { "foo": "bar" } @@ -298,7 +298,7 @@ open class HttpProtocolUnitTestResponseGeneratorTests { let actual: InlineDocumentAsPayloadOutput = try await InlineDocumentAsPayloadOutput.httpOutput(from:)(httpResponse) let expected = InlineDocumentAsPayloadOutput( - documentValue: try SmithyReadWrite.Document.make(from: Data(""${'"'} + documentValue: try Smithy.Document.make(from: Data(""${'"'} { "foo": "bar" } diff --git a/smithy-swift-codegen/src/test/kotlin/SymbolProviderTest.kt b/smithy-swift-codegen/src/test/kotlin/SymbolProviderTest.kt index 3c16d4a9d..b983b6cb9 100644 --- a/smithy-swift-codegen/src/test/kotlin/SymbolProviderTest.kt +++ b/smithy-swift-codegen/src/test/kotlin/SymbolProviderTest.kt @@ -59,7 +59,7 @@ class SymbolProviderTest { "PrimitiveDouble, Double, 0.0, false, Swift", "Boolean, Bool, nil, true, Swift", "PrimitiveBoolean, Bool, false, false, Swift", - "Document, Document, nil, true, SmithyReadWrite" + "Document, Document, nil, true, Smithy" ) fun `creates primitives`(primitiveType: String, swiftType: String, expectedDefault: String, boxed: Boolean, namespace: String?) { val model = """