Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Implement tbDEX protocol parsing test vectors
Browse files Browse the repository at this point in the history
  • Loading branch information
amika-sq committed Feb 2, 2024
1 parent 5817275 commit f208348
Show file tree
Hide file tree
Showing 20 changed files with 494 additions and 121 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "Tests/tbDEXTestVectors/tbdex-spec"]
path = Tests/tbDEXTestVectors/tbdex-spec
url = https://github.com/TBD54566975/tbdex
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
bootstrap:
# Initialize submodules
git submodule update --init
# Initialize sparse checkout in the `tbdex-spec` submodule
git -C Tests/tbDEXTestVectors/tbdex-spec config core.sparseCheckout true
# Sparse checkout only the `hosted/test-vectors` directory from `tbdex-spec`
git -C Tests/tbDEXTestVectors/tbdex-spec sparse-checkout set hosted/test-vectors
# Update submodules so they sparse checkout takes effect
git submodule update

format:
swift format --in-place --recursive .
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ let package = Package(
.package(url: "https://github.com/Frizlab/swift-typeid.git", from: "0.3.0"),
.package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"),
.package(url: "https://github.com/TBD54566975/web5-swift", exact: "0.0.1"),
.package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.1.2"),
],
targets: [
.target(
Expand All @@ -29,8 +31,19 @@ let package = Package(
),
.testTarget(
name: "tbDEXTests",
dependencies: [
"tbDEX"
]
),
.testTarget(
name: "tbDEXTestVectors",
dependencies: [
"tbDEX",
.product(name: "SwiftTestReporter", package: "swift-junit"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
resources: [
.copy("tbdex-spec/hosted/test-vectors")
]
),
]
Expand Down
9 changes: 9 additions & 0 deletions Sources/tbDEX/Common/JSON/tbDEXDateFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

/// A date formatter that can be used to encode and decode dates in the ISO8601 format,
/// compatible with the larger tbDEX ecosystem.
let tbDEXDateFormatter: ISO8601DateFormatter = {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return dateFormatter
}()
22 changes: 22 additions & 0 deletions Sources/tbDEX/Common/JSON/tbDEXJSONDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

public class tbDEXJSONDecoder: JSONDecoder {

public override init() {
super.init()

dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)

if let date = tbDEXDateFormatter.date(from: dateString) {
return date
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid date: \(dateString)"
)
}
}
}
}
14 changes: 14 additions & 0 deletions Sources/tbDEX/Common/JSON/tbDEXJSONEncoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

public class tbDEXJSONEncoder: JSONEncoder {

public override init() {
super.init()

outputFormatting = .sortedKeys
dateEncodingStrategy = .custom { date, encoder in
var container = encoder.singleValueContainer()
try container.encode(tbDEXDateFormatter.string(from: date))
}
}
}
79 changes: 79 additions & 0 deletions Sources/tbDEX/Protocol/Models/AnyMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import AnyCodable
import Foundation

/// Enumeration that can represent any `Message` type.
///
/// `AnyMessage` should be used in contexts when given a `Message`, but the exact type
/// of the `Message` is unknown until runtime.
///
/// Example: When calling an endpoint that returns `Message`s, but it's impossible to know exactly
/// what kind of `Message` it is until the JSON response is parsed.
public enum AnyMessage {
case close(Close)
case order(Order)
case orderStatus(OrderStatus)
case quote(Quote)
case rfq(RFQ)

/// Parse a JSON string into an `AnyMessage` object, which can represent any message type.
/// - Parameter jsonString: A string containing a JSON representation of a `Message`
/// - Returns: An `AnyMessage` object, representing the parsed JSON string
public static func parse(_ jsonString: String) throws -> AnyMessage {
guard let data = jsonString.data(using: .utf8) else {
throw Error.invalidJSONString
}

return try tbDEXJSONDecoder().decode(AnyMessage.self, from: data)
}
}

// MARK: - Decodable

extension AnyMessage: Decodable {

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

// Read the JSON payload into a dictionary representation
let messageJSONObject = try container.decode([String: AnyCodable].self)

// Ensure that a metadata object is present within the JSON payload
guard let metadataJSONObject = messageJSONObject["metadata"]?.value as? [String: Any] else {
throw DecodingError.valueNotFound(
AnyMessage.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "metadata not found"
)
)
}

// Decode the metadata into a strongly-typed `MessageMetadata` object
let metadataData = try JSONSerialization.data(withJSONObject: metadataJSONObject)
let metadata = try tbDEXJSONDecoder().decode(MessageMetadata.self, from: metadataData)

// Decode the message itself into it's strongly-typed representation, indicated by the `metadata.kind` field
switch metadata.kind {
case .close:
self = .close(try container.decode(Close.self))
case .order:
self = .order(try container.decode(Order.self))
case .orderStatus:
self = .orderStatus(try container.decode(OrderStatus.self))
case .quote:
self = .quote(try container.decode(Quote.self))
case .rfq:
self = .rfq(try container.decode(RFQ.self))
}
}
}

// MARK: - Errors

extension AnyMessage {

public enum Error: Swift.Error {
/// The provided JSON string is invalid
case invalidJSONString
}
}
64 changes: 64 additions & 0 deletions Sources/tbDEX/Protocol/Models/AnyResource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import AnyCodable
import Foundation

/// Enumeration that can represent any `Resource` type.
///
/// `AnyResource` should be used in contexts when given a `Resource`, but the exact type
/// of the `Resource` is unknown until runtime.
///
/// Example: When calling an endpoint that returns `Resource`s, but it's impossible to know exactly
/// what kind of `Resource` it is until the JSON response is parsed.
public enum AnyResource {
case offering(Offering)

public static func parse(_ jsonString: String) throws -> AnyResource {
guard let data = jsonString.data(using: .utf8) else {
throw Error.invalidJSONString
}

return try tbDEXJSONDecoder().decode(AnyResource.self, from: data)
}
}

// MARK: - Decodable

extension AnyResource: Decodable {

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

// Read the JSON payload into a dictionary representation
let resourceJSONObject = try container.decode([String: AnyCodable].self)

// Ensure that a metadata object is present within the JSON payload
guard let metadataJSONObject = resourceJSONObject["metadata"]?.value as? [String: Any] else {
throw DecodingError.valueNotFound(
AnyResource.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "metadata not found"
)
)
}

// Decode the metadata into a strongly-typed `ResourceMetadata` object
let metadataData = try JSONSerialization.data(withJSONObject: metadataJSONObject)
let metadata = try tbDEXJSONDecoder().decode(ResourceMetadata.self, from: metadataData)

// Decode the resource itself into it's strongly-typed representation, indicated by the `metadata.kind` field
switch metadata.kind {
case .offering:
self = .offering(try container.decode(Offering.self))
}
}
}

// MARK: - Errors

extension AnyResource {

enum Error: Swift.Error {
/// The provided JSON string is invalid
case invalidJSONString
}
}
83 changes: 41 additions & 42 deletions Sources/tbDEX/Protocol/Models/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import Web5
/// Messages form exchanges between Alice and a PFI.
///
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#messages)
public struct Message<D: MessageData>: Codable {
public struct Message<D: MessageData>: Codable, Equatable {

/// An object containing fields about the Message.
public let metadata: Metadata
public let metadata: MessageMetadata

/// The actual Message content.
public let data: D
Expand All @@ -28,9 +28,9 @@ public struct Message<D: MessageData>: Codable {
data: D
) {
let now = Date()
self.metadata = Metadata(
id: TypeID(prefix: data.kind.rawValue)!,
kind: data.kind,
self.metadata = MessageMetadata(
id: TypeID(prefix: data.kind().rawValue)!,
kind: data.kind(),
from: from,
to: to,
exchangeID: exchangeID,
Expand All @@ -52,56 +52,55 @@ public struct Message<D: MessageData>: Codable {
func verify() async throws {
_ = try await CryptoUtils.verify(didURI: metadata.from, signature: signature, detachedPayload: try digest())
}

}

// MARK: - MessageData
/// Enum containing the different types of Messages
///
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#message-kinds)
public enum MessageKind: String, Codable {
case rfq
case close
case quote
case order
case orderStatus = "orderstatus"
}

/// The actual content for a `Message`.
public protocol MessageData: Codable {

/// The kind of Message the data represents.
var kind: Message<Self>.Kind { get }
public protocol MessageData: Codable, Equatable {

/// The `MessageKind` the data represents.
func kind() -> MessageKind
}

// MARK: - Nested Types

extension Message {

/// Enum containing the different types of Messages
///
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#message-kinds)
public enum Kind: String, Codable {
case rfq
case close
case quote
case order
case orderStatus = "orderstatus"
}

/// Structure containing fields about the Message and is present in every tbDEX Message.
///
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#metadata-1)
public struct Metadata: Codable {
/// Structure containing fields about the Message and is present in every tbDEX Message.
///
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#metadata-1)
public struct MessageMetadata: Codable, Equatable {

/// The message's unique identifier
public let id: TypeID
/// The message's unique identifier
public let id: TypeID

/// The data property's type. e.g. `rfq`
public let kind: Kind
/// The data property's type. e.g. `rfq`
public let kind: MessageKind

/// The sender's DID URI
public let from: String
/// The sender's DID URI
public let from: String

/// The recipient's DID URI
public let to: String
/// The recipient's DID URI
public let to: String

/// ID for a "exchange" of messages between Alice <-> PFI. Set by the first message in an exchange.
public let exchangeID: String
/// ID for a "exchange" of messages between Alice <-> PFI. Set by the first message in an exchange.
public let exchangeID: String

/// The time at which the message was created
public let createdAt: Date
/// The time at which the message was created
public let createdAt: Date

enum CodingKeys: String, CodingKey {
case id
case kind
case from
case to
case exchangeID = "exchangeId"
case createdAt
}
}
7 changes: 3 additions & 4 deletions Sources/tbDEX/Protocol/Models/Messages/Close.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ public typealias Close = Message<CloseData>
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#close)
public struct CloseData: MessageData {

public var kind: Message<CloseData>.Kind {
.close
}

/// An explanation of why the exchange is being closed/completed
public let reason: String?

public func kind() -> MessageKind {
return .close
}
}
4 changes: 2 additions & 2 deletions Sources/tbDEX/Protocol/Models/Messages/Order.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ public typealias Order = Message<OrderData>
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#order)
public struct OrderData: MessageData {

public var kind: Message<OrderData>.Kind {
.order
public func kind() -> MessageKind {
return .order
}

}
7 changes: 3 additions & 4 deletions Sources/tbDEX/Protocol/Models/Messages/OrderStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ public typealias OrderStatus = Message<OrderStatusData>
/// [Specification Reference](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#orderstatus)
public struct OrderStatusData: MessageData {

public var kind: Message<OrderStatusData>.Kind {
.orderStatus
}

/// Current status of Order that's being executed
public let orderStatus: String

public func kind() -> MessageKind {
return .orderStatus
}
}
Loading

0 comments on commit f208348

Please sign in to comment.