Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Schema-based serialization #1853

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
8 changes: 7 additions & 1 deletion codegen/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ extension Target.Dependency {
static var smithyWaitersAPI: Self { .product(name: "SmithyWaitersAPI", package: "smithy-swift") }
static var smithyTestUtils: Self { .product(name: "SmithyTestUtil", package: "smithy-swift") }
static var smithyStreams: Self { .product(name: "SmithyStreams", package: "smithy-swift") }
static var smithyReadWrite: Self { .product(name: "SmithyReadWrite", package: "smithy-swift") }
static var smithyJSON: Self { .product(name: "SmithyJSON", package: "smithy-swift") }
}

// MARK: - Base Package
Expand Down Expand Up @@ -94,7 +96,8 @@ private var protocolTestTargets: [Target] {
.init(name: "RPCEventStream", sourcePath: "\(baseDirLocal)/RPCEventStream", buildOnly: true),
.init(name: "Waiters", sourcePath: "\(baseDirLocal)/Waiters", testPath: "../codegen/protocol-test-codegen-local/Tests"),
.init(name: "StringArrayEndpointParam", sourcePath: "\(baseDirLocal)/StringArrayEndpointParam"),
.init(name: "RPCV2CBORTestSDK", sourcePath: "\(baseDir)/smithy-rpcv2-cbor")
.init(name: "RPCV2CBORTestSDK", sourcePath: "\(baseDir)/smithy-rpcv2-cbor"),
.init(name: "Performance", sourcePath: "\(baseDirLocal)/Performance", testPath: "../codegen/protocol-test-codegen-local/Tests"),
]
return protocolTests.flatMap { protocolTest in
let target = Target.target(
Expand All @@ -115,11 +118,14 @@ private var protocolTestTargets: [Target] {
.smithyChecksumsAPI,
.smithyChecksums,
.smithyWaitersAPI,
.smithyReadWrite,
.smithyJSON,
.awsSDKCommon,
.awsSDKIdentity,
.awsSDKHTTPAuth,
.awsSDKEventStreamsAuth,
.awsSDKChecksums,

],
path: "\(protocolTest.sourcePath)/swift-codegen/Sources/\(protocolTest.name)"
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
@testable import Performance
@_spi(SmithyReadWrite) import SmithyReadWrite
@_spi(SmithyReadWrite) import SmithyJSON

typealias TestStructure = PerformanceClientTypes.TestStructure

final class PerformanceTests: XCTestCase {

static var array10: [TestStructure] = []
static var data10: Data = Data()
static var array100: [TestStructure] = []
static var data100: Data = Data()
static var array1000: [TestStructure] = []
static var data1000: Data = Data()

override static func setUp() {
(Self.array10, Self.data10) = try! generateData(n: 10)
(Self.array100, Self.data100) = try! generateData(n: 100)
(Self.array1000, Self.data1000) = try! generateData(n: 1000)
}

// MARK: - Schema performance tests

func test_schema_10() throws {
try run_test_schema(original: Self.array10, data: Self.data10)
}

func test_schema_100() throws {
try run_test_schema(original: Self.array100, data: Self.data100)
}

func test_schema_1000() throws {
try run_test_schema(original: Self.array1000, data: Self.data1000)
}

func run_test_schema(original: [TestStructure], data: Data, file: StaticString = #file, line: UInt = #line) throws {
var dup = [TestStructure]()
measure {
do {
let reader = try Reader.from(data: data)
dup = try reader.readListNonNull(schema: schema__namespace_aws_smithy_swift_performance__name_TestStructureList)
} catch {
XCTFail("Threw error: \(error)", file: file, line: line)
}
}
XCTAssert(original == dup)
}

// MARK: - ReadWrite performance tests

func test_readWrite_10() throws {
try run_test_readWrite(original: Self.array10, data: Self.data10)
}

func test_readWrite_100() throws {
try run_test_readWrite(original: Self.array100, data: Self.data100)
}

func test_readWrite_1000() throws {
try run_test_readWrite(original: Self.array1000, data: Self.data1000)
}

func run_test_readWrite(original: [TestStructure], data: Data, file: StaticString = #file, line: UInt = #line) throws {
var dup = [TestStructure]()
measure {
do {
let reader = try Reader.from(data: data)
dup = try reader.readList(memberReadingClosure: TestStructure.read(from:), memberNodeInfo: "member", isFlattened: false)
} catch {
XCTFail("Threw error: \(error)", file: file, line: line)
}
}
XCTAssert(original == dup)
}

// MARK: - Swift Decodable performance tests

func test_decodable_10() throws {
try run_test_decodable(original: Self.array10, data: Self.data10)
}

func test_decodable_100() throws {
try run_test_decodable(original: Self.array100, data: Self.data100)
}

func test_decodable_1000() throws {
try run_test_decodable(original: Self.array1000, data: Self.data1000)
}

func run_test_decodable(original: [TestStructure], data: Data, file: StaticString = #file, line: UInt = #line) throws {
var dup = [TestStructure]()
measure {
do {
dup = try JSONDecoder().decode([TestStructure].self, from: data)
} catch {
XCTFail("Threw error: \(error)", file: file, line: line)
}
}
XCTAssertEqual(original, dup)
}

// MARK: - Test data generation

private static func generateData(n: Int) throws -> ([TestStructure], Data) {
let array = (0..<n).map { i in
let arrayOfInts = (0..<100).map { _ in Int.random(in: 0...1000) }
let mapOfDoubles = Dictionary(uniqueKeysWithValues: (
(0..<100).map { _ in (UUID().uuidString, Double.random(in: 0...10)) }
))
return TestStructure(
bool: Bool.random(),
int: i,
listOfInts: arrayOfInts,
mapOfDoubles: mapOfDoubles,
string: UUID().uuidString
)
}
let writer = Writer(nodeInfo: "")
try writer.write(array, with: listWritingClosure(memberWritingClosure: TestStructure.write(value:to:), memberNodeInfo: "member", isFlattened: false))
let data = try writer.data()
return (array, data)
}
}

// MARK: - TestStructure extensions

// Here is TestStructure:

//public struct TestStructure: Swift.Sendable {
// public var bool: Swift.Bool?
// public var int: Swift.Int?
// public var listOfInts: [Swift.Int]?
// public var mapOfDoubles: [Swift.String: Swift.Double]?
// public var string: Swift.String?
//}

extension TestStructure {

static func read(from reader: Reader) throws -> TestStructure {
guard reader.hasContent else { throw ReaderError.requiredValueNotPresent }
var value = TestStructure()
value.int = try reader["int"].readIfPresent()
value.string = try reader["string"].readIfPresent()
value.bool = try reader["bool"].readIfPresent()
value.listOfInts = try reader["ListOfInts"].readListIfPresent(memberReadingClosure: ReadingClosures.readInt(from:), memberNodeInfo: "member", isFlattened: false)
value.mapOfDoubles = try reader["MapOfDoubles"].readMapIfPresent(valueReadingClosure: ReadingClosures.readDouble(from:), keyNodeInfo: "key", valueNodeInfo: "value", isFlattened: false)
return value
}
}

extension TestStructure: Codable {

public init(from decoder: any Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.int = try container.decodeIfPresent(Int.self, forKey: .int)
self.string = try container.decodeIfPresent(String.self, forKey: .string)
self.bool = try container.decodeIfPresent(Bool.self, forKey: .bool)
self.listOfInts = try container.decodeIfPresent([Int].self, forKey: .listOfInts)
self.mapOfDoubles = try container.decodeIfPresent([String: Double].self, forKey: .mapOfDoubles)
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(int, forKey: .int)
try container.encodeIfPresent(string, forKey: .string)
try container.encodeIfPresent(bool, forKey: .bool)
try container.encodeIfPresent(listOfInts, forKey: .listOfInts)
try container.encodeIfPresent(mapOfDoubles, forKey: .mapOfDoubles)
}

enum CodingKeys: String, CodingKey {
case int
case string
case bool
case listOfInts = "ListOfInts"
case mapOfDoubles = "MapOfDoubles"
}
}

extension TestStructure: Equatable {

public static func ==(lhs: PerformanceClientTypes.TestStructure, rhs: PerformanceClientTypes.TestStructure) -> Bool {
if lhs.int != rhs.int { return false }
if lhs.string != rhs.string { return false }
if lhs.bool != rhs.bool { return false }
if lhs.listOfInts != rhs.listOfInts { return false }
if lhs.mapOfDoubles != rhs.mapOfDoubles { return false }
return true
}
}
6 changes: 5 additions & 1 deletion codegen/protocol-test-codegen-local/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ val codegenTests = listOf(
CodegenTest(
"aws.endpointtests.stringarray#EndpointStringArray",
"StringArrayEndpointParam"
)
),
CodegenTest(
"aws.smithy.swift.performance#Performance",
"Performance"
),
)

fun generateSmithyBuild(tests: List<CodegenTest>): String {
Expand Down
49 changes: 49 additions & 0 deletions codegen/protocol-test-codegen-local/model/performance-tests.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
$version: "2.0"

namespace aws.smithy.swift.performance

use aws.protocols#restJson1
use aws.api#service

// A service which has a GET operation with waiters defined upon it.
// The acceptor in each waiter serves as subject for unit testing,
// to ensure that the logic in code-generated acceptors works as
// expected.
@service(sdkId: "Performance")
@restJson1
service Performance {
version: "2022-11-30",
operations: [GetWidget]
}

@http(uri: "/widget", method: "POST")
operation GetWidget {
input: IO,
output: IO
errors: []
}

structure IO {
list: TestStructureList
}

list TestStructureList {
member: TestStructure
}

structure TestStructure {
int: Integer
string: String
bool: Boolean
ListOfInts: ListOfIntegers
MapOfDoubles: MapOfDoubles
}

list ListOfIntegers {
member: Integer
}

map MapOfDoubles {
key: String
value: Double
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum GreetingWithErrorsOutputError {
static func httpError(from httpResponse: SmithyHTTPAPI.HTTPResponse) async throws -> Swift.Error {
let data = try await httpResponse.data()
let responseReader = try SmithyJSON.Reader.from(data: data)
responseReader.respectsJSONName = true
let baseError = try AWSClientRuntime.RestJSONError(httpResponse: httpResponse, responseReader: responseReader, noErrorWrapping: false)
if let error = baseError.customError() { return error }
if let error = try httpServiceError(baseError: baseError) { return error }
Expand Down
1 change: 1 addition & 0 deletions scripts/protogen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ rm -f codegen/protocol-test-codegen-local/build/smithyprojections/protocol-test-
rm -f codegen/protocol-test-codegen-local/build/smithyprojections/protocol-test-codegen-local/EventStream/swift-codegen/Package.swift
rm -f codegen/protocol-test-codegen-local/build/smithyprojections/protocol-test-codegen-local/RPCEventStream/swift-codegen/Package.swift
rm -f codegen/protocol-test-codegen-local/build/smithyprojections/protocol-test-codegen-local/Waiters/swift-codegen/Package.swift
rm -f codegen/protocol-test-codegen-local/build/smithyprojections/protocol-test-codegen-local/Performance/swift-codegen/Package.swift

# If on Mac, reopen Xcode to the refreshed tests
if [ -x "$(command -v osascript)" ]; then
Expand Down
Loading