Skip to content

Commit

Permalink
Create message-kind-specific parse methods and remove circular depend…
Browse files Browse the repository at this point in the history
…ency (#224)

* Parse message kinds into specific concrete class

* Fix
  • Loading branch information
diehuxx authored Apr 1, 2024
1 parent d87adb2 commit 4804128
Show file tree
Hide file tree
Showing 27 changed files with 418 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion httpserver/src/test/kotlin/ServerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,7 +106,7 @@ class GetExchangesTest {
.map { exchange ->
exchange.elements().asSequence().map {
val string = it.toString()
Message.parse(string)
Parser.parseMessage(string)
}.toList()
}
.toList()
Expand Down
152 changes: 152 additions & 0 deletions protocol/src/main/kotlin/tbdex/sdk/protocol/Parser.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
16 changes: 15 additions & 1 deletion protocol/src/main/kotlin/tbdex/sdk/protocol/models/Balance.kt
Original file line number Diff line number Diff line change
@@ -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

/**
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions protocol/src/main/kotlin/tbdex/sdk/protocol/models/Close.kt
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
}
}
}
46 changes: 0 additions & 46 deletions protocol/src/main/kotlin/tbdex/sdk/protocol/models/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}


Expand Down
Loading

0 comments on commit 4804128

Please sign in to comment.