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 (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
amika-sq authored Feb 2, 2024
1 parent 5817275 commit 075ff98
Show file tree
Hide file tree
Showing 22 changed files with 509 additions and 154 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- name: Bootstrap
run: make bootstrap

- uses: swift-actions/setup-swift@v1
with:
swift-version: "5.9"

- name: Build
run: swift build

- name: Run tests
run: swift test
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
42 changes: 9 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,14 @@
# $PROJECT_NAME README
# tbdex-swift

Congrats, project leads! You got a new project to grow!
WIP!

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

## Introduction
## Cloning

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

Then maybe a dive into what this project does.

Diagrams and other visuals are helpful here. Perhaps code snippets showing usage.

Project leads should complete, alongside this `README`:
* [CODEOWNERS](./CODEOWNERS) - set project lead(s)
* [CONTRIBUTING.md](./CONTRIBUTING.md) - Fill out how to: install prereqs, build, test, run, access CI, chat, discuss, file issues
* [Bug-report.md](.github/ISSUE_TEMPLATE/bug-report.md) - Fill out `Assignees` add codeowners @names
* [config.yml](.github/ISSUE_TEMPLATE/config.yml) - remove "(/add your discord channel..)" and replace the url with your Discord channel if applicable

The other files in this template repo may be used as-is:
* [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
* [GOVERNANCE.md](./GOVERNANCE.md)
* [LICENSE](./LICENSE)

## Project Resources

| Resource | Description |
| ------------------------------------------ | ------------------------------------------------------------------------------ |
| [CODEOWNERS](./CODEOWNERS) | Outlines the project lead(s) |
| [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) | Expected behavior for project contributors, promoting a welcoming environment |
| [CONTRIBUTING.md](./CONTRIBUTING.md) | Developer guide to build, test, run, access CI, chat, discuss, file issues |
| [GOVERNANCE.md](./GOVERNANCE.md) | Project governance |
| [LICENSE](./LICENSE) | Apache License, Version 2.0 |
This will configure the repository's submodules properly, and ensure you're all set to go!
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
}
}
Loading

0 comments on commit 075ff98

Please sign in to comment.