Skip to content

Commit

Permalink
Add Metadata collection
Browse files Browse the repository at this point in the history
  • Loading branch information
gjcairo committed Oct 24, 2023
1 parent e97206c commit d345119
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 41 deletions.
266 changes: 264 additions & 2 deletions Sources/GRPCCore/Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,267 @@
* limitations under the License.
*/

// FIXME: placeholder.
public typealias Metadata = [String: String]
import Foundation

/// A collection of metadata key-value pairs, found in RPC streams.
/// A key can have multiple values associated to it.
/// Values can be either strings or binary data, in the form of `Data`.
/// - Note: Binary values must have keys ending in `-bin`, and this will be checked when adding pairs.
public struct Metadata: Sendable, Hashable {

/// A metadata value. It can either be a simple string, or binary data.
public enum MetadataValue: Sendable, Hashable {
case string(String)
case binary(Data)
}

/// A metadata key-value pair.
public struct MetadataKeyValue: Sendable, Hashable {
internal let key: String
internal let value: MetadataValue

/// Constructor for a metadata key-value pair.
/// - Parameters:
/// - key: The key for the key-value pair.
/// - value: The value to be associated to the given key. If it's a binary value, then the associated
/// key must end in `-bin`, otherwise, this method will produce an assertion failure.
init(key: String, value: MetadataValue) {
if case .binary = value {
assert(key.hasSuffix("-bin"), "Keys for binary values must end in -bin")
}
self.key = key
self.value = value
}
}

private let lockedElements: LockedValueBox<[MetadataKeyValue]>
private var elements: [MetadataKeyValue] {
get {
self.lockedElements.withLockedValue { $0 }
}
set {
self.lockedElements.withLockedValue { $0 = newValue }
}
}

/// The Metadata collection's capacity.
public var capacity: Int {
self.elements.capacity
}

/// Initialize an empty Metadata collection.
public init() {
self.lockedElements = .init([])
}

public static func == (lhs: Metadata, rhs: Metadata) -> Bool {
lhs.elements == rhs.elements
}

public func hash(into hasher: inout Hasher) {
hasher.combine(self.elements)
}

/// Reserve the specified minimum capacity in the collection.
/// - Parameter minimumCapacity: The minimum capacity to reserve in the collection.
public mutating func reserveCapacity(_ minimumCapacity: Int) {
self.elements.reserveCapacity(minimumCapacity)
}

/// Add a new key-value pair, where the value is a string.
/// - Parameters:
/// - key: The key to be associated with the given value.
/// - stringValue: The string value to be associated with the given key.
public mutating func add(key: String, stringValue: String) {
self.add(key: key, value: .string(stringValue))
}

/// Add a new key-value pair, where the value is binary data, in the form of `Data`.
/// - Parameters:
/// - key: The key to be associated with the given value. Must end in `-bin`.
/// - binaryValue: The `Data` to be associated with the given key.
public mutating func add(key: String, binaryValue: Data) {
self.add(key: key, value: .binary(binaryValue))
}

/// Add a new key-value pair.
/// - Parameters:
/// - key: The key to be associated with the given value. If value is binary, it must end in `-bin`.
/// - value: The ``MetadataValue`` to be associated with the given key.
public mutating func add(key: String, value: MetadataValue) {
self.elements.append(.init(key: key, value: value))
}

/// Adds a key-value pair to the collection, where the value is a string.
/// If there are pairs already associated to the given key, they will all be removed first, and the new pair
/// will be added. If no pairs are present with the given key, a new one will be added.
/// - Parameters:
/// - key: The key to be associated with the given value.
/// - stringValue: The string value to be associated with the given key.
public mutating func replaceOrAdd(key: String, stringValue: String) {
self.lockedElements.withLockedValue { elements in
elements.removeAll { metadataKeyValue in
metadataKeyValue.key == key
}
elements.append(.init(key: key, value: .string(stringValue)))
}
}

/// Adds a key-value pair to the collection, where the value is `Data`.
/// If there are pairs already associated to the given key, they will all be removed first, and the new pair
/// will be added. If no pairs are present with the given key, a new one will be added.
/// - Parameters:
/// - key: The key to be associated with the given value. Must end in `-bin`.
/// - binaryValue: The `Data` to be associated with the given key.
public mutating func replaceOrAdd(key: String, binaryValue: Data) {
self.lockedElements.withLockedValue { elements in
elements.removeAll { metadataKeyValue in
metadataKeyValue.key == key
}
elements.append(.init(key: key, value: .binary(binaryValue)))
}
}

/// Adds a key-value pair to the collection.
/// If there are pairs already associated to the given key, they will all be removed first, and the new pair
/// will be added. If no pairs are present with the given key, a new one will be added.
/// - Parameters:
/// - key: The key to be associated with the given value. If value is binary, it must end in `-bin`.
/// - value: The ``MetadataValue`` to be associated with the given key.
public mutating func replaceOrAdd(key: String, value: MetadataValue) {
self.lockedElements.withLockedValue { elements in
elements.removeAll { metadataKeyValue in
metadataKeyValue.key == key
}
elements.append(.init(key: key, value: value))
}
}
}

extension Metadata: RandomAccessCollection {
public typealias Element = MetadataKeyValue

public struct Index: Comparable, Sendable {
@usableFromInline
let _base: Array<Element>.Index

@inlinable
init(_base: Array<Element>.Index) {
self._base = _base
}

@inlinable
public static func < (lhs: Index, rhs: Index) -> Bool {
return lhs._base < rhs._base
}
}

public var startIndex: Index {
return .init(_base: self.elements.startIndex)
}

public var endIndex: Index {
return .init(_base: self.elements.endIndex)
}

public func index(before i: Index) -> Index {
return .init(_base: self.elements.index(before: i._base))
}

public func index(after i: Index) -> Index {
return .init(_base: self.elements.index(after: i._base))
}

public subscript(position: Index) -> Element {
self.elements[position._base]
}
}

extension Metadata {

/// An iterator for all string values associated with a given key.
/// This iterator will only return values originally stored as strings for a given key.
public struct StringValuesIterator: IteratorProtocol {
private var metadataIterator: Metadata.Iterator
private let key: String

init(forKey key: String, metadata: Metadata) {
self.metadataIterator = metadata.makeIterator()
self.key = key
}

public mutating func next() -> String? {
while let nextKeyValue = self.metadataIterator.next() {
if nextKeyValue.key == self.key {
switch nextKeyValue.value {
case .string(let stringValue):
return stringValue
case .binary:
continue
}
}
}
return nil
}
}

/// Create a new ``StringValuesIterator`` that iterates over the string values for the given key.
/// - Parameter key: The key over whose string values this iterator will iterate.
/// - Returns: An iterator to iterate over string values for the given key.
public func makeStringValuesIterator(forKey key: String) -> StringValuesIterator {
StringValuesIterator(forKey: key, metadata: self)
}

/// A subscript to get a ``StringValuesIterator`` for a given key.
public subscript(keyForStringValues key: String) -> StringValuesIterator {
StringValuesIterator(forKey: key, metadata: self)
}
}

extension Metadata {

/// An iterator for all binary data values associated with a given key.
/// This iterator will only return values originally stored as binary data for a given key.
public struct BinaryValuesIterator: IteratorProtocol {
private var metadataIterator: Metadata.Iterator
private let key: String

init(forKey key: String, metadata: Metadata) {
self.metadataIterator = metadata.makeIterator()
self.key = key
}

public mutating func next() -> Data? {
while let nextKeyValue = self.metadataIterator.next() {
if nextKeyValue.key == self.key {
switch nextKeyValue.value {
case .string:
continue
case .binary(let binaryValue):
return binaryValue
}
}
}
return nil
}
}

/// Create a new ``BinaryValuesIterator`` that iterates over the `Data` values for the given key.
/// - Parameter key: The key over whose `Data` values this iterator will iterate.
/// - Returns: An iterator to iterate over `Data` values for the given key.
public func makeBinaryValuesIterator(forKey key: String) -> BinaryValuesIterator {
BinaryValuesIterator(forKey: key, metadata: self)
}

/// A subscript to get a ``BinaryValuesIterator`` for a given key.
public subscript(keyForBinaryValues key: String) -> BinaryValuesIterator {
BinaryValuesIterator(forKey: key, metadata: self)
}
}

extension Metadata: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, MetadataValue)...) {
let elements = elements.map { MetadataKeyValue(key: $0, value: $1) }
self.lockedElements = .init(elements)
}
}
4 changes: 2 additions & 2 deletions Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import XCTest
final class ClientRequestTests: XCTestCase {
func testSingleToStreamConversion() async throws {
let (messages, continuation) = AsyncStream.makeStream(of: String.self)
let single = ClientRequest.Single(message: "foo", metadata: ["bar": "baz"])
let single = ClientRequest.Single(message: "foo", metadata: ["bar": Metadata.MetadataValue.string("baz")])
let stream = ClientRequest.Stream(single: single)

XCTAssertEqual(stream.metadata, ["bar": "baz"])
XCTAssertEqual(stream.metadata, ["bar": Metadata.MetadataValue.string("baz")])
try await stream.producer(.gathering(into: continuation))
continuation.finish()
let collected = try await messages.collect()
Expand Down
36 changes: 18 additions & 18 deletions Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,48 @@ final class ClientResponseTests: XCTestCase {
func testAcceptedSingleResponseConvenienceMethods() {
let response = ClientResponse.Single(
message: "message",
metadata: ["foo": "bar"],
trailingMetadata: ["bar": "baz"]
metadata: ["foo": Metadata.MetadataValue.string("bar")],
trailingMetadata: ["bar": Metadata.MetadataValue.string("baz")]
)

XCTAssertEqual(response.metadata, ["foo": "bar"])
XCTAssertEqual(response.metadata, ["foo": Metadata.MetadataValue.string("bar")])
XCTAssertEqual(try response.message, "message")
XCTAssertEqual(response.trailingMetadata, ["bar": "baz"])
XCTAssertEqual(response.trailingMetadata, ["bar": Metadata.MetadataValue.string("baz")])
}

func testRejectedSingleResponseConvenienceMethods() {
let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"])
let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": Metadata.MetadataValue.string("baz")])
let response = ClientResponse.Single(of: String.self, error: error)

XCTAssertEqual(response.metadata, [:])
XCTAssertThrowsRPCError(try response.message) {
XCTAssertEqual($0, error)
}
XCTAssertEqual(response.trailingMetadata, ["bar": "baz"])
XCTAssertEqual(response.trailingMetadata, ["bar": Metadata.MetadataValue.string("baz")])
}

func testAcceptedStreamResponseConvenienceMethods() async throws {
let response = ClientResponse.Stream(
of: String.self,
metadata: ["foo": "bar"],
metadata: ["foo": Metadata.MetadataValue.string("bar")],
bodyParts: RPCAsyncSequence(
wrapping: AsyncStream {
$0.yield(.message("foo"))
$0.yield(.message("bar"))
$0.yield(.message("baz"))
$0.yield(.trailingMetadata(["baz": "baz"]))
$0.yield(.trailingMetadata(["baz": Metadata.MetadataValue.string("baz")]))
$0.finish()
}
)
)

XCTAssertEqual(response.metadata, ["foo": "bar"])
XCTAssertEqual(response.metadata, ["foo": Metadata.MetadataValue.string("bar")])
let messages = try await response.messages.collect()
XCTAssertEqual(messages, ["foo", "bar", "baz"])
}

func testRejectedStreamResponseConvenienceMethods() async throws {
let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"])
let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": Metadata.MetadataValue.string("baz")])
let response = ClientResponse.Stream(of: String.self, error: error)

XCTAssertEqual(response.metadata, [:])
Expand All @@ -75,26 +75,26 @@ final class ClientResponseTests: XCTestCase {
func testStreamToSingleConversionForValidStream() async throws {
let stream = ClientResponse.Stream(
of: String.self,
metadata: ["foo": "bar"],
bodyParts: .elements(.message("foo"), .trailingMetadata(["bar": "baz"]))
metadata: ["foo": Metadata.MetadataValue.string("bar")],
bodyParts: .elements(.message("foo"), .trailingMetadata(["bar": Metadata.MetadataValue.string("baz")]))
)

let single = await ClientResponse.Single(stream: stream)
XCTAssertEqual(single.metadata, ["foo": "bar"])
XCTAssertEqual(single.metadata, ["foo": Metadata.MetadataValue.string("bar")])
XCTAssertEqual(try single.message, "foo")
XCTAssertEqual(single.trailingMetadata, ["bar": "baz"])
XCTAssertEqual(single.trailingMetadata, ["bar": Metadata.MetadataValue.string("baz")])
}

func testStreamToSingleConversionForFailedStream() async throws {
let error = RPCError(code: .aborted, message: "aborted", metadata: ["bar": "baz"])
let error = RPCError(code: .aborted, message: "aborted", metadata: ["bar": Metadata.MetadataValue.string("baz")])
let stream = ClientResponse.Stream(of: String.self, error: error)

let single = await ClientResponse.Single(stream: stream)
XCTAssertEqual(single.metadata, [:])
XCTAssertThrowsRPCError(try single.message) {
XCTAssertEqual($0, error)
}
XCTAssertEqual(single.trailingMetadata, ["bar": "baz"])
XCTAssertEqual(single.trailingMetadata, ["bar": Metadata.MetadataValue.string("baz")])
}

func testStreamToSingleConversionForInvalidSingleStream() async throws {
Expand All @@ -106,7 +106,7 @@ final class ClientResponseTests: XCTestCase {
for body in bodies {
let stream = ClientResponse.Stream(
of: String.self,
metadata: ["foo": "bar"],
metadata: ["foo": Metadata.MetadataValue.string("bar")],
bodyParts: .elements(body)
)

Expand All @@ -129,7 +129,7 @@ final class ClientResponseTests: XCTestCase {
for body in bodies {
let stream = ClientResponse.Stream(
of: String.self,
metadata: ["foo": "bar"],
metadata: ["foo": Metadata.MetadataValue.string("bar")],
bodyParts: .elements(body)
)

Expand Down
Loading

0 comments on commit d345119

Please sign in to comment.