diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 27324c9f66..0363624932 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -26,6 +26,7 @@ jobs: pull-requests: write env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SBT_OPTS: -Xmx2G container: image: ghcr.io/hyperledger-labs/ci-debian-jdk-22:0.1.0 volumes: diff --git a/build.sbt b/build.sbt index 3f177de778..b92face4dd 100644 --- a/build.sbt +++ b/build.sbt @@ -118,6 +118,7 @@ lazy val D = new { val circeGeneric: ModuleID = "io.circe" %% "circe-generic" % V.circe val circeParser: ModuleID = "io.circe" %% "circe-parser" % V.circe + val networkntJsonSchemaValidator = "com.networknt" % "json-schema-validator" % V.jsonSchemaValidator val jwtCirce = "com.github.jwt-scala" %% "jwt-circe" % V.jwtCirceVersion val jsonCanonicalization: ModuleID = "io.github.erdtman" % "java-json-canonicalization" % "1.1" val titaniumJsonLd: ModuleID = "com.apicatalog" % "titanium-json-ld" % "1.4.0" @@ -179,16 +180,28 @@ lazy val D_Shared = new { D.zio, D.zioHttp, D.scalaUri, + D.zioPrelude, // FIXME: split shared DB stuff as subproject? D.doobieHikari, D.doobiePostgres, D.zioCatsInterop, - D.zioPrelude, + ) +} + +lazy val D_SharedJson = new { + lazy val dependencies: Seq[ModuleID] = + Seq( + D.zio, + D.zioJson, + D.circeCore, + D.circeGeneric, + D.circeParser, D.jsonCanonicalization, D.titaniumJsonLd, D.jakartaJson, D.ironVC, D.scodecBits, + D.networkntJsonSchemaValidator ) } @@ -306,8 +319,6 @@ lazy val D_Pollux_VC_JWT = new { val zio = "dev.zio" %% "zio" % V.zio val zioPrelude = "dev.zio" %% "zio-prelude" % V.zioPreludeVersion - val networkntJsonSchemaValidator = "com.networknt" % "json-schema-validator" % V.jsonSchemaValidator - val zioTest = "dev.zio" %% "zio-test" % V.zio % Test val zioTestSbt = "dev.zio" %% "zio-test-sbt" % V.zio % Test val zioTestMagnolia = "dev.zio" %% "zio-test-magnolia" % V.zio % Test @@ -315,7 +326,7 @@ lazy val D_Pollux_VC_JWT = new { // Dependency Modules val zioDependencies: Seq[ModuleID] = Seq(zio, zioPrelude, zioTest, zioTestSbt, zioTestMagnolia) val baseDependencies: Seq[ModuleID] = - zioDependencies :+ D.jwtCirce :+ networkntJsonSchemaValidator :+ D.nimbusJwt :+ D.scalaTest + zioDependencies :+ D.jwtCirce :+ D.networkntJsonSchemaValidator :+ D.nimbusJwt :+ D.scalaTest // Project Dependencies lazy val polluxVcJwtDependencies: Seq[ModuleID] = baseDependencies @@ -456,6 +467,15 @@ lazy val shared = (project in file("shared/core")) libraryDependencies ++= D_Shared.dependencies ) +lazy val sharedJson = (project in file("shared/json")) + .settings(commonSetttings) + .settings( + name := "shared-json", + crossPaths := false, + libraryDependencies ++= D_SharedJson.dependencies + ) + .dependsOn(shared) + lazy val sharedCrypto = (project in file("shared/crypto")) .settings(commonSetttings) .settings( @@ -714,7 +734,7 @@ lazy val polluxVcJWT = project name := "pollux-vc-jwt", libraryDependencies ++= D_Pollux_VC_JWT.polluxVcJwtDependencies ) - .dependsOn(castorCore) + .dependsOn(castorCore, sharedJson) lazy val polluxCore = project .in(file("pollux/core")) @@ -734,6 +754,7 @@ lazy val polluxCore = project polluxAnoncreds, polluxVcJWT, polluxSDJWT, + polluxPreX ) lazy val polluxDoobie = project @@ -747,6 +768,12 @@ lazy val polluxDoobie = project .dependsOn(shared) .dependsOn(sharedTest % "test->test") +lazy val polluxPreX = project + .in(file("pollux/prex")) + .settings(commonSetttings) + .settings(name := "pollux-prex") + .dependsOn(shared, sharedJson) + // ######################## // ### Pollux Anoncreds ### // ######################## @@ -891,6 +918,7 @@ releaseProcess := Seq[ReleaseStep]( lazy val aggregatedProjects: Seq[ProjectReference] = Seq( shared, + sharedJson, sharedCrypto, sharedTest, models, @@ -917,6 +945,7 @@ lazy val aggregatedProjects: Seq[ProjectReference] = Seq( polluxAnoncreds, polluxAnoncredsTest, polluxSDJWT, + polluxPreX, connectCore, connectDoobie, agentWalletAPI, diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala index 393115be47..05d56eb62b 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/http/ZHttp4sBlazeServer.scala @@ -9,7 +9,7 @@ import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router import org.hyperledger.identus.api.http.ErrorResponse import org.hyperledger.identus.shared.crypto.Sha256Hash -import org.hyperledger.identus.shared.utils.Json +import org.hyperledger.identus.shared.json.Json import org.hyperledger.identus.system.controller.SystemEndpoints import sttp.tapir.* import sttp.tapir.model.ServerRequest diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala index d1f893316c..64269a575d 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/codec/CirceJsonInterop.scala @@ -1,36 +1,15 @@ package org.hyperledger.identus.api.http.codec import io.circe.Json as CirceJson +import org.hyperledger.identus.shared.json.JsonInterop import sttp.tapir.json.zio.* import sttp.tapir.Schema import zio.json.* import zio.json.ast.Json as ZioJson object CirceJsonInterop { - - private def toZioJsonAst(circeJson: CirceJson): ZioJson = { - val encoded = circeJson.noSpaces - encoded.fromJson[ZioJson] match { - case Left(failure) => - throw Exception(s"Circe and Zio Json interop fail. Unable to convert from Circe to Zio AST. $failure") - case Right(value) => value - } - } - - private def toCirceJsonAst(zioJson: ZioJson): CirceJson = { - val encoded = zioJson.toJson - io.circe.parser.parse(encoded).left.map(_.toString) match { - case Left(failure) => - throw Exception(s"Circe and Zio Json interop fail. Unable to convert from Zio to Circe AST. $failure") - case Right(value) => value - } - } - - given encodeJson: JsonEncoder[CirceJson] = JsonEncoder[ZioJson].contramap(toZioJsonAst) - - given decodeJson: JsonDecoder[CirceJson] = JsonDecoder[ZioJson].map(toCirceJsonAst) - + given encodeJson: JsonEncoder[CirceJson] = JsonEncoder[ZioJson].contramap(JsonInterop.toZioJsonAst) + given decodeJson: JsonDecoder[CirceJson] = JsonDecoder[ZioJson].map(JsonInterop.toCirceJsonAst) given schemaJson: Schema[CirceJson] = - Schema.derived[ZioJson].map[CirceJson](js => Some(toCirceJsonAst(js)))(toZioJsonAst) - + Schema.derived[ZioJson].map[CirceJson](js => Some(JsonInterop.toCirceJsonAst(js)))(JsonInterop.toZioJsonAst) } diff --git a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/ConnectionRecord.scala b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/ConnectionRecord.scala index 1d215cc9fb..026e651312 100644 --- a/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/ConnectionRecord.scala +++ b/connect/core/src/main/scala/org/hyperledger/identus/connect/core/model/ConnectionRecord.scala @@ -3,10 +3,7 @@ package org.hyperledger.identus.connect.core.model import org.hyperledger.identus.connect.core.model.ConnectionRecord.{ProtocolState, Role} import org.hyperledger.identus.mercury.protocol.connection.{ConnectionRequest, ConnectionResponse} import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation -import org.hyperledger.identus.shared.models.Failure -import org.hyperledger.identus.shared.models.WalletAccessContext -import org.hyperledger.identus.shared.models.WalletId -import zio.ZIO +import org.hyperledger.identus.shared.models.{Failure, WalletId} import java.time.temporal.ChronoUnit import java.time.Instant diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/CredentialOfferAttachment.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/CredentialOfferAttachment.scala index ad1537f49d..9897e0f0dc 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/CredentialOfferAttachment.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/CredentialOfferAttachment.scala @@ -2,7 +2,8 @@ package org.hyperledger.identus.pollux.core.model import io.circe.* import io.circe.generic.semiauto.* -import org.hyperledger.identus.pollux.core.model.presentation.{Options, PresentationDefinition} +import org.hyperledger.identus.pollux.core.model.presentation.Options +import org.hyperledger.identus.pollux.prex.PresentationDefinition final case class CredentialOfferAttachment(options: Options, presentation_definition: PresentationDefinition) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala index 886d44caa1..47993a370b 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala @@ -4,7 +4,7 @@ import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.mercury.protocol.presentproof.{Presentation, ProposePresentation, RequestPresentation} import org.hyperledger.identus.shared.models.{Failure, WalletAccessContext, WalletId} -import zio.{UIO, URIO, ZIO} +import zio.{URIO, ZIO} import java.time.temporal.ChronoUnit import java.time.Instant diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala index 9ced10234c..e1b1acd1f2 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialSchemaError.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.model.error -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError +import org.hyperledger.identus.shared.json.JsonSchemaError import org.hyperledger.identus.shared.models.{Failure, StatusCode} sealed trait CredentialSchemaError( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala index 3a47821c7e..bcf096c449 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala @@ -1,8 +1,8 @@ package org.hyperledger.identus.pollux.core.model.error -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError import org.hyperledger.identus.pollux.core.model.DidCommID import org.hyperledger.identus.pollux.core.service.URIDereferencerError +import org.hyperledger.identus.shared.json.JsonSchemaError import org.hyperledger.identus.shared.models.{Failure, StatusCode} sealed trait PresentationError( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachment.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachment.scala index 7ee15e682b..41bd261a50 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachment.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachment.scala @@ -2,67 +2,7 @@ package org.hyperledger.identus.pollux.core.model.presentation import io.circe.* import io.circe.generic.semiauto.* - -case class Field( - id: Option[String] = None, - path: Seq[String] = Seq.empty, - name: Option[String] = None, - purpose: Option[String] = None -) -object Field { - given Encoder[Field] = deriveEncoder[Field] - given Decoder[Field] = deriveDecoder[Field] -} - -case class Jwt(alg: Seq[String], proof_type: Seq[String]) -object Jwt { - given Encoder[Jwt] = deriveEncoder[Jwt] - given Decoder[Jwt] = deriveDecoder[Jwt] -} -case class Ldp(proof_type: Seq[String]) -object Ldp { - given Encoder[Ldp] = deriveEncoder[Ldp] - given Decoder[Ldp] = deriveDecoder[Ldp] -} -case class ClaimFormat(jwt: Option[Jwt] = None, ldp: Option[Ldp] = None) -object ClaimFormat { - given Encoder[ClaimFormat] = deriveEncoder[ClaimFormat] - given Decoder[ClaimFormat] = deriveDecoder[ClaimFormat] -} -case class Constraints(fields: Option[Seq[Field]]) -object Constraints { - given Encoder[Constraints] = deriveEncoder[Constraints] - given Decoder[Constraints] = deriveDecoder[Constraints] -} - -/** Refer to Input Descriptors - */ -case class InputDescriptor( - id: String = java.util.UUID.randomUUID.toString(), - name: Option[String] = None, - purpose: Option[String] = None, - format: Option[ClaimFormat] = None, - constraints: Constraints -) -object InputDescriptor { - given Encoder[InputDescriptor] = deriveEncoder[InputDescriptor] - given Decoder[InputDescriptor] = deriveDecoder[InputDescriptor] -} - -/** Refer to Presentation - * Definition - */ -case class PresentationDefinition( - id: String = java.util.UUID.randomUUID.toString(), // UUID - input_descriptors: Seq[InputDescriptor] = Seq.empty, - name: Option[String] = None, - purpose: Option[String] = None, - format: Option[ClaimFormat] = None -) -object PresentationDefinition { - given Encoder[PresentationDefinition] = deriveEncoder[PresentationDefinition] - given Decoder[PresentationDefinition] = deriveDecoder[PresentationDefinition] -} +import org.hyperledger.identus.pollux.prex.PresentationDefinition case class Options(challenge: String, domain: String) object Options { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala index 2486c17cf2..809f0b1c88 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala @@ -8,8 +8,8 @@ import org.hyperledger.identus.pollux.core.model.schema.`type`.{ CredentialSchemaType } import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 -import org.hyperledger.identus.pollux.core.model.schema.validator.{JsonSchemaValidator, JsonSchemaValidatorImpl} import org.hyperledger.identus.pollux.core.service.URIDereferencer +import org.hyperledger.identus.shared.json.{JsonSchemaValidator, JsonSchemaValidatorImpl} import zio.* import zio.json.* import zio.json.ast.Json diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/AnoncredSchemaType.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/AnoncredSchemaType.scala index b8a699d957..7a5ae12ca2 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/AnoncredSchemaType.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/AnoncredSchemaType.scala @@ -3,13 +3,8 @@ package org.hyperledger.identus.pollux.core.model.schema.`type` import com.networknt.schema.* import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1.* -import org.hyperledger.identus.pollux.core.model.schema.validator.{ - JsonSchemaError, - JsonSchemaUtils, - JsonSchemaValidatorImpl, - SchemaSerDes -} import org.hyperledger.identus.pollux.core.model.schema.Schema +import org.hyperledger.identus.shared.json.{JsonSchemaError, JsonSchemaUtils, JsonSchemaValidatorImpl, SchemaSerDes} import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaSerDesV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaSerDesV1.scala index e0659beae9..d3cf6336d9 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaSerDesV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaSerDesV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.model.schema.`type` -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* import zio.json.ast.Json diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaType.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaType.scala index 0efa26528c..8e1a4b2e52 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaType.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialJsonSchemaType.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.pollux.core.model.schema.`type` -import org.hyperledger.identus.pollux.core.model.schema.validator.{JsonSchemaError, JsonSchemaValidatorImpl} import org.hyperledger.identus.pollux.core.model.schema.Schema +import org.hyperledger.identus.shared.json.{JsonSchemaError, JsonSchemaValidatorImpl} import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialSchemaType.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialSchemaType.scala index 5dee559af8..03edd43936 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialSchemaType.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/CredentialSchemaType.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.pollux.core.model.schema.`type` -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError import org.hyperledger.identus.pollux.core.model.schema.Schema +import org.hyperledger.identus.shared.json.JsonSchemaError import zio.IO trait CredentialSchemaType { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/anoncred/AnoncredSchemaSerDesV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/anoncred/AnoncredSchemaSerDesV1.scala index 3890974da2..5e0ae8f9e4 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/anoncred/AnoncredSchemaSerDesV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/type/anoncred/AnoncredSchemaSerDesV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaError.scala deleted file mode 100644 index 73993f1959..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaError.scala +++ /dev/null @@ -1,17 +0,0 @@ -package org.hyperledger.identus.pollux.core.model.schema.validator - -sealed trait JsonSchemaError { - def error: String -} - -object JsonSchemaError { - case class JsonSchemaParsingError(error: String) extends JsonSchemaError - - case class JsonValidationErrors(errors: Seq[String]) extends JsonSchemaError { - def error: String = errors.mkString(";") - } - - case class UnsupportedJsonSchemaSpecVersion(error: String) extends JsonSchemaError - - case class UnexpectedError(error: String) extends JsonSchemaError -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaUtils.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaUtils.scala deleted file mode 100644 index db76a7439a..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaUtils.scala +++ /dev/null @@ -1,55 +0,0 @@ -package org.hyperledger.identus.pollux.core.model.schema.validator - -import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} -import com.networknt.schema.* -import com.networknt.schema.SpecVersion.VersionFlag -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError.* -import zio.* -import zio.json.ast.Json - -object JsonSchemaUtils { - def jsonSchema( - schema: String, - supportedVersions: IndexedSeq[VersionFlag] = IndexedSeq.empty - ): IO[JsonSchemaError, JsonSchema] = { - for { - jsonSchemaNode <- toJsonNode(schema) - specVersion <- ZIO - .attempt(SpecVersionDetector.detect(jsonSchemaNode)) - .mapError(t => UnexpectedError(t.getMessage)) - _ <- - if (supportedVersions.nonEmpty && !supportedVersions.contains(specVersion)) - ZIO.fail( - UnsupportedJsonSchemaSpecVersion( - s"Unsupported JsonSchemaVersion. Current:$specVersion ExpectedOneOf:${supportedVersions.map(_.getId)}" - ) - ) - else ZIO.unit - mapper <- ZIO.attempt(new ObjectMapper()).mapError(t => UnexpectedError(t.getMessage)) - factory <- ZIO - .attempt(JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(specVersion)).jsonMapper(mapper).build) - .mapError(t => UnexpectedError(t.getMessage)) - jsonSchema <- ZIO.attempt(factory.getSchema(jsonSchemaNode)).mapError(t => UnexpectedError(t.getMessage)) - } yield jsonSchema - } - - def from( - schema: Json, - supportedVersions: IndexedSeq[VersionFlag] = IndexedSeq.empty - ): IO[JsonSchemaError, JsonSchema] = { - jsonSchema(schema.toString(), supportedVersions) - } - - def toJsonNode(json: Json): IO[JsonSchemaError, JsonNode] = { - toJsonNode(json.toString()) - } - - def toJsonNode(json: String): IO[JsonSchemaError, JsonNode] = { - for { - mapper <- ZIO.attempt(new ObjectMapper()).mapError(t => UnexpectedError(t.getMessage)) - jsonSchemaNode <- ZIO - .attempt(mapper.readTree(json)) - .mapError(t => JsonSchemaParsingError(t.getMessage)) - } yield jsonSchemaNode - } -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaValidator.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaValidator.scala deleted file mode 100644 index 31e002323e..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaValidator.scala +++ /dev/null @@ -1,12 +0,0 @@ -package org.hyperledger.identus.pollux.core.model.schema.validator - -import com.fasterxml.jackson.databind.JsonNode -import zio.* - -trait JsonSchemaValidator { - def validate(claims: String): IO[JsonSchemaError, Unit] - - def validate(claimsJsonNode: JsonNode): IO[JsonSchemaError, Unit] = { - validate(claimsJsonNode.toString) - } -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaValidatorImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaValidatorImpl.scala deleted file mode 100644 index 7ad58c51b1..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/JsonSchemaValidatorImpl.scala +++ /dev/null @@ -1,35 +0,0 @@ -package org.hyperledger.identus.pollux.core.model.schema.validator - -import com.networknt.schema.* -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.* -import org.hyperledger.identus.pollux.core.model.schema.Schema -import zio.* - -case class JsonSchemaValidatorImpl(schemaValidator: JsonSchema) extends JsonSchemaValidator { - override def validate(jsonString: String): IO[JsonSchemaError, Unit] = { - import scala.jdk.CollectionConverters.* - for { - // Convert claims to JsonNode - jsonClaims <- JsonSchemaUtils.toJsonNode(jsonString) - - // Validate claims JsonNode - validationMessages <- ZIO - .attempt(schemaValidator.validate(jsonClaims).asScala.toSeq) - .mapError(t => JsonSchemaError.JsonValidationErrors(Seq(t.getMessage))) - - validationResult <- - if (validationMessages.isEmpty) ZIO.unit - else ZIO.fail(JsonSchemaError.JsonValidationErrors(validationMessages.map(_.getMessage))) - } yield validationResult - } - -} - -object JsonSchemaValidatorImpl { - def from(schema: Schema): IO[JsonSchemaError, JsonSchemaValidatorImpl] = { - for { - jsonSchema <- JsonSchemaUtils.from(schema, IndexedSeq(SpecVersion.VersionFlag.V202012)) - } yield JsonSchemaValidatorImpl(jsonSchema) - } -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/SchemaSerDes.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/SchemaSerDes.scala deleted file mode 100644 index 982a91ff8e..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/validator/SchemaSerDes.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.hyperledger.identus.pollux.core.model.schema.validator - -import com.networknt.schema.JsonSchema -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError.* -import zio.{IO, ZIO} -import zio.json.* -import zio.json.ast.Json -import zio.json.ast.Json.* - -class SchemaSerDes[S](jsonSchemaSchemaStr: String) { - - def initialiseJsonSchema: IO[JsonSchemaError, JsonSchema] = - JsonSchemaUtils.jsonSchema(jsonSchemaSchemaStr) - - def serializeToJsonString(instance: S)(using encoder: JsonEncoder[S]): String = { - instance.toJson - } - - def serialize(instance: S)(using encoder: JsonEncoder[S]): Either[String, Json] = { - instance.toJsonAST - } - - def deserialize( - schema: zio.json.ast.Json - )(using decoder: JsonDecoder[S]): IO[JsonSchemaError, S] = { - deserialize(schema.toString()) - } - - def deserialize( - jsonString: String - )(using decoder: JsonDecoder[S]): IO[JsonSchemaError, S] = { - for { - _ <- validate(jsonString) - anoncredSchema <- - ZIO - .fromEither(decoder.decodeJson(jsonString)) - .mapError(JsonSchemaError.JsonSchemaParsingError.apply) - } yield anoncredSchema - } - - def deserializeAsJson(jsonString: String): IO[JsonSchemaError, Json] = { - for { - _ <- validate(jsonString) - json <- - ZIO - .fromEither(jsonString.fromJson[Json]) - .mapError(JsonSchemaError.JsonSchemaParsingError.apply) - } yield json - } - - def validate(jsonString: String): IO[JsonSchemaError, Unit] = { - for { - jsonSchemaSchema <- JsonSchemaUtils.jsonSchema(jsonSchemaSchemaStr) - schemaValidator = JsonSchemaValidatorImpl(jsonSchemaSchema) - result <- schemaValidator.validate(jsonString) - } yield result - } - -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala index 9dcca15cee..68e76e8f22 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialDefinitionServiceImpl.scala @@ -16,7 +16,6 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{ InvalidURI } import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition.{Filter, FilteredEntries} import org.hyperledger.identus.pollux.core.model.secret.CredentialDefinitionSecret @@ -27,6 +26,7 @@ import org.hyperledger.identus.pollux.core.service.serdes.{ ProofKeyCredentialDefinitionSchemaSerDesV1, PublicCredentialDefinitionSerDesV1 } +import org.hyperledger.identus.shared.json.JsonSchemaError import zio.* import java.net.URI diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index 8ace946882..cd4d35ffbc 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -24,6 +24,7 @@ import org.hyperledger.identus.pollux.core.model.secret.CredentialDefinitionSecr import org.hyperledger.identus.pollux.core.model.CredentialFormat.AnonCreds import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.ProtocolState.OfferReceived import org.hyperledger.identus.pollux.core.repository.{CredentialRepository, CredentialStatusListRepository} +import org.hyperledger.identus.pollux.prex.{ClaimFormat, Jwt, PresentationDefinition} import org.hyperledger.identus.pollux.sdjwt.* import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *} import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Ed25519PublicKey, Secp256k1KeyPair} @@ -967,7 +968,7 @@ class CredentialServiceImpl( format = Some(offerFormat.name), payload = PresentationAttachment( Some(Options(challenge, domain)), - PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K"), proof_type = Nil))))) + PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K")))))) ) ) ) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala index cb7d435905..ce4b3ab333 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala @@ -17,7 +17,7 @@ import org.hyperledger.identus.pollux.core.service.serdes.{AnoncredCredentialPro import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, PresentationCompact} import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} import org.hyperledger.identus.shared.models.* -import zio._ +import zio.* import zio.json.* import java.time.Instant diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredCredentialProofsV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredCredentialProofsV1.scala index a28807b781..efc87e761c 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredCredentialProofsV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredCredentialProofsV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.service.serdes -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala index 18edcfe9e0..9092edf3c3 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.service.serdes -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationV1.scala index ccca4fa58c..1b149ad247 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/AnoncredPresentationV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.service.serdes -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PrivateCredentialDefinitionSchemaSerDesV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PrivateCredentialDefinitionSchemaSerDesV1.scala index 5bea91b7f2..03f085b1f2 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PrivateCredentialDefinitionSchemaSerDesV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PrivateCredentialDefinitionSchemaSerDesV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.service.serdes -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/ProofKeyCredentialDefinitionSchemaSerDesV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/ProofKeyCredentialDefinitionSchemaSerDesV1.scala index ee15cf065e..2c0f03b965 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/ProofKeyCredentialDefinitionSchemaSerDesV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/ProofKeyCredentialDefinitionSchemaSerDesV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.service.serdes -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PublicCredentialDefinitionSchemaSerDesV1.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PublicCredentialDefinitionSchemaSerDesV1.scala index af6e84ae26..ef238f4c8c 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PublicCredentialDefinitionSchemaSerDesV1.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/serdes/PublicCredentialDefinitionSchemaSerDesV1.scala @@ -1,6 +1,6 @@ package org.hyperledger.identus.pollux.core.service.serdes -import org.hyperledger.identus.pollux.core.model.schema.validator.SchemaSerDes +import org.hyperledger.identus.shared.json.SchemaSerDes import zio.* import zio.json.* diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachmentSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachmentSpec.scala index 52109d1a8c..b4e77143e7 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachmentSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/presentation/PresentationAttachmentSpec.scala @@ -4,6 +4,7 @@ import io.circe.parser.* import io.circe.syntax.* import io.circe.Json import munit.* +import org.hyperledger.identus.pollux.prex.* class PresentationAttachmentSpec extends ZSuite { diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/AnoncredSchemaTypeSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/AnoncredSchemaTypeSpec.scala index 6d8d3fd686..9b3be99fbd 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/AnoncredSchemaTypeSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/AnoncredSchemaTypeSpec.scala @@ -1,7 +1,7 @@ package org.hyperledger.identus.pollux.core.model.schema import org.hyperledger.identus.pollux.core.model.schema.`type`.AnoncredSchemaType -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError +import org.hyperledger.identus.shared.json.JsonSchemaError import zio.* import zio.json.* import zio.json.ast.Json diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala index 0d10160e21..341565de22 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchemaSpec.scala @@ -4,8 +4,8 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.CredentialSchemaValidationError import org.hyperledger.identus.pollux.core.model.schema.`type`.{AnoncredSchemaType, CredentialJsonSchemaType} import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 -import org.hyperledger.identus.pollux.core.model.schema.validator.JsonSchemaError.JsonValidationErrors import org.hyperledger.identus.pollux.core.model.schema.AnoncredSchemaTypeSpec.test +import org.hyperledger.identus.shared.json.JsonSchemaError.JsonValidationErrors import zio.json.* import zio.json.ast.Json import zio.json.ast.Json.* diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index 8eb43c7353..e8d9fcf907 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -9,12 +9,13 @@ import org.hyperledger.identus.castor.core.service.DIDService import org.hyperledger.identus.mercury.model.{AttachmentDescriptor, DidId} import org.hyperledger.identus.mercury.protocol.issuecredential.* import org.hyperledger.identus.pollux.core.model.* -import org.hyperledger.identus.pollux.core.model.presentation.{ClaimFormat, Ldp, Options, PresentationDefinition} +import org.hyperledger.identus.pollux.core.model.presentation.Options import org.hyperledger.identus.pollux.core.repository.{ CredentialDefinitionRepositoryInMemory, CredentialRepositoryInMemory, CredentialStatusListRepositoryInMemory } +import org.hyperledger.identus.pollux.prex.{ClaimFormat, Ldp, PresentationDefinition} import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinition.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinition.scala new file mode 100644 index 0000000000..5789c214c0 --- /dev/null +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinition.scala @@ -0,0 +1,100 @@ +package org.hyperledger.identus.pollux.prex + +import com.networknt.schema.{JsonSchema, SpecVersion} +import io.circe.* +import io.circe.generic.semiauto.* +import io.circe.Json as CirceJson +import org.hyperledger.identus.shared.json.{JsonInterop, JsonSchemaError, JsonSchemaUtils} +import zio.* +import zio.json.ast.Json as ZioJson + +// TODO: define proper type +type JsonPath = String + +opaque type FieldFilter = ZioJson + +object FieldFilter { + given Encoder[FieldFilter] = Encoder.encodeJson.contramap(JsonInterop.toCirceJsonAst) + given Decoder[FieldFilter] = Decoder.decodeJson.map(JsonInterop.toZioJsonAst) + + extension (f: FieldFilter) + def asJsonZio: ZioJson = f + def asJsonCirce: CirceJson = JsonInterop.toCirceJsonAst(f) + + // Json schema draft 7 must be used + // https://identity.foundation/presentation-exchange/spec/v2.1.1/#json-schema + def toJsonSchema: IO[JsonSchemaError, JsonSchema] = + JsonSchemaUtils.jsonSchemaAtVersion(f.toString(), SpecVersion.VersionFlag.V7) +} + +case class Field( + id: Option[String] = None, + path: Seq[JsonPath] = Seq.empty, + name: Option[String] = None, + purpose: Option[String] = None, + filter: Option[FieldFilter] = None +) + +object Field { + given Encoder[Field] = deriveEncoder[Field] + given Decoder[Field] = deriveDecoder[Field] +} + +case class Jwt(alg: Seq[String]) + +object Jwt { + given Encoder[Jwt] = deriveEncoder[Jwt] + given Decoder[Jwt] = deriveDecoder[Jwt] +} + +case class Ldp(proof_type: Seq[String]) + +object Ldp { + given Encoder[Ldp] = deriveEncoder[Ldp] + given Decoder[Ldp] = deriveDecoder[Ldp] +} + +case class ClaimFormat(jwt: Option[Jwt] = None, ldp: Option[Ldp] = None) + +object ClaimFormat { + given Encoder[ClaimFormat] = deriveEncoder[ClaimFormat] + given Decoder[ClaimFormat] = deriveDecoder[ClaimFormat] +} + +case class Constraints(fields: Option[Seq[Field]]) + +object Constraints { + given Encoder[Constraints] = deriveEncoder[Constraints] + given Decoder[Constraints] = deriveDecoder[Constraints] +} + +/** Refer to Input Descriptors + */ +case class InputDescriptor( + id: String = java.util.UUID.randomUUID.toString(), + name: Option[String] = None, + purpose: Option[String] = None, + format: Option[ClaimFormat] = None, + constraints: Constraints +) + +object InputDescriptor { + given Encoder[InputDescriptor] = deriveEncoder[InputDescriptor] + given Decoder[InputDescriptor] = deriveDecoder[InputDescriptor] +} + +/** Refer to Presentation + * Definition + */ +case class PresentationDefinition( + id: String = java.util.UUID.randomUUID.toString(), // UUID + input_descriptors: Seq[InputDescriptor] = Seq.empty, + name: Option[String] = None, + purpose: Option[String] = None, + format: Option[ClaimFormat] = None +) + +object PresentationDefinition { + given Encoder[PresentationDefinition] = deriveEncoder[PresentationDefinition] + given Decoder[PresentationDefinition] = deriveDecoder[PresentationDefinition] +} diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidator.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidator.scala new file mode 100644 index 0000000000..3160d373dc --- /dev/null +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidator.scala @@ -0,0 +1,88 @@ +package org.hyperledger.identus.pollux.prex + +import org.hyperledger.identus.pollux.prex.PresentationDefinitionError.{ + InvalidFilterJsonSchema, + JsonSchemaOptionNotSupported +} +import org.hyperledger.identus.shared.json.{JsonSchemaError, JsonSchemaValidator, JsonSchemaValidatorImpl} +import org.hyperledger.identus.shared.models.{Failure, StatusCode} +import zio.* + +import scala.jdk.CollectionConverters.* + +sealed trait PresentationDefinitionError extends Failure { + override def namespace: String = "PresentationDefinitionError" +} + +object PresentationDefinitionError { + final case class InvalidFilterJsonSchema(json: String, error: JsonSchemaError) extends PresentationDefinitionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = + s"PresentationDefinition input_descriptors filter '$json' is not a valid JsonSchema Draft 7" + } + + final case class JsonSchemaOptionNotSupported(invalidKeys: Set[String], allowedKeys: Set[String]) + extends PresentationDefinitionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = { + val invalidKeysStr = invalidKeys.mkString(", ") + val allowedKeysStr = allowedKeys.mkString(", ") + s"PresentationDefinition input_descriptors filter json schema contains unsupported keys: $invalidKeysStr. Supported keys are: $allowedKeysStr" + } + } +} + +trait PresentationDefinitionValidator { + def validate(pd: PresentationDefinition): IO[PresentationDefinitionError, Unit] +} + +object PresentationDefinitionValidatorImpl { + def layer: Layer[JsonSchemaError, PresentationDefinitionValidator] = + ZLayer.scoped { + JsonSchemaValidatorImpl.draft7Meta + .map(PresentationDefinitionValidatorImpl(_)) + } +} + +class PresentationDefinitionValidatorImpl(filterSchemaValidator: JsonSchemaValidator) + extends PresentationDefinitionValidator { + override def validate(pd: PresentationDefinition): IO[PresentationDefinitionError, Unit] = { + val filters = pd.input_descriptors + .flatMap(_.constraints.fields) + .flatten + .flatMap(_.filter) + + for { + _ <- validateFilters(filters) + _ <- validateAllowedFilterSchemaKeys(filters) + } yield () + } + + // while we use full-blown json-schema library, we limit the schema optiton + // to make sure verfier don't go crazy on schema causing problem with holder interoperability + // see SDK supported keys https://github.com/hyperledger/identus-edge-agent-sdk-ts/blob/da27890ad4ff3d32576bda8bc99a1185e7239a4c/src/domain/models/VerifiableCredential.ts#L120 + private def validateAllowedFilterSchemaKeys(filters: Seq[FieldFilter]): IO[PresentationDefinitionError, Unit] = { + val allowedSchemaKeys = Set("type", "pattern", "enum", "const", "value") + ZIO + .foreach(filters) { filter => + val schemaKeys = filter.asJsonZio.asObject.fold(Seq.empty)(_.keys.toSeq) + val invalidKeys = schemaKeys.filterNot(allowedSchemaKeys.contains) + + if invalidKeys.isEmpty + then ZIO.unit + else ZIO.fail(JsonSchemaOptionNotSupported(invalidKeys.toSet, allowedSchemaKeys)) + } + .unit + } + + private def validateFilters(filters: Seq[FieldFilter]): IO[PresentationDefinitionError, Unit] = + ZIO + .foreach(filters) { filter => + val json = filter.asJsonZio.toString() + filterSchemaValidator + .validate(json) + .mapError(InvalidFilterJsonSchema(json, _)) + } + .unit + +} diff --git a/pollux/prex/src/test/resources/pd/filter_by_cred_type.json b/pollux/prex/src/test/resources/pd/filter_by_cred_type.json new file mode 100644 index 0000000000..bb7fee3ea0 --- /dev/null +++ b/pollux/prex/src/test/resources/pd/filter_by_cred_type.json @@ -0,0 +1,22 @@ +{ + "presentation_definition": { + "id": "first simple example", + "input_descriptors": [ + { + "id": "A specific type of VC", + "name": "A specific type of VC", + "purpose": "We want a VC of this type", + "constraints": { + "fields": [ + { + "path": ["$.type"], + "filter": { + "type": "array" + } + } + ] + } + } + ] + } +} diff --git a/pollux/prex/src/test/resources/pd/minimal_example.json b/pollux/prex/src/test/resources/pd/minimal_example.json new file mode 100644 index 0000000000..801d2a99cb --- /dev/null +++ b/pollux/prex/src/test/resources/pd/minimal_example.json @@ -0,0 +1,25 @@ +{ + "comment": "Note: VP, OIDC, DIDComm, or CHAPI outer wrapper would be here.", + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "input_descriptors": [ + { + "id": "wa_driver_license", + "name": "Washington State Business License", + "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.dateOfBirth", + "$.credentialSubject.dob", + "$.vc.credentialSubject.dateOfBirth", + "$.vc.credentialSubject.dob" + ] + } + ] + } + } + ] + } +} diff --git a/pollux/prex/src/test/resources/pd/single_group.json b/pollux/prex/src/test/resources/pd/single_group.json new file mode 100644 index 0000000000..3a140f3650 --- /dev/null +++ b/pollux/prex/src/test/resources/pd/single_group.json @@ -0,0 +1,76 @@ +{ + "comment": "VP, OIDC, DIDComm, or CHAPI outer wrapper here", + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "submission_requirements": [ + { + "name": "Citizenship Information", + "rule": "pick", + "count": 1, + "from": "A" + } + ], + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.credentialSchema.id", "$.vc.credentialSchema.id"], + "filter": { + "type": "string", + "const": "https://eu.com/claims/DriversLicense.json" + } + }, + { + "path": ["$.issuer", "$.vc.issuer", "$.iss"], + "purpose": "We can only accept digital driver's licenses issued by national authorities of member states or trusted notarial auditors.", + "filter": { + "type": "string", + "pattern": "^did:example:gov1$|^did:example:gov2$" + } + }, + { + "path": [ + "$.credentialSubject.dob", + "$.vc.credentialSubject.dob", + "$.dob" + ], + "filter": { + "type": "string" + } + } + ] + } + }, + { + "id": "citizenship_input_2", + "name": "US Passport", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.credentialSchema.id", "$.vc.credentialSchema.id"], + "filter": { + "type": "string", + "const": "hub://did:foo:123/Collections/schema.us.gov/passport.json" + } + }, + { + "path": [ + "$.credentialSubject.birth_date", + "$.vc.credentialSubject.birth_date", + "$.birth_date" + ], + "filter": { + "type": "string" + } + } + ] + } + } + ] + } +} diff --git a/pollux/prex/src/test/resources/pd/two_filters_simplified.json b/pollux/prex/src/test/resources/pd/two_filters_simplified.json new file mode 100644 index 0000000000..5c6b24a768 --- /dev/null +++ b/pollux/prex/src/test/resources/pd/two_filters_simplified.json @@ -0,0 +1,37 @@ +{ + "presentation_definition": { + "id": "Scalable trust example", + "input_descriptors": [ + { + "id": "any type of credit card from any bank", + "name": "any type of credit card from any bank", + "purpose": "Please provide your credit card details", + "constraints": { + "fields": [ + { + "path": ["$.termsOfUse.type"], + "filter": { + "type": "string", + "pattern": "^https://train.trust-scheme.de/info$" + } + }, + { + "path": ["$.termsOfUse.trustScheme"], + "filter": { + "type": "string", + "pattern": "^worldbankfederation.com$" + } + }, + { + "path": ["$.type"], + "filter": { + "type": "string", + "pattern": "^creditCard$" + } + } + ] + } + } + ] + } +} diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidatorSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidatorSpec.scala new file mode 100644 index 0000000000..bd57c15230 --- /dev/null +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidatorSpec.scala @@ -0,0 +1,115 @@ +package org.hyperledger.identus.pollux.prex + +import io.circe.* +import io.circe.generic.auto.* +import io.circe.parser.* +import org.hyperledger.identus.pollux.prex.PresentationDefinitionError.{ + InvalidFilterJsonSchema, + JsonSchemaOptionNotSupported +} +import zio.* +import zio.test.* +import zio.test.Assertion.* + +import scala.io.Source +import scala.util.Using + +object PresentationDefinitionValidatorSpec extends ZIOSpecDefault { + + final case class ExampleTransportEnvelope(presentation_definition: PresentationDefinition) + + override def spec = suite("PresentationDefinitionValidatorSpec")( + test("accept presentation-definition examples from spec") { + val resourcePaths = Seq( + "pd/minimal_example.json", + "pd/filter_by_cred_type.json", + "pd/two_filters_simplified.json", + "pd/single_group.json", + ) + ZIO + .foreach(resourcePaths) { path => + for { + validator <- ZIO.service[PresentationDefinitionValidator] + _ <- ZIO + .fromTry(Using(Source.fromResource(path))(_.mkString)) + .flatMap(json => ZIO.fromEither(decode[ExampleTransportEnvelope](json))) + .map(_.presentation_definition) + .flatMap(validator.validate) + } yield () + } + .as(assertCompletes) + }, + test("reject when filter is invalid json-schema") { + val pdJson = + """{ + | "presentation_definition": { + | "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + | "input_descriptors": [ + | { + | "id": "wa_driver_license", + | "name": "Washington State Business License", + | "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", + | "constraints": { + | "fields": [ + | { + | "path": ["$.credentialSubject.dateOfBirth"], + | "filter": {"type": 123} + | } + | ] + | } + | } + | ] + | } + |} + """.stripMargin + + for { + validator <- ZIO.service[PresentationDefinitionValidator] + pd <- ZIO + .fromEither(decode[ExampleTransportEnvelope](pdJson)) + .map(_.presentation_definition) + filters <- ZIO + .succeed(pd.input_descriptors.flatMap(_.constraints.fields.getOrElse(Nil))) + .map(_.flatMap(_.filter)) + exit <- validator.validate(pd).exit + } yield assert(exit)(failsWithA[InvalidFilterJsonSchema]) + }, + test("reject when filter is valid but use disallowed filter property") { + val pdJson = + """{ + | "presentation_definition": { + | "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + | "input_descriptors": [ + | { + | "id": "wa_driver_license", + | "name": "Washington State Business License", + | "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", + | "constraints": { + | "fields": [ + | { + | "path": ["$.credentialSubject.dateOfBirth"], + | "filter": {"type": "string", "format": "date-time"} + | } + | ] + | } + | } + | ] + | } + |} + """.stripMargin + + for { + validator <- ZIO.service[PresentationDefinitionValidator] + pd <- ZIO + .fromEither(decode[ExampleTransportEnvelope](pdJson)) + .map(_.presentation_definition) + filters <- ZIO + .succeed(pd.input_descriptors.flatMap(_.constraints.fields.getOrElse(Nil))) + .map(_.flatMap(_.filter)) + exit <- validator.validate(pd).exit + } yield assert(exit)(failsWithA[JsonSchemaOptionNotSupported]) + } + ) + .provide(PresentationDefinitionValidatorImpl.layer) + +} diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala index 8bf6ab2086..ca27945182 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala @@ -7,7 +7,8 @@ import com.nimbusds.jwt.SignedJWT import io.circe.* import io.circe.syntax.* import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Ed25519PublicKey, KmpEd25519KeyOps} -import org.hyperledger.identus.shared.utils.{Base64Utils, Json as JsonUtils} +import org.hyperledger.identus.shared.json.Json as JsonUtils +import org.hyperledger.identus.shared.utils.Base64Utils import scodec.bits.ByteVector import zio.* diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index 9a670fbb6a..64ebc7902d 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -8,7 +8,7 @@ import io.circe.parser.decode import io.circe.syntax.* import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString -import org.hyperledger.identus.shared.crypto.{KmpSecp256k1KeyOps, PublicKey as ApolloPublicKey} +import org.hyperledger.identus.shared.crypto.KmpSecp256k1KeyOps import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.utils.Base64Utils import pdi.jwt.* diff --git a/project/LicenseReport.scala b/project/LicenseReport.scala index 470172628b..403fa77d96 100644 --- a/project/LicenseReport.scala +++ b/project/LicenseReport.scala @@ -1,7 +1,7 @@ -import sbt.Logger +import sbt.internal.librarymanagement.{IvyRetrieve, IvySbt} import sbt.librarymanagement._ import sbt.librarymanagement.ivy._ -import sbt.internal.librarymanagement.{IvySbt, IvyRetrieve} +import sbt.Logger // Since ivy fails to resolve project dependencies, customized version is used to ignore any failure. // This is OK as we only grab license information from the resolution metadata, diff --git a/shared/json/src/main/resources/json-schema/draft-07.json b/shared/json/src/main/resources/json-schema/draft-07.json new file mode 100644 index 0000000000..8875d422c2 --- /dev/null +++ b/shared/json/src/main/resources/json-schema/draft-07.json @@ -0,0 +1,166 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/schemaArray" }], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/stringArray" }] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/utils/Json.scala b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/Json.scala similarity index 97% rename from shared/core/src/main/scala/org/hyperledger/identus/shared/utils/Json.scala rename to shared/json/src/main/scala/org/hyperledger/identus/shared/json/Json.scala index ecbc60f101..40a8a85a21 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/utils/Json.scala +++ b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/Json.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.shared.utils +package org.hyperledger.identus.shared.json import com.apicatalog.jsonld.document.JsonDocument import com.apicatalog.jsonld.http.media.MediaType diff --git a/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonInterop.scala b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonInterop.scala new file mode 100644 index 0000000000..bd6e41ab51 --- /dev/null +++ b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonInterop.scala @@ -0,0 +1,25 @@ +package org.hyperledger.identus.shared.json + +import io.circe.Json as CirceJson +import zio.json.* +import zio.json.ast.Json as ZioJson + +object JsonInterop { + def toZioJsonAst(circeJson: CirceJson): ZioJson = { + val encoded = circeJson.noSpaces + encoded.fromJson[ZioJson] match { + case Left(failure) => + throw Exception(s"Circe and Zio Json interop fail. Unable to convert from Circe to Zio AST. $failure") + case Right(value) => value + } + } + + def toCirceJsonAst(zioJson: ZioJson): CirceJson = { + val encoded = zioJson.toJson + io.circe.parser.parse(encoded).left.map(_.toString) match { + case Left(failure) => + throw Exception(s"Circe and Zio Json interop fail. Unable to convert from Zio to Circe AST. $failure") + case Right(value) => value + } + } +} diff --git a/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonSchema.scala b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonSchema.scala new file mode 100644 index 0000000000..299e6ce50c --- /dev/null +++ b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonSchema.scala @@ -0,0 +1,189 @@ +package org.hyperledger.identus.shared.json + +import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} +import com.networknt.schema.* +import com.networknt.schema.SpecVersion.VersionFlag +import org.hyperledger.identus.shared.json.JsonSchemaError.{ + JsonSchemaParsingError, + UnexpectedError, + UnsupportedJsonSchemaSpecVersion +} +import zio.* +import zio.json.* +import zio.json.ast.Json + +import scala.io.Source + +sealed trait JsonSchemaError { + def error: String +} + +object JsonSchemaError { + case class JsonSchemaParsingError(error: String) extends JsonSchemaError + + case class JsonValidationErrors(errors: Seq[String]) extends JsonSchemaError { + def error: String = errors.mkString(";") + } + + case class UnsupportedJsonSchemaSpecVersion(error: String) extends JsonSchemaError + + case class UnexpectedError(error: String) extends JsonSchemaError +} + +trait JsonSchemaValidator { + def validate(claims: String): IO[JsonSchemaError, Unit] + + def validate(claimsJsonNode: JsonNode): IO[JsonSchemaError, Unit] = { + validate(claimsJsonNode.toString) + } +} + +case class JsonSchemaValidatorImpl(schemaValidator: JsonSchema) extends JsonSchemaValidator { + override def validate(jsonString: String): IO[JsonSchemaError, Unit] = { + import scala.jdk.CollectionConverters.* + for { + // Convert claims to JsonNode + jsonClaims <- JsonSchemaUtils.toJsonNode(jsonString) + + // Validate claims JsonNode + validationMessages <- ZIO + .attempt(schemaValidator.validate(jsonClaims).asScala.toSeq) + .mapError(t => JsonSchemaError.JsonValidationErrors(Seq(t.getMessage))) + + validationResult <- + if (validationMessages.isEmpty) ZIO.unit + else ZIO.fail(JsonSchemaError.JsonValidationErrors(validationMessages.map(_.getMessage))) + } yield validationResult + } + +} + +object JsonSchemaValidatorImpl { + def from(schema: Json): IO[JsonSchemaError, JsonSchemaValidator] = { + for { + jsonSchema <- JsonSchemaUtils.from(schema, IndexedSeq(SpecVersion.VersionFlag.V202012)) + } yield JsonSchemaValidatorImpl(jsonSchema) + } + + def draft7Meta: ZIO[Scope, JsonSchemaError, JsonSchemaValidator] = + ZIO + .acquireRelease { + ZIO + .attempt(Source.fromResource("json-schema/draft-07.json")) + .mapError(e => UnexpectedError(e.getMessage)) + }(src => ZIO.attempt(src).orDie) + .map(_.mkString) + .flatMap(schema => JsonSchemaUtils.jsonSchemaAtVersion(schema, SpecVersion.VersionFlag.V7)) + .map(JsonSchemaValidatorImpl(_)) +} + +object JsonSchemaUtils { + + /** Create a json schema where the version is inferred from $schema field with optional supported version check */ + def jsonSchema( + schema: String, + supportedVersions: IndexedSeq[VersionFlag] = IndexedSeq.empty + ): IO[JsonSchemaError, JsonSchema] = { + for { + jsonSchemaNode <- toJsonNode(schema) + specVersion <- ZIO + .attempt(SpecVersionDetector.detect(jsonSchemaNode)) + .mapError(t => UnexpectedError(t.getMessage)) + _ <- + if (supportedVersions.nonEmpty && !supportedVersions.contains(specVersion)) + ZIO.fail( + UnsupportedJsonSchemaSpecVersion( + s"Unsupported JsonSchemaVersion. Current:$specVersion ExpectedOneOf:${supportedVersions.map(_.getId)}" + ) + ) + else ZIO.unit + jsonSchema <- jsonSchemaAtVersion(schema, specVersion) + } yield jsonSchema + } + + /** Create a json schema at specific version */ + def jsonSchemaAtVersion( + schema: String, + specVersion: VersionFlag + ): IO[JsonSchemaError, JsonSchema] = { + for { + jsonSchemaNode <- toJsonNode(schema) + mapper <- ZIO.attempt(new ObjectMapper()).mapError(t => UnexpectedError(t.getMessage)) + factory <- ZIO + .attempt(JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(specVersion)).jsonMapper(mapper).build) + .mapError(t => UnexpectedError(t.getMessage)) + jsonSchema <- ZIO.attempt(factory.getSchema(jsonSchemaNode)).mapError(t => UnexpectedError(t.getMessage)) + } yield jsonSchema + } + + def from( + schema: Json, + supportedVersions: IndexedSeq[VersionFlag] = IndexedSeq.empty + ): IO[JsonSchemaError, JsonSchema] = { + jsonSchema(schema.toString(), supportedVersions) + } + + def toJsonNode(json: Json): IO[JsonSchemaError, JsonNode] = { + toJsonNode(json.toString()) + } + + def toJsonNode(json: String): IO[JsonSchemaError, JsonNode] = { + for { + mapper <- ZIO.attempt(new ObjectMapper()).mapError(t => UnexpectedError(t.getMessage)) + jsonSchemaNode <- ZIO + .attempt(mapper.readTree(json)) + .mapError(t => JsonSchemaParsingError(t.getMessage)) + } yield jsonSchemaNode + } +} + +class SchemaSerDes[S](jsonSchemaSchemaStr: String) { + + def initialiseJsonSchema: IO[JsonSchemaError, JsonSchema] = + JsonSchemaUtils.jsonSchema(jsonSchemaSchemaStr) + + def serializeToJsonString(instance: S)(using encoder: JsonEncoder[S]): String = { + instance.toJson + } + + def serialize(instance: S)(using encoder: JsonEncoder[S]): Either[String, Json] = { + instance.toJsonAST + } + + def deserialize( + schema: zio.json.ast.Json + )(using decoder: JsonDecoder[S]): IO[JsonSchemaError, S] = { + deserialize(schema.toString()) + } + + def deserialize( + jsonString: String + )(using decoder: JsonDecoder[S]): IO[JsonSchemaError, S] = { + for { + _ <- validate(jsonString) + schema <- + ZIO + .fromEither(decoder.decodeJson(jsonString)) + .mapError(JsonSchemaError.JsonSchemaParsingError.apply) + } yield schema + } + + def deserializeAsJson(jsonString: String): IO[JsonSchemaError, Json] = { + for { + _ <- validate(jsonString) + json <- + ZIO + .fromEither(jsonString.fromJson[Json]) + .mapError(JsonSchemaError.JsonSchemaParsingError.apply) + } yield json + } + + def validate(jsonString: String): IO[JsonSchemaError, Unit] = { + for { + jsonSchemaSchema <- JsonSchemaUtils.jsonSchema(jsonSchemaSchemaStr) + schemaValidator = JsonSchemaValidatorImpl(jsonSchemaSchema) + result <- schemaValidator.validate(jsonString) + } yield result + } + +}