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: Support RPCv2 CBOR wire protocol #887

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ let package = Package(
.library(name: "SmithyStreams", targets: ["SmithyStreams"]),
.library(name: "SmithyChecksumsAPI", targets: ["SmithyChecksumsAPI"]),
.library(name: "SmithyChecksums", targets: ["SmithyChecksums"]),
.library(name: "SmithyCBOR", targets: ["SmithyCBOR"]),
.library(name: "SmithyWaitersAPI", targets: ["SmithyWaitersAPI"]),
.library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]),
],
dependencies: {
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.40.0"),
.package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.42.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
]
let isDocCEnabled = ProcessInfo.processInfo.environment["AWS_SWIFT_SDK_ENABLE_DOCC"] != nil
Expand Down Expand Up @@ -92,6 +93,7 @@ let package = Package(
"SmithyStreams",
"SmithyChecksumsAPI",
"SmithyChecksums",
"SmithyCBOR",
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"),
],
resources: [
Expand Down Expand Up @@ -140,7 +142,7 @@ let package = Package(
),
.target(
name: "SmithyTestUtil",
dependencies: ["ClientRuntime", "SmithyHTTPAPI", "SmithyIdentity"]
dependencies: ["ClientRuntime", "SmithyHTTPAPI", "SmithyIdentity", "SmithyCBOR"]
),
.target(
name: "SmithyIdentity",
Expand Down Expand Up @@ -222,6 +224,14 @@ let package = Package(
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift")
]
),
.target(
name: "SmithyCBOR",
dependencies: [
"SmithyReadWrite",
"SmithyTimestamps",
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift")
]
),
.target(
name: "SmithyWaitersAPI"
),
Expand Down
237 changes: 237 additions & 0 deletions Sources/SmithyCBOR/Reader/Reader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import AwsCommonRuntimeKit
import Foundation

@_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyReader
@_spi(SmithyReadWrite) import enum SmithyReadWrite.ReaderError
@_spi(SmithyReadWrite) import protocol SmithyReadWrite.SmithyWriter
@_spi(Smithy) import struct Smithy.Document
@_spi(Smithy) import protocol Smithy.SmithyDocument
@_spi(SmithyTimestamps) import enum SmithyTimestamps.TimestampFormat
@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter

@_spi(SmithyReadWrite)
public final class Reader: SmithyReader {
public typealias NodeInfo = String

public let cborValue: CBORType?
public let nodeInfo: NodeInfo
public internal(set) var children: [Reader] = []
public internal(set) weak var parent: Reader?
public var hasContent: Bool { cborValue != nil && cborValue != .null }

public init(nodeInfo: NodeInfo, cborValue: CBORType?, parent: Reader? = nil) {
self.nodeInfo = nodeInfo
self.cborValue = cborValue
self.parent = parent
self.children = Self.children(from: cborValue, parent: self)
}

public static func from(data: Data) throws -> Reader {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function calls CBORDecoder which is defined in CRT for actually decoding individual types

let decoder = try CBORDecoder(data: [UInt8](data))
let rootValue: CBORType
if decoder.hasNext() {
rootValue = try decoder.popNext()
} else {
rootValue = .null
}
return Reader(nodeInfo: "", cborValue: rootValue, parent: nil)
}

private static func children(from cborValue: CBORType?, parent: Reader) -> [Reader] {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function helps form a graph for nested and complex types

var children = [Reader]()
switch cborValue {
case .map(let map):
for (key, value) in map {
let child = Reader(nodeInfo: key, cborValue: value, parent: parent)
children.append(child)
}
case .array(let array):
for (index, value) in array.enumerated() {
let child = Reader(nodeInfo: "\(index)", cborValue: value, parent: parent)
children.append(child)
}
default:
break
}
return children
}

public subscript(nodeInfo: NodeInfo) -> Reader {
if let match = children.first(where: { $0.nodeInfo == nodeInfo }) {
return match
} else {
return Reader(nodeInfo: nodeInfo, cborValue: nil, parent: self)
}
}

public func readIfPresent() throws -> String? {
switch cborValue {
case .text(let string):
return string
case .indef_text_start:
// Handle concatenation of indefinite-length text
var combinedText = ""
for child in children {
if let chunk = try child.readIfPresent() as String? {
combinedText += chunk
}
}
return combinedText
case .bytes(let data):
return String(data: data, encoding: .utf8)
default:
return nil
}
}

public func readIfPresent() throws -> Int8? {
switch cborValue {
case .int(let intValue): return Int8(intValue)
case .uint(let uintValue): return Int8(uintValue)
default: return nil
}
}

public func readIfPresent() throws -> Int16? {
switch cborValue {
case .int(let intValue): return Int16(intValue)
case .uint(let uintValue): return Int16(uintValue)
default: return nil
}
}

public func readIfPresent() throws -> Int? {
switch cborValue {
case .int(let intValue): return Int(intValue)
case .uint(let uintValue): return Int(uintValue)
default: return nil
}
}

public func readIfPresent() throws -> Float? {
switch cborValue {
case .double(let doubleValue): return Float(doubleValue)
case .int(let intValue): return Float(intValue)
case .uint(let uintValue): return Float(uintValue)
default: return nil
}
}

public func readIfPresent() throws -> Double? {
switch cborValue {
case .double(let doubleValue): return doubleValue
case .int(let intValue): return Double(intValue)
case .uint(let uintValue): return Double(uintValue)
default: return nil
}
}

public func readIfPresent() throws -> Bool? {
switch cborValue {
case .bool(let boolValue): return boolValue
default: return nil
}
}

public func readIfPresent() throws -> Data? {
switch cborValue {
case .bytes(let data): return data
case .text(let string): return Data(base64Encoded: string)
default: return nil
}
}

public func readIfPresent() throws -> Document? {
guard let cborValue else { return nil }
return Document(cborValue as! SmithyDocument)

Check warning on line 153 in Sources/SmithyCBOR/Reader/Reader.swift

View workflow job for this annotation

GitHub Actions / swiftlint

Force casts should be avoided (force_cast)
}

public func readIfPresent<T>() throws -> T? where T: RawRepresentable, T.RawValue == Int {
guard let rawValue: Int = try readIfPresent() else { return nil }
return T(rawValue: rawValue)
}

public func readIfPresent<T>() throws -> T? where T: RawRepresentable, T.RawValue == String {
guard let rawValue: String = try readIfPresent() else { return nil }
return T(rawValue: rawValue)
}

public func readTimestampIfPresent(format: TimestampFormat) throws -> Date? {
switch cborValue {
case .double(let doubleValue):
return Date(timeIntervalSince1970: doubleValue)
case .int(let intValue):
return Date(timeIntervalSince1970: Double(intValue))
case .uint(let uintValue):
return Date(timeIntervalSince1970: Double(uintValue))
case .text(let string):
return TimestampFormatter(format: format).date(from: string)
case .date(let dateValue):
return dateValue // Directly return the date value
default:
return nil
}
}

public func readMapIfPresent<Value>(
valueReadingClosure: (Reader) throws -> Value,
keyNodeInfo: NodeInfo,
valueNodeInfo: NodeInfo,
isFlattened: Bool
) throws -> [String: Value]? {
guard let cborValue else { return nil }
guard case .map(let map) = cborValue else { return nil }
var dict = [String: Value]()
for (key, _) in map {
let reader = self[key]
do {
let value = try valueReadingClosure(reader)
dict[key] = value
} catch ReaderError.requiredValueNotPresent {
if !(try reader.readNullIfPresent() ?? false) { throw ReaderError.requiredValueNotPresent }
}
}
return dict
}

public func readListIfPresent<Member>(
memberReadingClosure: (Reader) throws -> Member,
memberNodeInfo: NodeInfo,
isFlattened: Bool
) throws -> [Member]? {
guard let cborValue else { return nil }
guard case .array = cborValue else { return nil }

return try children.map { child in
// Check if the child is an indefinite-length text
if case .indef_text_start = child.cborValue {
var combinedText = ""
for grandChild in child.children {
if let chunk = try grandChild.readIfPresent() as String? {
combinedText += chunk
}
}
// Return the combined text
return combinedText as! Member

Check warning on line 222 in Sources/SmithyCBOR/Reader/Reader.swift

View workflow job for this annotation

GitHub Actions / swiftlint

Force casts should be avoided (force_cast)
} else {
// Handle regular values
return try memberReadingClosure(child)
}
}
}

public func readNullIfPresent() throws -> Bool? {
if cborValue == .null {
return true
} else {
return nil
}
}
}
Loading
Loading