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

Commit 075ff98

Browse files
authored
Implement tbDEX protocol parsing test vectors (#31)
1 parent 5817275 commit 075ff98

22 files changed

+509
-154
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ jobs:
1111
runs-on: macos-latest
1212
steps:
1313
- uses: actions/checkout@v4
14+
15+
- name: Bootstrap
16+
run: make bootstrap
17+
1418
- uses: swift-actions/setup-swift@v1
1519
with:
1620
swift-version: "5.9"
21+
1722
- name: Build
1823
run: swift build
24+
1925
- name: Run tests
2026
run: swift test

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "Tests/tbDEXTestVectors/tbdex-spec"]
2+
path = Tests/tbDEXTestVectors/tbdex-spec
3+
url = https://github.com/TBD54566975/tbdex

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1+
bootstrap:
2+
# Initialize submodules
3+
git submodule update --init
4+
# Initialize sparse checkout in the `tbdex-spec` submodule
5+
git -C Tests/tbDEXTestVectors/tbdex-spec config core.sparseCheckout true
6+
# Sparse checkout only the `hosted/test-vectors` directory from `tbdex-spec`
7+
git -C Tests/tbDEXTestVectors/tbdex-spec sparse-checkout set hosted/test-vectors
8+
# Update submodules so they sparse checkout takes effect
9+
git submodule update
10+
111
format:
212
swift format --in-place --recursive .

Package.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ let package = Package(
1717
.package(url: "https://github.com/Frizlab/swift-typeid.git", from: "0.3.0"),
1818
.package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"),
1919
.package(url: "https://github.com/TBD54566975/web5-swift", exact: "0.0.1"),
20+
.package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0"),
21+
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.1.2"),
2022
],
2123
targets: [
2224
.target(
@@ -29,8 +31,19 @@ let package = Package(
2931
),
3032
.testTarget(
3133
name: "tbDEXTests",
34+
dependencies: [
35+
"tbDEX"
36+
]
37+
),
38+
.testTarget(
39+
name: "tbDEXTestVectors",
3240
dependencies: [
3341
"tbDEX",
42+
.product(name: "SwiftTestReporter", package: "swift-junit"),
43+
.product(name: "CustomDump", package: "swift-custom-dump"),
44+
],
45+
resources: [
46+
.copy("tbdex-spec/hosted/test-vectors")
3447
]
3548
),
3649
]

README.md

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,14 @@
1-
# $PROJECT_NAME README
1+
# tbdex-swift
22

3-
Congrats, project leads! You got a new project to grow!
3+
WIP!
44

5-
This stub is meant to help you form a strong community around your work. It's yours to adapt, and may
6-
diverge from this initial structure. Just keep the files seeded in this repo, and the rest is yours to evolve!
5+
# Prerequisites
76

8-
## Introduction
7+
## Cloning
98

10-
Orient users to the project here. This is a good place to start with an assumption
11-
that the user knows very little - so start with the Big Picture and show how this
12-
project fits into it. It may be good to reference/link the broader architecture in the
13-
`collaboration` repo or the developer site here.
9+
After cloning this repository, run:
10+
```
11+
make bootstrap
12+
```
1413

15-
Then maybe a dive into what this project does.
16-
17-
Diagrams and other visuals are helpful here. Perhaps code snippets showing usage.
18-
19-
Project leads should complete, alongside this `README`:
20-
* [CODEOWNERS](./CODEOWNERS) - set project lead(s)
21-
* [CONTRIBUTING.md](./CONTRIBUTING.md) - Fill out how to: install prereqs, build, test, run, access CI, chat, discuss, file issues
22-
* [Bug-report.md](.github/ISSUE_TEMPLATE/bug-report.md) - Fill out `Assignees` add codeowners @names
23-
* [config.yml](.github/ISSUE_TEMPLATE/config.yml) - remove "(/add your discord channel..)" and replace the url with your Discord channel if applicable
24-
25-
The other files in this template repo may be used as-is:
26-
* [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
27-
* [GOVERNANCE.md](./GOVERNANCE.md)
28-
* [LICENSE](./LICENSE)
29-
30-
## Project Resources
31-
32-
| Resource | Description |
33-
| ------------------------------------------ | ------------------------------------------------------------------------------ |
34-
| [CODEOWNERS](./CODEOWNERS) | Outlines the project lead(s) |
35-
| [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) | Expected behavior for project contributors, promoting a welcoming environment |
36-
| [CONTRIBUTING.md](./CONTRIBUTING.md) | Developer guide to build, test, run, access CI, chat, discuss, file issues |
37-
| [GOVERNANCE.md](./GOVERNANCE.md) | Project governance |
38-
| [LICENSE](./LICENSE) | Apache License, Version 2.0 |
14+
This will configure the repository's submodules properly, and ensure you're all set to go!
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
/// A date formatter that can be used to encode and decode dates in the ISO8601 format,
4+
/// compatible with the larger tbDEX ecosystem.
5+
let tbDEXDateFormatter: ISO8601DateFormatter = {
6+
let dateFormatter = ISO8601DateFormatter()
7+
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
8+
return dateFormatter
9+
}()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
3+
public class tbDEXJSONDecoder: JSONDecoder {
4+
5+
public override init() {
6+
super.init()
7+
8+
dateDecodingStrategy = .custom { decoder in
9+
let container = try decoder.singleValueContainer()
10+
let dateString = try container.decode(String.self)
11+
12+
if let date = tbDEXDateFormatter.date(from: dateString) {
13+
return date
14+
} else {
15+
throw DecodingError.dataCorruptedError(
16+
in: container,
17+
debugDescription: "Invalid date: \(dateString)"
18+
)
19+
}
20+
}
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
public class tbDEXJSONEncoder: JSONEncoder {
4+
5+
public override init() {
6+
super.init()
7+
8+
outputFormatting = .sortedKeys
9+
dateEncodingStrategy = .custom { date, encoder in
10+
var container = encoder.singleValueContainer()
11+
try container.encode(tbDEXDateFormatter.string(from: date))
12+
}
13+
}
14+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import AnyCodable
2+
import Foundation
3+
4+
/// Enumeration that can represent any `Message` type.
5+
///
6+
/// `AnyMessage` should be used in contexts when given a `Message`, but the exact type
7+
/// of the `Message` is unknown until runtime.
8+
///
9+
/// Example: When calling an endpoint that returns `Message`s, but it's impossible to know exactly
10+
/// what kind of `Message` it is until the JSON response is parsed.
11+
public enum AnyMessage {
12+
case close(Close)
13+
case order(Order)
14+
case orderStatus(OrderStatus)
15+
case quote(Quote)
16+
case rfq(RFQ)
17+
18+
/// Parse a JSON string into an `AnyMessage` object, which can represent any message type.
19+
/// - Parameter jsonString: A string containing a JSON representation of a `Message`
20+
/// - Returns: An `AnyMessage` object, representing the parsed JSON string
21+
public static func parse(_ jsonString: String) throws -> AnyMessage {
22+
guard let data = jsonString.data(using: .utf8) else {
23+
throw Error.invalidJSONString
24+
}
25+
26+
return try tbDEXJSONDecoder().decode(AnyMessage.self, from: data)
27+
}
28+
}
29+
30+
// MARK: - Decodable
31+
32+
extension AnyMessage: Decodable {
33+
34+
public init(from decoder: Decoder) throws {
35+
let container = try decoder.singleValueContainer()
36+
37+
// Read the JSON payload into a dictionary representation
38+
let messageJSONObject = try container.decode([String: AnyCodable].self)
39+
40+
// Ensure that a metadata object is present within the JSON payload
41+
guard let metadataJSONObject = messageJSONObject["metadata"]?.value as? [String: Any] else {
42+
throw DecodingError.valueNotFound(
43+
AnyMessage.self,
44+
DecodingError.Context(
45+
codingPath: decoder.codingPath,
46+
debugDescription: "metadata not found"
47+
)
48+
)
49+
}
50+
51+
// Decode the metadata into a strongly-typed `MessageMetadata` object
52+
let metadataData = try JSONSerialization.data(withJSONObject: metadataJSONObject)
53+
let metadata = try tbDEXJSONDecoder().decode(MessageMetadata.self, from: metadataData)
54+
55+
// Decode the message itself into it's strongly-typed representation, indicated by the `metadata.kind` field
56+
switch metadata.kind {
57+
case .close:
58+
self = .close(try container.decode(Close.self))
59+
case .order:
60+
self = .order(try container.decode(Order.self))
61+
case .orderStatus:
62+
self = .orderStatus(try container.decode(OrderStatus.self))
63+
case .quote:
64+
self = .quote(try container.decode(Quote.self))
65+
case .rfq:
66+
self = .rfq(try container.decode(RFQ.self))
67+
}
68+
}
69+
}
70+
71+
// MARK: - Errors
72+
73+
extension AnyMessage {
74+
75+
public enum Error: Swift.Error {
76+
/// The provided JSON string is invalid
77+
case invalidJSONString
78+
}
79+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import AnyCodable
2+
import Foundation
3+
4+
/// Enumeration that can represent any `Resource` type.
5+
///
6+
/// `AnyResource` should be used in contexts when given a `Resource`, but the exact type
7+
/// of the `Resource` is unknown until runtime.
8+
///
9+
/// Example: When calling an endpoint that returns `Resource`s, but it's impossible to know exactly
10+
/// what kind of `Resource` it is until the JSON response is parsed.
11+
public enum AnyResource {
12+
case offering(Offering)
13+
14+
public static func parse(_ jsonString: String) throws -> AnyResource {
15+
guard let data = jsonString.data(using: .utf8) else {
16+
throw Error.invalidJSONString
17+
}
18+
19+
return try tbDEXJSONDecoder().decode(AnyResource.self, from: data)
20+
}
21+
}
22+
23+
// MARK: - Decodable
24+
25+
extension AnyResource: Decodable {
26+
27+
public init(from decoder: Decoder) throws {
28+
let container = try decoder.singleValueContainer()
29+
30+
// Read the JSON payload into a dictionary representation
31+
let resourceJSONObject = try container.decode([String: AnyCodable].self)
32+
33+
// Ensure that a metadata object is present within the JSON payload
34+
guard let metadataJSONObject = resourceJSONObject["metadata"]?.value as? [String: Any] else {
35+
throw DecodingError.valueNotFound(
36+
AnyResource.self,
37+
DecodingError.Context(
38+
codingPath: decoder.codingPath,
39+
debugDescription: "metadata not found"
40+
)
41+
)
42+
}
43+
44+
// Decode the metadata into a strongly-typed `ResourceMetadata` object
45+
let metadataData = try JSONSerialization.data(withJSONObject: metadataJSONObject)
46+
let metadata = try tbDEXJSONDecoder().decode(ResourceMetadata.self, from: metadataData)
47+
48+
// Decode the resource itself into it's strongly-typed representation, indicated by the `metadata.kind` field
49+
switch metadata.kind {
50+
case .offering:
51+
self = .offering(try container.decode(Offering.self))
52+
}
53+
}
54+
}
55+
56+
// MARK: - Errors
57+
58+
extension AnyResource {
59+
60+
enum Error: Swift.Error {
61+
/// The provided JSON string is invalid
62+
case invalidJSONString
63+
}
64+
}

0 commit comments

Comments
 (0)