diff --git a/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt b/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt index 798e826d..4646e809 100644 --- a/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt +++ b/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt @@ -14,6 +14,7 @@ import tbdex.sdk.httpclient.models.Exchange import tbdex.sdk.httpclient.models.GetExchangesFilter import tbdex.sdk.httpclient.models.GetOfferingsFilter import tbdex.sdk.httpclient.models.TbdexResponseException +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.Validator import tbdex.sdk.protocol.models.Balance import tbdex.sdk.protocol.models.Close @@ -275,7 +276,7 @@ object TbdexHttpClient { val responseString = response.body?.string() val jsonNode = jsonMapper.readTree(responseString) return jsonNode.get("data").elements().asSequence() - .map { Message.parse(it.toString()) } + .map { Parser.parseMessage(it.toString()) } .toList() } @@ -319,7 +320,7 @@ object TbdexHttpClient { jsonNode.get("data").elements().forEach { jsonExchange -> val exchange = jsonExchange.elements().asSequence() - .map { Message.parse(it.toString()) } + .map { Parser.parseMessage(it.toString()) } .toList() exchanges.add(exchange) } diff --git a/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/CreateExchange.kt b/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/CreateExchange.kt index e0f0127c..2d254542 100644 --- a/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/CreateExchange.kt +++ b/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/CreateExchange.kt @@ -10,6 +10,7 @@ import tbdex.sdk.httpserver.models.CreateExchangeCallback import tbdex.sdk.httpserver.models.ErrorResponse import tbdex.sdk.httpserver.models.ExchangesApi import tbdex.sdk.httpserver.models.OfferingsApi +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.models.Message import tbdex.sdk.protocol.models.Offering import tbdex.sdk.protocol.models.Rfq @@ -42,7 +43,7 @@ suspend fun createExchange( val jsonNode = Json.jsonMapper.readTree(requestBody) val rfqJsonString = jsonNode["rfq"].toString() - rfq = Message.parse(rfqJsonString) as Rfq + rfq = Rfq.parse(rfqJsonString) if (jsonNode["replyTo"] != null) { replyTo = jsonNode["replyTo"].asText() } diff --git a/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/SubmitMessage.kt b/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/SubmitMessage.kt index c9db5ddd..6c2642ff 100644 --- a/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/SubmitMessage.kt +++ b/httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/SubmitMessage.kt @@ -8,6 +8,7 @@ import tbdex.sdk.httpclient.models.ErrorDetail import tbdex.sdk.httpserver.models.Callbacks import tbdex.sdk.httpserver.models.ErrorResponse import tbdex.sdk.httpserver.models.ExchangesApi +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.models.Close import tbdex.sdk.protocol.models.Message import tbdex.sdk.protocol.models.MessageKind @@ -30,7 +31,7 @@ suspend fun submitMessage( val message: Message try { - message = Message.parse(call.receiveText()) + message = Parser.parseMessage(call.receiveText()) } catch (e: Exception) { val errorDetail = ErrorDetail(detail = "Parsing of TBDex message failed: ${e.message}") val errorResponse = ErrorResponse(listOf(errorDetail)) diff --git a/httpserver/src/test/kotlin/ServerTest.kt b/httpserver/src/test/kotlin/ServerTest.kt index d01303e4..155e484d 100644 --- a/httpserver/src/test/kotlin/ServerTest.kt +++ b/httpserver/src/test/kotlin/ServerTest.kt @@ -27,7 +27,7 @@ open class ServerTest { val serverConfig = TbdexHttpServerConfig( port = 8080, pfiDid = TestData.pfiDid.uri, - balancesEnabled = true + balancesApi = FakeBalancesApi(), ) val tbdexServer = TbdexHttpServer(serverConfig) tbdexServer.configure(this) diff --git a/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetBalancesTest.kt b/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetBalancesTest.kt index a330f772..abbed52d 100644 --- a/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetBalancesTest.kt +++ b/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetBalancesTest.kt @@ -106,7 +106,7 @@ class GetBalancesTest { val jsonNode = Json.jsonMapper.readTree(responseString) val balances = jsonNode.get("data").elements().asSequence() .map { balance -> - Resource.parse(balance.toString()) + Balance.parse(balance.toString()) } .toList() diff --git a/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangeTest.kt b/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangeTest.kt index 626785ad..35f8624d 100644 --- a/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangeTest.kt +++ b/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangeTest.kt @@ -26,6 +26,7 @@ import tbdex.sdk.httpclient.RequestToken import tbdex.sdk.httpserver.models.ErrorResponse import tbdex.sdk.httpserver.models.ExchangesApi import tbdex.sdk.httpserver.models.GetExchangeCallback +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.models.Message import tbdex.sdk.protocol.models.Rfq import tbdex.sdk.protocol.serialization.Json @@ -101,7 +102,7 @@ class GetExchangeTest { val exchange = jsonNode.get("data").elements().asSequence() .map { val string = it.toString() - Message.parse(string) + Parser.parseMessage(string) }.toList() assertEquals((exchange[0] as Rfq).metadata.from, TestData.aliceDid.uri) diff --git a/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangesTest.kt b/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangesTest.kt index b5d8c909..8b09da24 100644 --- a/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangesTest.kt +++ b/httpserver/src/test/kotlin/tbdex/sdk/httpserver/handlers/GetExchangesTest.kt @@ -27,6 +27,7 @@ import tbdex.sdk.httpserver.models.ErrorResponse import tbdex.sdk.httpserver.models.ExchangesApi import tbdex.sdk.httpserver.models.GetExchangesCallback import tbdex.sdk.httpserver.models.GetExchangesFilter +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.models.Message import tbdex.sdk.protocol.serialization.Json import web5.sdk.crypto.InMemoryKeyManager @@ -105,7 +106,7 @@ class GetExchangesTest { .map { exchange -> exchange.elements().asSequence().map { val string = it.toString() - Message.parse(string) + Parser.parseMessage(string) }.toList() } .toList() diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/Parser.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/Parser.kt new file mode 100644 index 00000000..f6488c7a --- /dev/null +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/Parser.kt @@ -0,0 +1,152 @@ +package tbdex.sdk.protocol + +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.databind.JsonNode +import tbdex.sdk.protocol.models.Balance +import tbdex.sdk.protocol.models.Close +import tbdex.sdk.protocol.models.Message +import tbdex.sdk.protocol.models.MessageKind +import tbdex.sdk.protocol.models.Offering +import tbdex.sdk.protocol.models.Order +import tbdex.sdk.protocol.models.OrderStatus +import tbdex.sdk.protocol.models.Quote +import tbdex.sdk.protocol.models.Resource +import tbdex.sdk.protocol.models.ResourceKind +import tbdex.sdk.protocol.models.Rfq +import tbdex.sdk.protocol.serialization.Json + +/** + * Utility functions for parsing TBDex Messages and Resources + */ +object Parser { + /** + * Takes an existing Message in the form of a json string and parses it into a Message object. + * Validates object structure and performs an integrity check using the message signature. + * + * @param payload The message as a json string. + * @return The json string parsed into a concrete Message implementation. + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + */ + fun parseMessage(payload: String): Message { + val jsonMessage = parseMessageToJsonNode(payload) + val kind = jsonMessage.get("metadata").get("kind").asText() + + val messageType = when (MessageKind.valueOf(kind)) { + MessageKind.rfq -> Rfq::class.java + MessageKind.order -> Order::class.java + MessageKind.orderstatus -> OrderStatus::class.java + MessageKind.quote -> Quote::class.java + MessageKind.close -> Close::class.java + } + + val message = Json.jsonMapper.convertValue(jsonMessage, messageType) + message.verify() + + return message + } + + /** + * Takes an existing Message in the form of a json string and parses it into a JsonNode object. + * Validates object structure. + * + * @param payload The message as a json string. + * @return The json string parsed into a JsonNode object. + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + */ + fun parseMessageToJsonNode(payload: String): JsonNode { + val jsonMessage: JsonNode + + try { + jsonMessage = Json.jsonMapper.readTree(payload) + } catch (e: JsonParseException) { + throw IllegalArgumentException("unexpected character at offset ${e.location.charOffset}") + } + + require(jsonMessage.isObject) { "expected payload to be a json object" } + + // validate message structure + Validator.validate(jsonMessage, "message") + + val jsonMessageData = jsonMessage.get("data") + val kind = jsonMessage.get("metadata").get("kind").asText() + + // validate specific message data (Rfq, Quote, etc) + Validator.validate(jsonMessageData, kind) + + return jsonMessage + } + + /** + * Takes an existing Resource in the form of a json string and parses it into a Resource object. + * Validates object structure and performs an integrity check using the resource signature. + * + * @param payload The resource as a json string. + * @return The json string parsed into a concrete Resource implementation. + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + */ + fun parseResource(payload: String): Resource { + val jsonResource: JsonNode = try { + Json.jsonMapper.readTree(payload) + } catch (e: JsonParseException) { + throw IllegalArgumentException("unexpected character at offset ${e.location.charOffset}") + } + + require(jsonResource.isObject) { "expected payload to be a json object" } + + // validate message structure + Validator.validate(jsonResource, "resource") + + val dataJson = jsonResource.get("data") + val kind = jsonResource.get("metadata").get("kind").asText() + + // validate specific resource data + Validator.validate(dataJson, kind) + + val resourceType = when (ResourceKind.valueOf(kind)) { + ResourceKind.offering -> Offering::class.java + ResourceKind.balance -> Balance::class.java + } + + val resource = Json.jsonMapper.convertValue(jsonResource, resourceType) + resource.verify() + + return resource + } + + /** + * Takes an existing REsource in the form of a json string and parses it into a JsonNode object. + * Validates object structure. + * + * @param payload The resource as a json string. + * @return The json string parsed into a JsonNode object. + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + */ + fun parseResourceToJsonNode(payload: String): JsonNode { + val jsonResource: JsonNode + + try { + jsonResource = Json.jsonMapper.readTree(payload) + } catch (e: JsonParseException) { + throw IllegalArgumentException("unexpected character at offset ${e.location.charOffset}") + } + + require(jsonResource.isObject) { "expected payload to be a json object" } + + // validate resource structure + Validator.validate(jsonResource, "resource") + + val jsonResourceData = jsonResource.get("data") + val kind = jsonResource.get("metadata").get("kind").asText() + + // validate specific message data (Rfq, Quote, etc) + Validator.validate(jsonResourceData, kind) + + return jsonResource + } +} \ No newline at end of file diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Balance.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Balance.kt index d1b5adfc..c02b085c 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Balance.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Balance.kt @@ -1,7 +1,9 @@ package tbdex.sdk.protocol.models import de.fxlae.typeid.TypeId +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.Validator +import tbdex.sdk.protocol.serialization.Json import java.time.OffsetDateTime /** @@ -32,7 +34,19 @@ class Balance( * @param payload The Balance as a json string. * @return The json string parsed into a concrete Balance implementation. */ - fun parse(payload: String) = Resource.parse(payload) as Balance + fun parse(payload: String): Balance { + val jsonResource = Parser.parseResourceToJsonNode(payload) + + val kind = jsonResource.get("metadata").get("kind").asText() + if (kind != "balance") { + throw IllegalArgumentException("Message must be an Balance but resource kind was $kind") + } + + val resource = Json.jsonMapper.convertValue(jsonResource, Balance::class.java) + resource.verify() + + return resource + } /** * Creates a new `Balance` resource, autopopulating the id, creation/updated time, and resource kind. diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Close.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Close.kt index f3897e09..5b01bad0 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Close.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Close.kt @@ -1,8 +1,10 @@ package tbdex.sdk.protocol.models import de.fxlae.typeid.TypeId +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.Validator import tbdex.sdk.protocol.models.Close.Companion.create +import tbdex.sdk.protocol.serialization.Json import tbdex.sdk.protocol.validateExchangeId import java.time.OffsetDateTime @@ -64,5 +66,30 @@ class Close private constructor( Validator.validateData(closeData, "close") return Close(metadata, closeData) } + + /** + * Takes an existing Close in the form of a json string and parses it into a Close object. + * Validates object structure and performs an integrity check using the message signature. + * + * @param payload The Close as a json string. + * @return The json string parsed into a Close + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + * @throws IllegalArgumentException if the payload is not a Close + */ + fun parse(payload: String): Close { + val jsonMessage = Parser.parseMessageToJsonNode(payload) + + val kind = jsonMessage.get("metadata").get("kind").asText() + if (kind != "close") { + throw IllegalArgumentException("Message must be a Close but message kind was $kind") + } + + val message = Json.jsonMapper.convertValue(jsonMessage, Close::class.java) + message.verify() + + return message + } } } \ No newline at end of file diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Message.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Message.kt index 786b046f..a4286728 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Message.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Message.kt @@ -97,52 +97,6 @@ sealed class Message { override fun toString(): String { return Json.stringify(this) } - - companion object { - /** - * Takes an existing Message in the form of a json string and parses it into a Message object. - * Validates object structure and performs an integrity check using the message signature. - * - * @param payload The message as a json string. - * @return The json string parsed into a concrete Message implementation. - * @throws IllegalArgumentException if the payload is not valid json. - * @throws IllegalArgumentException if the payload does not conform to the expected json schema. - * @throws IllegalArgumentException if the payload signature verification fails. - */ - fun parse(payload: String): Message { - val jsonMessage: JsonNode - - try { - jsonMessage = jsonMapper.readTree(payload) - } catch (e: JsonParseException) { - throw IllegalArgumentException("unexpected character at offset ${e.location.charOffset}") - } - - require(jsonMessage.isObject) { "expected payload to be a json object" } - - // validate message structure - Validator.validate(jsonMessage, "message") - - val jsonMessageData = jsonMessage.get("data") - val kind = jsonMessage.get("metadata").get("kind").asText() - - // validate specific message data (Rfq, Quote, etc) - Validator.validate(jsonMessageData, kind) - - val messageType = when (MessageKind.valueOf(kind)) { - MessageKind.rfq -> Rfq::class.java - MessageKind.order -> Order::class.java - MessageKind.orderstatus -> OrderStatus::class.java - MessageKind.quote -> Quote::class.java - MessageKind.close -> Close::class.java - } - - val message = jsonMapper.convertValue(jsonMessage, messageType) - message.verify() - - return message - } - } } diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Offering.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Offering.kt index 1bd5a984..ca29afeb 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Offering.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Offering.kt @@ -1,10 +1,12 @@ package tbdex.sdk.protocol.models import de.fxlae.typeid.TypeId +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.Validator import tbdex.sdk.protocol.models.Close.Companion.create import tbdex.sdk.protocol.models.Offering.Companion.create import tbdex.sdk.protocol.models.Order.Companion.create +import tbdex.sdk.protocol.serialization.Json import java.time.OffsetDateTime /** @@ -29,14 +31,6 @@ class Offering private constructor( override var signature: String? = null ) : Resource() { companion object { - /** - * Takes an existing Offering in the form of a json string and parses it into an Offering object. - * Validates object structure and performs an integrity check using the signature. - * - * @param payload The offering as a json string. - * @return The json string parsed into a concrete Offering implementation. - */ - fun parse(payload: String) = Resource.parse(payload) as Offering /** * Creates a new `Offering` resource, autopopulating the id, creation/updated time, and resource kind. @@ -60,5 +54,30 @@ class Offering private constructor( return Offering(metadata, data) } + + /** + * Takes an existing Offering in the form of a json string and parses it into an Offering object. + * Validates object structure and performs an integrity check using the resource signature. + * + * @param payload The Offering as a json string. + * @return The json string parsed into an Offering + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + * @throws IllegalArgumentException if the payload is not an Offering + */ + fun parse(payload: String): Offering { + val jsonResource = Parser.parseResourceToJsonNode(payload) + + val kind = jsonResource.get("metadata").get("kind").asText() + if (kind != "offering") { + throw IllegalArgumentException("Message must be an Offering but resource kind was $kind") + } + + val resource = Json.jsonMapper.convertValue(jsonResource, Offering::class.java) + resource.verify() + + return resource + } } } \ No newline at end of file diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Order.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Order.kt index 75241d3c..7f0802b2 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Order.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Order.kt @@ -1,8 +1,10 @@ package tbdex.sdk.protocol.models import de.fxlae.typeid.TypeId +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.models.Close.Companion.create import tbdex.sdk.protocol.models.Order.Companion.create +import tbdex.sdk.protocol.serialization.Json import tbdex.sdk.protocol.validateExchangeId import java.time.OffsetDateTime @@ -60,5 +62,29 @@ class Order private constructor( ) return Order(metadata, OrderData()) } + + /** + * Takes an existing Order in the form of a json string and parses it into an Order object. + * Validates object structure and performs an integrity check using the message signature. + * + * @param payload The Order as a json string. + * @return The json string parsed into an Order + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + * @throws IllegalArgumentException if the payload is not an Order + */ + fun parse(payload: String): Order { + val jsonMessage = Parser.parseMessageToJsonNode(payload) + val kind = jsonMessage.get("metadata").get("kind").asText() + if (kind != "order") { + throw IllegalArgumentException("Message must be an Order but message kind was $kind") + } + + val message = Json.jsonMapper.convertValue(jsonMessage, Order::class.java) + message.verify() + + return message + } } } \ No newline at end of file diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/OrderStatus.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/OrderStatus.kt index 6f16f733..7c280d59 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/OrderStatus.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/OrderStatus.kt @@ -1,9 +1,11 @@ package tbdex.sdk.protocol.models import de.fxlae.typeid.TypeId +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.Validator import tbdex.sdk.protocol.models.Close.Companion.create import tbdex.sdk.protocol.models.OrderStatus.Companion.create +import tbdex.sdk.protocol.serialization.Json import tbdex.sdk.protocol.validateExchangeId import java.time.OffsetDateTime @@ -65,5 +67,30 @@ class OrderStatus private constructor( return OrderStatus(metadata, orderStatusData) } + + /** + * Takes an existing OrderStatus in the form of a json string and parses it into an OrderStatus object. + * Validates object structure and performs an integrity check using the message signature. + * + * @param payload The OrderStatus as a json string. + * @return The json string parsed into an OrderStatus + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + * @throws IllegalArgumentException if the payload is not an OrderStatus + */ + fun parse(payload: String): OrderStatus { + val jsonMessage = Parser.parseMessageToJsonNode(payload) + + val kind = jsonMessage.get("metadata").get("kind").asText() + if (kind != "orderstatus") { + throw IllegalArgumentException("Message must be an OrderStatus but message kind was $kind") + } + + val message = Json.jsonMapper.convertValue(jsonMessage, OrderStatus::class.java) + message.verify() + + return message + } } } \ No newline at end of file diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Quote.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Quote.kt index ea06c56a..71a83c42 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Quote.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Quote.kt @@ -1,9 +1,11 @@ package tbdex.sdk.protocol.models import de.fxlae.typeid.TypeId +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.Validator import tbdex.sdk.protocol.models.Close.Companion.create import tbdex.sdk.protocol.models.Quote.Companion.create +import tbdex.sdk.protocol.serialization.Json import tbdex.sdk.protocol.validateExchangeId import java.time.OffsetDateTime @@ -64,5 +66,30 @@ class Quote private constructor( return Quote(metadata, quoteData) } + + /** + * Takes an existing Quote in the form of a json string and parses it into a Quote object. + * Validates object structure and performs an integrity check using the message signature. + * + * @param payload The Quote as a json string. + * @return The json string parsed into a Quote + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + * @throws IllegalArgumentException if the payload is not a Quote + */ + fun parse(payload: String): Quote { + val jsonMessage = Parser.parseMessageToJsonNode(payload) + + val kind = jsonMessage.get("metadata").get("kind").asText() + if (kind != "quote") { + throw IllegalArgumentException("Message must be a Quote but message kind was $kind") + } + + val message = Json.jsonMapper.convertValue(jsonMessage, Quote::class.java) + message.verify() + + return message + } } } \ No newline at end of file diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Resource.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Resource.kt index 109ea909..02d34b84 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Resource.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Resource.kt @@ -94,45 +94,4 @@ sealed class Resource { override fun toString(): String { return Json.stringify(this) } - - companion object { - /** - * Takes an existing Resource in the form of a json string and parses it into a Resource object. - * Validates object structure and performs an integrity check using the resource signature. - * - * @param payload The resource as a json string. - * @return The json string parsed into a concrete Resource implementation. - * @throws IllegalArgumentException if the payload is not valid json. - * @throws IllegalArgumentException if the payload does not conform to the expected json schema. - * @throws IllegalArgumentException if the payload signature verification fails. - */ - fun parse(payload: String): Resource { - val jsonResource: JsonNode = try { - jsonMapper.readTree(payload) - } catch (e: JsonParseException) { - throw IllegalArgumentException("unexpected character at offset ${e.location.charOffset}") - } - - require(jsonResource.isObject) { "expected payload to be a json object" } - - // validate message structure - Validator.validate(jsonResource, "resource") - - val dataJson = jsonResource.get("data") - val kind = jsonResource.get("metadata").get("kind").asText() - - // validate specific resource data - Validator.validate(dataJson, kind) - - val resourceType = when (ResourceKind.valueOf(kind)) { - ResourceKind.offering -> Offering::class.java - ResourceKind.balance -> Balance::class.java - } - - val resource = jsonMapper.convertValue(jsonResource, resourceType) - resource.verify() - - return resource - } - } } diff --git a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt index 2c407da9..a7c1d2a4 100644 --- a/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt +++ b/protocol/src/main/kotlin/tbdex/sdk/protocol/models/Rfq.kt @@ -2,6 +2,7 @@ package tbdex.sdk.protocol.models import com.fasterxml.jackson.databind.JsonNode import de.fxlae.typeid.TypeId +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.Validator import tbdex.sdk.protocol.models.Close.Companion.create import tbdex.sdk.protocol.models.Rfq.Companion.create @@ -112,5 +113,30 @@ class Rfq private constructor( return Rfq(metadata, rfqData, private) } + + /** + * Takes an existing Rfq in the form of a json string and parses it into a Rfq object. + * Validates object structure and performs an integrity check using the message signature. + * + * @param payload The Rfq as a json string. + * @return The json string parsed into a Rfq + * @throws IllegalArgumentException if the payload is not valid json. + * @throws IllegalArgumentException if the payload does not conform to the expected json schema. + * @throws IllegalArgumentException if the payload signature verification fails. + * @throws IllegalArgumentException if the payload is not an RFQ + */ + fun parse(payload: String): Rfq { + val jsonMessage = Parser.parseMessageToJsonNode(payload) + + val kind = jsonMessage.get("metadata").get("kind").asText() + if (kind != "rfq") { + throw IllegalArgumentException("Message must be an RFQ but message kind was $kind") + } + + val message = Json.jsonMapper.convertValue(jsonMessage, Rfq::class.java) + message.verify() + + return message + } } } \ No newline at end of file diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt index a670ac97..0f17f368 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/TbdexTestVectorsProtocol.kt @@ -77,7 +77,7 @@ class TbdexTestVectorsProtocol { val input = vector["input"].textValue() assertNotNull(input) - val tbDEXMessage = Message.parse(input) + val tbDEXMessage = Parser.parseMessage(input) assertIs(tbDEXMessage) assertEquals(vector["output"], Json.jsonMapper.readTree(tbDEXMessage.toString())) @@ -87,7 +87,7 @@ class TbdexTestVectorsProtocol { val input = vector["input"].textValue() assertNotNull(input) - val tbDEXMessage = Resource.parse(input) + val tbDEXMessage = Parser.parseResource(input) assertIs(tbDEXMessage) assertEquals(vector["output"], Json.jsonMapper.readTree(tbDEXMessage.toString())) @@ -97,6 +97,6 @@ class TbdexTestVectorsProtocol { // private fun testErrorTestVector(vector: JsonNode) { // val input = vector["input"].textValue() // assertNotNull(input) - // assertThrows(Message.parse(vector["input"]) + // assertThrows(Parser.parseMessage(vector["input"]) // } } \ No newline at end of file diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/BalanceTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/BalanceTest.kt index 21311558..cd735ac2 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/BalanceTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/BalanceTest.kt @@ -35,7 +35,7 @@ class BalanceTest { val balance = TestData.getBalance() balance.sign(TestData.PFI_DID) val jsonResource = balance.toString() - val parsedBalance = Resource.parse(jsonResource) + val parsedBalance = Balance.parse(jsonResource) assertIs(parsedBalance) assertThat(parsedBalance.toString()).isEqualTo(jsonResource) @@ -46,7 +46,7 @@ class BalanceTest { val balance = TestData.getBalance() balance.sign(TestData.PFI_DID) - val parsedBalance = assertDoesNotThrow { Resource.parse(Json.stringify(balance)) } + val parsedBalance = assertDoesNotThrow { Balance.parse(Json.stringify(balance)) } assertIs(parsedBalance) } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/CloseTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/CloseTest.kt index c1491755..003eee61 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/CloseTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/CloseTest.kt @@ -8,6 +8,7 @@ import assertk.assertions.startsWith import com.nimbusds.jose.JWSObject import de.fxlae.typeid.TypeId import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows import tbdex.sdk.protocol.TestData import tbdex.sdk.protocol.serialization.Json import kotlin.test.Test @@ -45,17 +46,25 @@ class CloseTest { val close = TestData.getClose() close.sign(TestData.PFI_DID) val jsonMessage = close.toString() - val parsedMessage = Message.parse(jsonMessage) + val parsedMessage = Close.parse(jsonMessage) assertIs(parsedMessage) assertThat(parsedMessage.toString()).isEqualTo(jsonMessage) } + @Test + fun `parse() throws if json string is not a Close`() { + val quote = TestData.getQuote() + quote.sign(TestData.ALICE_DID) + val jsonMessage = quote.toString() + assertThrows { Close.parse(jsonMessage) } + } + @Test fun `can validate a close`() { val close = TestData.getClose() close.sign(TestData.PFI_DID) - assertDoesNotThrow { Message.parse(Json.stringify(close)) } + assertDoesNotThrow { Close.parse(Json.stringify(close)) } } } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/MessageTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/MessageTest.kt index df41a732..15463f90 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/MessageTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/MessageTest.kt @@ -7,6 +7,7 @@ import assertk.assertions.isNotNull import com.nimbusds.jose.JWSObject import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.TestData import tbdex.sdk.protocol.ValidatorException import tbdex.sdk.protocol.serialization.Json @@ -31,7 +32,7 @@ class MessageTest { rfq.sign(TestData.ALICE_DID) val order = TestData.getOrder() order.sign(TestData.ALICE_DID) - val messages = listOf(rfq.toString(), order.toString()).map { Message.parse(it) } + val messages = listOf(rfq.toString(), order.toString()).map { Parser.parseMessage(it) } assertIs(messages.first()) assertIs(messages.last()) @@ -52,14 +53,14 @@ class MessageTest { @Test fun `parse throws error if json string is not valid`() { - val exception = assertThrows { Message.parse(";;;;") } + val exception = assertThrows { Parser.parseMessage(";;;;") } assertThat(exception.message!!).contains("unexpected character at offset") } @Test fun `parse throws error if message is unsigned`() { val exception = assertFailsWith { - Message.parse(Json.stringify(TestData.getQuote())) + Parser.parseMessage(Json.stringify(TestData.getQuote())) } assertContains(exception.errors, "$.signature: is missing but it is required") @@ -68,7 +69,7 @@ class MessageTest { @Test fun `parse throws error if message did is invalid`() { val exception = assertFailsWith { - Message.parse(Json.stringify(TestData.getOrderStatusWithInvalidDid())) + Parser.parseMessage(Json.stringify(TestData.getOrderStatusWithInvalidDid())) } assertContains(exception.errors[0], "does not match the regex pattern ^did") @@ -84,7 +85,7 @@ class MessageTest { order.sign(TestData.ALICE_DID) listOf(rfq, quote, order).map { - assertDoesNotThrow { Message.parse(Json.stringify(it)) } + assertDoesNotThrow { Parser.parseMessage(Json.stringify(it)) } } } @@ -95,7 +96,7 @@ class MessageTest { rfqFromAlice.sign(TestData.PFI_DID) val exception = assertThrows { - Message.parse(Json.stringify(rfqFromAlice)) + Parser.parseMessage(Json.stringify(rfqFromAlice)) } assertThat(exception.message!!).contains("Signature verification failed: Was not signed by the expected DID") } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OfferingTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OfferingTest.kt index ebece63e..edfce4a2 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OfferingTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OfferingTest.kt @@ -5,6 +5,7 @@ import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.startsWith import org.junit.jupiter.api.assertDoesNotThrow +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.TestData import tbdex.sdk.protocol.serialization.Json import kotlin.test.Test @@ -36,7 +37,7 @@ class OfferingTest { val offering = TestData.getOffering() offering.sign(TestData.PFI_DID) val jsonResource = offering.toString() - val parsed = Resource.parse(jsonResource) + val parsed = Parser.parseResource(jsonResource) assertIs(parsed) assertThat(parsed.toString()).isEqualTo(jsonResource) @@ -47,7 +48,7 @@ class OfferingTest { val offering = TestData.getOffering() offering.sign(TestData.PFI_DID) - val parsedOffering = assertDoesNotThrow { Resource.parse(Json.stringify(offering)) } + val parsedOffering = assertDoesNotThrow { Parser.parseResource(Json.stringify(offering)) } assertIs(parsedOffering) } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderStatusTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderStatusTest.kt index 49e38761..ad967569 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderStatusTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderStatusTest.kt @@ -6,6 +6,8 @@ import assertk.assertions.isEqualTo import assertk.assertions.startsWith import de.fxlae.typeid.TypeId import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.TestData import tbdex.sdk.protocol.serialization.Json import kotlin.test.Test @@ -33,17 +35,25 @@ class OrderStatusTest { val orderStatus = TestData.getOrderStatus() orderStatus.sign(TestData.PFI_DID) val jsonMessage = orderStatus.toString() - val parsedMessage = Message.parse(jsonMessage) + val parsedMessage = OrderStatus.parse(jsonMessage) assertIs(parsedMessage) assertThat(parsedMessage.toString()).isEqualTo(jsonMessage) } + @Test + fun `parse() throws if json string is not an OrderStatus`() { + val quote = TestData.getQuote() + quote.sign(TestData.ALICE_DID) + val jsonMessage = quote.toString() + assertThrows { OrderStatus.parse(jsonMessage) } + } + @Test fun `can validate an orderStatus`() { val orderStatus = TestData.getOrderStatus() orderStatus.sign(TestData.PFI_DID) - assertDoesNotThrow { Message.parse(Json.stringify(orderStatus)) } + assertDoesNotThrow { OrderStatus.parse(Json.stringify(orderStatus)) } } } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderTest.kt index e143b950..58d85099 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/OrderTest.kt @@ -7,6 +7,8 @@ import assertk.assertions.startsWith import de.fxlae.typeid.TypeId import org.junit.jupiter.api.assertAll import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.TestData import tbdex.sdk.protocol.serialization.Json import kotlin.test.Test @@ -32,17 +34,25 @@ class OrderTest { val order = TestData.getOrder() order.sign(TestData.ALICE_DID) val jsonMessage = order.toString() - val parsedMessage = Message.parse(jsonMessage) + val parsedMessage = Order.parse(jsonMessage) assertIs(parsedMessage) assertThat(parsedMessage.toString()).isEqualTo(jsonMessage) } + @Test + fun `parse() throws if json string is not an Order`() { + val quote = TestData.getQuote() + quote.sign(TestData.ALICE_DID) + val jsonMessage = quote.toString() + assertThrows { Order.parse(jsonMessage) } + } + @Test fun `can validate an order`() { val order = TestData.getOrder() order.sign(TestData.ALICE_DID) - assertDoesNotThrow { Message.parse(Json.stringify(order)) } + assertDoesNotThrow { Order.parse(Json.stringify(order)) } } } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/QuoteTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/QuoteTest.kt index cc0af1f1..e1de313e 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/QuoteTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/QuoteTest.kt @@ -6,6 +6,8 @@ import assertk.assertions.isEqualTo import assertk.assertions.startsWith import de.fxlae.typeid.TypeId import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.TestData import tbdex.sdk.protocol.serialization.Json import java.time.OffsetDateTime @@ -38,18 +40,26 @@ class QuoteTest { val quote = TestData.getQuote() quote.sign(TestData.PFI_DID) val jsonMessage = quote.toString() - val parsedMessage = Message.parse(jsonMessage) + val parsedMessage = Quote.parse(jsonMessage) assertIs(parsedMessage) assertThat(parsedMessage.toString()).isEqualTo(jsonMessage) } + @Test + fun `parse() throws if json string is not a Quote`() { + val rfq = TestData.getRfq() + rfq.sign(TestData.ALICE_DID) + val jsonMessage = rfq.toString() + assertThrows { Quote.parse(jsonMessage) } + } + @Test fun `can validate a quote`() { val quote = TestData.getQuote() quote.sign(TestData.PFI_DID) - assertDoesNotThrow { Message.parse(Json.stringify(quote)) } + assertDoesNotThrow { Quote.parse(Json.stringify(quote)) } } } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/ResourceTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/ResourceTest.kt index f022cdc4..4bd0f347 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/ResourceTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/ResourceTest.kt @@ -7,6 +7,7 @@ import assertk.assertions.isNotNull import com.nimbusds.jose.JWSObject import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows +import tbdex.sdk.protocol.Parser import tbdex.sdk.protocol.TestData import tbdex.sdk.protocol.ValidatorException import tbdex.sdk.protocol.serialization.Json @@ -43,7 +44,7 @@ class ResourceTest { @Test fun `parse throws error if json string is not valid`() { - assertThrows { Resource.parse(";;;;") } + assertThrows { Parser.parseResource(";;;;") } } @Test @@ -52,7 +53,7 @@ class ResourceTest { // do not sign it val exception = assertThrows { - Resource.parse(Json.stringify(offeringFromPfi)) + Offering.parse(Json.stringify(offeringFromPfi)) } assertThat(exception.message!!).contains( "invalid payload." @@ -65,7 +66,7 @@ class ResourceTest { // do not sign it val exception = assertThrows { - Resource.parse(Json.stringify(balanceFromPfi)) + Offering.parse(Json.stringify(balanceFromPfi)) } assertThat(exception.message!!).contains( "invalid payload." @@ -79,7 +80,7 @@ class ResourceTest { offeringFromPfi.sign(TestData.ALICE_DID) val exception = assertThrows { - Resource.parse(Json.stringify(offeringFromPfi)) + Parser.parseResource(Json.stringify(offeringFromPfi)) } assertThat(exception.message!!).contains( "Signature verification failed: Was not signed by the expected DID" @@ -93,7 +94,7 @@ class ResourceTest { balanceFromPfi.sign(TestData.ALICE_DID) val exception = assertThrows { - Resource.parse(Json.stringify(balanceFromPfi)) + Balance.parse(Json.stringify(balanceFromPfi)) } assertThat(exception.message!!).contains("Signature verification failed: Was not signed by the expected DID") } diff --git a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt index 69a13079..0d5fd117 100644 --- a/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt +++ b/protocol/src/test/kotlin/tbdex/sdk/protocol/models/RfqTest.kt @@ -41,18 +41,26 @@ class RfqTest { val rfq = TestData.getRfq() rfq.sign(TestData.ALICE_DID) val jsonMessage = rfq.toString() - val parsedMessage = Message.parse(jsonMessage) + val parsedMessage = Rfq.parse(jsonMessage) assertIs(parsedMessage) assertThat(parsedMessage.toString()).isEqualTo(jsonMessage) } + @Test + fun `parse() throws if json string is not an Rfq`() { + val quote = TestData.getQuote() + quote.sign(TestData.ALICE_DID) + val jsonMessage = quote.toString() + assertThrows { Rfq.parse(jsonMessage) } + } + @Test fun `can validate a rfq`() { val rfq = TestData.getRfq() rfq.sign(TestData.ALICE_DID) - assertDoesNotThrow { Message.parse(Json.stringify(rfq)) } + assertDoesNotThrow { Rfq.parse(Json.stringify(rfq)) } } @Test