Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix serial/deserialization of presentation definitions #90

Merged
merged 4 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions credentials/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ dependencies {
implementation("com.nfeld.jsonpathkt:jsonpathkt:2.0.1")
implementation("com.nimbusds:nimbus-jose-jwt:9.34")
implementation("decentralized-identity:did-common-java:1.9.0")
implementation("com.networknt:json-schema-validator:1.0.87")

testImplementation(kotlin("test"))
testImplementation("com.willowtreeapps.assertk:assertk:0.27.0")
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package web5.sdk.credentials

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonRawValue
import com.fasterxml.jackson.databind.JsonNode
import com.networknt.schema.JsonSchema
import com.networknt.schema.JsonSchemaFactory
import com.networknt.schema.SpecVersion

/**
* Presentation Exchange
*
Expand All @@ -15,7 +23,9 @@ public class PresentationDefinitionV2(
public val name: String? = null,
public val purpose: String? = null,
public val format: Format? = null,
@JsonProperty("submission_requirements")
public val submissionRequirements: List<SubmissionRequirement>? = null,
@JsonProperty("input_descriptors")
public val inputDescriptors: List<InputDescriptorV2>,
public val frame: Map<String, Any>? = null
)
Expand All @@ -40,6 +50,7 @@ public class InputDescriptorV2(
*/
public class ConstraintsV2(
public val fields: List<FieldV2>? = null,
@JsonProperty("limit_disclosure")
public val limitDisclosure: ConformantConsumerDisclosure? = null
)

Expand All @@ -52,11 +63,21 @@ public class FieldV2(
public val id: String? = null,
public val path: List<String>,
public val purpose: String? = null,
public val filter: FilterV2? = null,
@JsonRawValue
@JsonProperty("filter")
private val filterJson: JsonNode? = null,
public val predicate: Optionality? = null,
public val name: String? = null,
public val optional: Boolean? = null
)
) {
@get:JsonIgnore
public val filterSchema: JsonSchema?
get() {
if (filterJson == null) return null
val schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)
return schemaFactory.getSchema(filterJson)
}
}

/**
* Enumeration representing consumer disclosure options. Represents the possible values of `limit_disclosure' property
Expand All @@ -75,7 +96,9 @@ public enum class ConformantConsumerDisclosure(public val str: String) {
*/
public class Format(
public val jwt: JwtObject? = null,
@JsonProperty("jwt_vc")
public val jwtVc: JwtObject? = null,
@JsonProperty("jwt_vp")
public val jwtVp: JwtObject? = null
)

Expand All @@ -97,6 +120,7 @@ public class SubmissionRequirement(
public val min: Int? = null,
public val max: Int? = null,
public val from: String? = null,
@JsonProperty("from_nested")
public val fromNested: List<SubmissionRequirement>? = null
)

Expand All @@ -107,21 +131,6 @@ public enum class Rules {
All, Pick
}

/**
* Represents a number or string value.
*/
public sealed class NumberOrString {
/**
* Creates a NumberOrString from a number value.
*/
public class NumberValue(public val value: Double) : NumberOrString()

/**
* Creates a NumberOrString from a string value.
*/
public class StringValue(public val value: String) : NumberOrString()
}

/**
* Enumeration representing optionality.
*/
Expand All @@ -130,25 +139,3 @@ public enum class Optionality {
Preferred
}

/**
* Represents filtering constraints.
*/
public class FilterV2(
public val const: NumberOrString? = null,
public val enum: List<NumberOrString>? = null,
public val exclusiveMinimum: NumberOrString? = null,
public val exclusiveMaximum: NumberOrString? = null,
public val format: String? = null,
public val formatMaximum: String? = null,
public val formatMinimum: String? = null,
public val formatExclusiveMaximum: String? = null,
public val formatExclusiveMinimum: String? = null,
public val minLength: Int? = null,
public val maxLength: Int? = null,
public val minimum: NumberOrString? = null,
public val maximum: NumberOrString? = null,
public val not: Any? = null,
public val pattern: String? = null,
public val type: String
)

Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public object PresentationExchange {
var satisfied = true
for (field in requiredFields) {
// we ignore field filters
if (field.filter != null) {
if (field.filterSchema != null) {
throw UnsupportedOperationException("Field Filter is not implemented")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package web5.sdk.credentials

import assertk.assertThat
import assertk.assertions.contains
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.networknt.schema.JsonSchemaFactory
import com.networknt.schema.SpecVersion
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow

class PresentationDefinitionTest {
val jsonMapper: ObjectMapper = ObjectMapper()
.registerKotlinModule()
.findAndRegisterModules()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)

@Test
fun `can serialize`() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One additional test that I find very useful is around idempotency. See https://github.com/TBD54566975/web5-kt/blob/a8d10d9fa92226d2432dbb8fd85f9a46792bf5be/dids/src/test/kotlin/web5/sdk/dids/DidResolutionResultTest.kt#L13 for an example. It makes sure that data isn't lost in the round trip.

val factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)
val filter = factory.getSchema(
"""
{"type":"string","const":"123"}
""".trimIndent()
)
val pd = PresentationDefinitionV2(
id = "test-pd-id",
name = "simple PD",
purpose = "pd for testing",
inputDescriptors = listOf(
InputDescriptorV2(
id = "whatever",
purpose = "purpose",
constraints = ConstraintsV2(
fields = listOf(
FieldV2(
id = "field-id",
path = listOf("$.issuer"),
filterJson = filter.schemaNode
)
)
)
)
)
)
val serializedPd = jsonMapper.writeValueAsString(pd)
println(serializedPd)

assertThat(serializedPd).contains("input_descriptors")
assertThat(serializedPd).contains("123")
}

@Test
fun `can deserialize`() {
val pdString = """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would recommend splitting the json into a separate file.

{
"id": "398f69f3-a3d4-4fb1-939a-82281671f7e5",
"input_descriptors": [
{
"id": "0edade78-ed51-44ae-a0fd-5636372c0978",
"constraints": {
"fields": [
{
"path": [
"${'$'}.issuer"
],
"filter": {
"type": "string",
"const": "did:ion:EiD6Jcwrqb5lFLFWyW59uLizo5lBuChieiqtd0TFN0xsng:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJ6cC1mNnFMTW1EazZCNDFqTFhIXy1kd0xOLW9DS2lTcDJaa19WQ2t4X3ZFIiwicHVibGljS2V5SndrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6IjNmVFk3VXpBaU9VNVpGZ05VVjl3bm5pdEtGQk51RkNPLWxlRXBDVzhHOHMiLCJ5IjoidjJoNlRqTDF0TnYwSDNWb09Fbll0UVBxRHZOVC0wbVdZUUdLTGRSakJ3ayJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpQjk3STI2bmUwdkhXYXduODk1Y1dnVlE0cFF5NmN1OUFlSzV2aW44X3JVeXcifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaURqSmlEdm9RekstRl94V05VVzlzMTBUVmlpdEI0Z1JoS09iYlh2S1pwdlNRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCbEk1NWx6b3JoeE42TVBqUlZtV2ZZY3MxNzNKOFk3S0hTeU5LcmZiTzVfdyJ9fQ"
}
},
{
"path": [
"${'$'}.type[*]"
],
"filter": {
"type": "string",
"pattern": "^SanctionCredential${'$'}"
}
}
]
}
}
]
}
""".trimIndent()

assertDoesNotThrow { jsonMapper.readValue(pdString, PresentationDefinitionV2::class.java) }
}
}