From b3cf828d001f7499c414e9dc559f5152997445e6 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev - IOHK <102033808+yshyn-iohk@users.noreply.github.com> Date: Tue, 22 Nov 2022 18:07:12 +0700 Subject: [PATCH] feat(apollo): add schema registry to the agent using Tapir library. ATL-1334 (#94) * [ATL-2014] build: External registry set to domain name * feat(pollux): implement schema-registry in-memory using Tapir Co-authored-by: David --- infrastructure/local/haproxy/haproxy.cfg | 8 +++ prism-agent/service/build.sbt | 2 +- .../service/project/Dependencies.scala | 42 +++++++++--- prism-agent/service/project/build.properties | 2 +- prism-agent/service/project/build.sbt | 1 + .../src/main/resources/application.conf | 9 +++ .../io/iohk/atala/agent/server/Main.scala | 8 ++- .../io/iohk/atala/agent/server/Modules.scala | 19 +++++- .../atala/agent/server/config/AppConfig.scala | 9 ++- .../agent/server/http/ZHttpEndpoints.scala | 24 +++++++ .../atala/agent/server/http/ZHttpServer.scala | 45 +++++++++++++ .../iohk/atala/api/http/FailureResponse.scala | 39 +++++++++++ .../io/iohk/atala/pollux/schema/Models.scala | 66 +++++++++++++++++++ .../schema/SchemaRegistryEndpoints.scala | 66 +++++++++++++++++++ .../SchemaRegistryServerEndpoints.scala | 56 ++++++++++++++++ .../service/SchemaRegistryService.scala | 21 ++++++ .../SchemaRegistryServiceInMemory.scala | 31 +++++++++ 17 files changed, 434 insertions(+), 14 deletions(-) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpServer.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala diff --git a/infrastructure/local/haproxy/haproxy.cfg b/infrastructure/local/haproxy/haproxy.cfg index d56754eda1..9437b6acb0 100644 --- a/infrastructure/local/haproxy/haproxy.cfg +++ b/infrastructure/local/haproxy/haproxy.cfg @@ -27,6 +27,7 @@ frontend https-in use_backend mediator if { path_beg -i /mediator } use_backend swagger-ui if { path_beg -i /apidocs } use_backend prism-agent if { path_beg -i /prism-agent } + use_backend prism-agent-tapir if { path_beg -i /tapir } backend mediator balance roundrobin @@ -42,6 +43,13 @@ backend prism-agent option forwardfor server s1 prism-agent:8080 maxconn 32 +backend prism-agent-tapir + balance roundrobin + http-request set-uri %[url,regsub(^/tapir,,)] if { path_beg /tapir } + option httpclose + option forwardfor + server s1 prism-agent:8085 maxconn 32 + backend swagger-ui balance roundrobin option httpclose diff --git a/prism-agent/service/build.sbt b/prism-agent/service/build.sbt index 91577c74f2..8398e39dfd 100644 --- a/prism-agent/service/build.sbt +++ b/prism-agent/service/build.sbt @@ -60,7 +60,7 @@ lazy val server = project Docker / dockerUsername := Some("input-output-hk"), Docker / githubOwner := "atala-prism-building-blocks", Docker / dockerRepository := Some("ghcr.io"), - dockerExposedPorts := Seq(8080), + dockerExposedPorts := Seq(8080, 8085), dockerBaseImage := "openjdk:11" ) .enablePlugins(OpenApiGeneratorPlugin, JavaAppPackaging, DockerPlugin) diff --git a/prism-agent/service/project/Dependencies.scala b/prism-agent/service/project/Dependencies.scala index 24ad03293c..f7ca91b73a 100644 --- a/prism-agent/service/project/Dependencies.scala +++ b/prism-agent/service/project/Dependencies.scala @@ -5,6 +5,7 @@ object Dependencies { val zio = "2.0.2" val zioConfig = "3.0.2" val zioHttp = "2.0.0-RC11" + val zioInteropCats = "3.3.0" val akka = "2.6.20" val akkaHttp = "10.2.9" val castor = "0.2.0" @@ -12,12 +13,16 @@ object Dependencies { val bouncyCastle = "1.70" val logback = "1.4.4" val mercury = "0.6.0" + val zioJson = "0.3.0" + val tapir = "1.2.2" } private lazy val zio = "dev.zio" %% "zio" % Versions.zio private lazy val zioConfig = "dev.zio" %% "zio-config" % Versions.zioConfig private lazy val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % Versions.zioConfig private lazy val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % Versions.zioConfig + private lazy val zioJson = "dev.zio" %% "zio-json" % Versions.zioJson + private lazy val zioInteropCats = "dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats private lazy val zioTest = "dev.zio" %% "zio-test" % Versions.zio % Test private lazy val zioTestSbt = "dev.zio" %% "zio-test-sbt" % Versions.zio % Test @@ -45,26 +50,47 @@ object Dependencies { private lazy val logback = "ch.qos.logback" % "logback-classic" % Versions.logback + private lazy val tapirSwaggerUiBundle = "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % Versions.tapir + private lazy val tapirJsonZio = "com.softwaremill.sttp.tapir" %% "tapir-json-zio" % Versions.tapir + + private lazy val tapirZioHttpServer = "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % Versions.tapir + private lazy val tapirHttp4sServerZio = "com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % Versions.tapir + private lazy val http4sBlazeServer = "org.http4s" %% "http4s-blaze-server" % "0.23.12" + + private lazy val tapirRedocBundle = "com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % Versions.tapir + private lazy val tapirSttpStubServer = "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % Versions.tapir % Test + + // Dependency Modules - private lazy val baseDependencies: Seq[ModuleID] = - Seq(zio, zioTest, zioTestSbt, zioTestMagnolia, zioConfig, zioConfigMagnolia, zioConfigTypesafe) + private lazy val baseDependencies: Seq[ModuleID] = Seq(zio, zioTest, zioTestSbt, zioTestMagnolia, zioConfig, zioConfigMagnolia, zioConfigTypesafe, zioJson, logback, zioHttp) private lazy val castorDependencies: Seq[ModuleID] = Seq(castorCore, castorSqlDoobie) private lazy val polluxDependencies: Seq[ModuleID] = Seq(polluxCore, polluxSqlDoobie) private lazy val mercuryDependencies: Seq[ModuleID] = Seq(mercuryAgent) private lazy val akkaHttpDependencies: Seq[ModuleID] = Seq(akkaTyped, akkaStream, akkaHttp, akkaSprayJson).map(_.cross(CrossVersion.for3Use2_13)) private lazy val bouncyDependencies: Seq[ModuleID] = Seq(bouncyBcpkix, bouncyBcprov) + private lazy val tapirDependencies: Seq[ModuleID] = + Seq( + tapirSwaggerUiBundle, + tapirJsonZio, + tapirRedocBundle, + tapirSttpStubServer, + tapirZioHttpServer, + tapirHttp4sServerZio, + http4sBlazeServer) + // Project Dependencies - lazy val keyManagementDependencies: Seq[ModuleID] = baseDependencies ++ castorDependencies ++ bouncyDependencies + lazy val keyManagementDependencies: Seq[ModuleID] = + baseDependencies ++ + castorDependencies ++ + bouncyDependencies + lazy val serverDependencies: Seq[ModuleID] = - baseDependencies ++ + baseDependencies ++ akkaHttpDependencies ++ castorDependencies ++ polluxDependencies ++ mercuryDependencies ++ - Seq( - zioHttp, - logback - ) + tapirDependencies } diff --git a/prism-agent/service/project/build.properties b/prism-agent/service/project/build.properties index 22af2628c4..563a014da4 100644 --- a/prism-agent/service/project/build.properties +++ b/prism-agent/service/project/build.properties @@ -1 +1 @@ -sbt.version=1.7.1 +sbt.version=1.7.2 diff --git a/prism-agent/service/project/build.sbt b/prism-agent/service/project/build.sbt index 73b9bc70cf..6e5ff1dec0 100644 --- a/prism-agent/service/project/build.sbt +++ b/prism-agent/service/project/build.sbt @@ -2,5 +2,6 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.3") addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.9") addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") +addDependencyTreePlugin libraryDependencies ++= Seq("org.openapitools" % "openapi-generator" % "6.0.0") diff --git a/prism-agent/service/server/src/main/resources/application.conf b/prism-agent/service/server/src/main/resources/application.conf index 453d2559ba..8b9715cbf9 100644 --- a/prism-agent/service/server/src/main/resources/application.conf +++ b/prism-agent/service/server/src/main/resources/application.conf @@ -35,4 +35,13 @@ pollux { password = "postgres" password = ${?POLLUX_DB_PASSWORD} } +} + +agent { + httpEndpoint { + http { + port = 8085 + port =${?AGENT_HTTP_PORT} + } + } } \ No newline at end of file diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index febc0bfad1..f03d171290 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -75,7 +75,13 @@ object Main extends ZIOAppDefault { .debug .fork - _ <- Modules.app(restServicePort).provide(didCommLayer) + _ <- Modules + .app(restServicePort) + .provide(didCommLayer) + .fork + + _ <- Modules.zioApp.fork + _ <- ZIO.never } yield () } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index 0bf536704c..18ea3aa94a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -6,7 +6,7 @@ import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors import akka.http.scaladsl.server.Route import doobie.util.transactor.Transactor -import io.iohk.atala.agent.server.http.{HttpRoutes, HttpServer} +import io.iohk.atala.agent.server.http.{HttpRoutes, HttpServer, ZHttp4sBlazeServer, ZHttpEndpoints} import io.iohk.atala.castor.core.service.{DIDService, DIDServiceImpl} import io.iohk.atala.agent.server.http.marshaller.{ DIDApiMarshallerImpl, @@ -73,6 +73,9 @@ import io.iohk.atala.mercury.protocol.issuecredential._ import io.iohk.atala.pollux.core.model.error.IssueCredentialError import io.iohk.atala.pollux.core.model.error.IssueCredentialError.RepositoryError import java.io.IOException +import cats.implicits.* +import io.iohk.atala.pollux.schema.SchemaRegistryServerEndpoints +import io.iohk.atala.pollux.service.SchemaRegistryServiceInMemory object Modules { @@ -84,6 +87,19 @@ object Modules { .unit } + lazy val zioApp = { + val zioHttpServerApp = for { + allSchemaRegistryEndpoints <- SchemaRegistryServerEndpoints.all + allEndpoints = ZHttpEndpoints.withDocumentations[Task](allSchemaRegistryEndpoints) + appConfig <- ZIO.service[AppConfig] + httpServer <- ZHttp4sBlazeServer.start(allEndpoints, port = appConfig.agent.httpEndpoint.http.port) + } yield httpServer + + zioHttpServerApp + .provideLayer(SchemaRegistryServiceInMemory.layer ++ SystemModule.configLayer) + .unit + } + def didCommServiceEndpoint(port: Int) = { val header = "content-type" -> MediaTypes.contentTypeEncrypted val app: HttpApp[DidComm with CredentialService, Throwable] = @@ -209,7 +225,6 @@ object Modules { } } - object SystemModule { val actorSystemLayer: TaskLayer[ActorSystem[Nothing]] = ZLayer.scoped( ZIO.acquireRelease( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala index 2f6ddca592..57db4989e1 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala @@ -6,7 +6,8 @@ import zio.config.magnolia.Descriptor final case class AppConfig( iris: IrisConfig, castor: CastorConfig, - pollux: PolluxConfig + pollux: PolluxConfig, + agent: AgentConfig ) object AppConfig { @@ -21,3 +22,9 @@ final case class PolluxConfig(database: DatabaseConfig) final case class GrpcServiceConfig(host: String, port: Int) final case class DatabaseConfig(host: String, port: Int, databaseName: String, username: String, password: String) + +final case class AgentConfig(httpEndpoint: HttpEndpointConfig) + +final case class HttpEndpointConfig(http: HttpConfig) + +final case class HttpConfig(port: Int) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala new file mode 100644 index 0000000000..eb3369874b --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala @@ -0,0 +1,24 @@ +package io.iohk.atala.agent.server.http + +import zio.{Task, ZIO, ZLayer, URIO} +import io.iohk.atala.pollux.schema.SchemaRegistryServerEndpoints +import io.iohk.atala.pollux.service.SchemaRegistryService +import sttp.tapir.redoc.bundle.RedocInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter +//import sttp.tapir.ztapir.{RichZServerEndpoint, ZServerEndpoint} +import sttp.tapir.redoc.RedocUIOptions +import sttp.tapir.server.ServerEndpoint +import scala.concurrent.Future + +object ZHttpEndpoints { + def swaggerEndpoints[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] = + SwaggerInterpreter().fromServerEndpoints[F](apiEndpoints, "Prism Agent", "1.0.0") + + def redocEndpoints[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] = + RedocInterpreter(redocUIOptions = RedocUIOptions.default.copy(pathPrefix = List("redoc"))) + .fromServerEndpoints[F](apiEndpoints, title = "Prism Agent", version = "1.0.0") + + def withDocumentations[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] = { + apiEndpoints ++ swaggerEndpoints[F](apiEndpoints) ++ redocEndpoints[F](apiEndpoints) + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpServer.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpServer.scala new file mode 100644 index 0000000000..7daaa79da5 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpServer.scala @@ -0,0 +1,45 @@ +package io.iohk.atala.agent.server.http + +import cats.implicits.* +import io.iohk.atala.agent.server.http.ZHttpEndpoints +import io.iohk.atala.pollux.schema.SchemaRegistryServerEndpoints +import io.iohk.atala.pollux.service.{SchemaRegistryService, SchemaRegistryServiceInMemory} +import org.http4s.* +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import org.slf4j.LoggerFactory +import sttp.model.StatusCode +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import sttp.tapir.server.interceptor.log.DefaultServerLog +import sttp.tapir.ztapir.ZServerEndpoint +import zhttp.http.HttpApp +import zhttp.service.server.ServerChannelFactory +import zhttp.service.{EventLoopGroup, Server} +import zio.interop.catz.* +import zio.* + +import scala.concurrent.ExecutionContext.Implicits.global + +object ZHttp4sBlazeServer { + def start( + endpoints: List[ZServerEndpoint[Any, Any]], + port: Int + ): Task[ExitCode] = { + val http4sEndpoints: HttpRoutes[Task] = + ZHttp4sServerInterpreter().from(endpoints).toRoutes + + val serve: Task[Unit] = + ZIO.executor.flatMap(executor => + BlazeServerBuilder[Task] + .withExecutionContext(executor.asExecutionContext) + .bindHttp(port, "0.0.0.0") + .withHttpApp(Router("/" -> http4sEndpoints).orNotFound) + .serve + .compile + .drain + ) + + serve.exitCode + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala new file mode 100644 index 0000000000..2b9279c6b6 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/FailureResponse.scala @@ -0,0 +1,39 @@ +package io.iohk.atala.api.http + +import sttp.model.StatusCode +import sttp.tapir.EndpointOutput.OneOf +import sttp.tapir.generic.auto.* +import sttp.tapir.json.zio.jsonBody +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} + +sealed trait FailureResponse + +case class NotFoundResponse(msg: String) extends FailureResponse + +object NotFoundResponse { + given encoder: zio.json.JsonEncoder[NotFoundResponse] = DeriveJsonEncoder.gen[NotFoundResponse] + given decoder: zio.json.JsonDecoder[NotFoundResponse] = DeriveJsonDecoder.gen[NotFoundResponse] +} + +case class BadRequest(msg: String, errors: List[String] = List.empty) extends FailureResponse + +object BadRequest { + given encoder: zio.json.JsonEncoder[BadRequest] = DeriveJsonEncoder.gen[BadRequest] + given decoder: zio.json.JsonDecoder[BadRequest] = DeriveJsonDecoder.gen[BadRequest] +} + +case class InternalServerError(msg: String) extends FailureResponse + +object InternalServerError { + given encoder: zio.json.JsonEncoder[InternalServerError] = DeriveJsonEncoder.gen[InternalServerError] + given decoder: zio.json.JsonDecoder[InternalServerError] = DeriveJsonDecoder.gen[InternalServerError] +} + +//An RFC-7807 compliant data structure for reporting errors to the client +case class ErrorResponse(`type`: String, title: String, status: Int, instance: String, details: Option[String]) + extends FailureResponse + +object ErrorResponse { + given encoder: zio.json.JsonEncoder[ErrorResponse] = DeriveJsonEncoder.gen[ErrorResponse] + given decoder: zio.json.JsonDecoder[ErrorResponse] = DeriveJsonDecoder.gen[ErrorResponse] +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala new file mode 100644 index 0000000000..268ed98063 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/Models.scala @@ -0,0 +1,66 @@ +package io.iohk.atala.pollux.schema + +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} + +import java.time.ZonedDateTime +import java.util.UUID + +case class VerifiableCredentialsSchemaInput( + id: Option[UUID], + name: String, + version: String, + description: Option[String], + attributes: List[String], + authored: Option[ZonedDateTime], + tags: List[String] +) +object VerifiableCredentialsSchemaInput { + given encoder: zio.json.JsonEncoder[VerifiableCredentialsSchemaInput] = + DeriveJsonEncoder.gen[VerifiableCredentialsSchemaInput] + given decoder: zio.json.JsonDecoder[VerifiableCredentialsSchemaInput] = + DeriveJsonDecoder.gen[VerifiableCredentialsSchemaInput] +} + +case class VerifiableCredentialsSchema( + id: UUID, + name: String, + version: String, + tags: List[String], + description: Option[String], + attributes: List[String], + author: String, + authored: ZonedDateTime, + proof: Option[Proof] +) + +object VerifiableCredentialsSchema { + def apply(in: VerifiableCredentialsSchemaInput): VerifiableCredentialsSchema = + VerifiableCredentialsSchema( + id = in.id.getOrElse(UUID.randomUUID()), + name = in.name, + version = in.version, + tags = in.tags, + description = in.description, + attributes = in.attributes, + author = "Prism Agent", + authored = in.authored.getOrElse(ZonedDateTime.now()), + proof = None + ) + + given encoder: zio.json.JsonEncoder[VerifiableCredentialsSchema] = DeriveJsonEncoder.gen[VerifiableCredentialsSchema] + given decoder: zio.json.JsonDecoder[VerifiableCredentialsSchema] = DeriveJsonDecoder.gen[VerifiableCredentialsSchema] +} + +case class Proof( + `type`: String, + created: ZonedDateTime, + verificationMethod: String, + proofPurpose: String, + proofValue: String, + domain: Option[String] +) + +object Proof { + given encoder: zio.json.JsonEncoder[Proof] = DeriveJsonEncoder.gen[Proof] + given decoder: zio.json.JsonDecoder[Proof] = DeriveJsonDecoder.gen[Proof] +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala new file mode 100644 index 0000000000..4b65b4d61c --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryEndpoints.scala @@ -0,0 +1,66 @@ +package io.iohk.atala.pollux.schema + +import io.iohk.atala.api.http.{BadRequest, FailureResponse, InternalServerError, NotFoundResponse} +import sttp.tapir.EndpointIO.Info +import sttp.tapir.json.zio.jsonBody +import sttp.tapir.{ + Endpoint, + EndpointInfo, + PublicEndpoint, + endpoint, + oneOf, + oneOfDefaultVariant, + oneOfVariant, + path, + stringToPath +} +import sttp.tapir.generic.auto.* +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder} +import sttp.model.StatusCode +import sttp.tapir.statusCode + +import java.util.UUID + +object SchemaRegistryEndpoints { + + val createSchemaEndpoint + : PublicEndpoint[VerifiableCredentialsSchemaInput, FailureResponse, VerifiableCredentialsSchema, Any] = + endpoint.post + .in("schema-registry" / "schemas") + .in( + jsonBody[VerifiableCredentialsSchemaInput] + .copy(info = Info.empty.description("Create schema input object with the metadata and attributes")) + ) + .out(statusCode(StatusCode.Created)) + .out(jsonBody[VerifiableCredentialsSchema]) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant(StatusCode.InternalServerError, jsonBody[InternalServerError]) + ) + ) + .name("createSchema") + .summary("Publish new schema to the schema registry") + .description( + "Publish the new schema with attributes to the schema registry on behalf of Cloud Agent. Schema will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it" + ) + .tag("Schema Registry") + + val getSchemaByIdEndpoint: PublicEndpoint[UUID, FailureResponse, VerifiableCredentialsSchema, Any] = + endpoint.get + .in( + "schema-registry" / "schemas" / path[UUID]("id") + .copy(info = Info.empty.description("Schema Id")) + ) + .out(jsonBody[VerifiableCredentialsSchema]) + .errorOut( + oneOf[FailureResponse]( + oneOfVariant(StatusCode.NotFound, jsonBody[NotFoundResponse]) + ) + ) + .name("getSchemaById") + .summary("Fetch the schema from the registry by id") + .description( + "Fetch the schema by the unique identifier. Verifiable Credential Schema in json format is returned." + ) + .tag("Schema Registry") +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala new file mode 100644 index 0000000000..5991ea7397 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/schema/SchemaRegistryServerEndpoints.scala @@ -0,0 +1,56 @@ +package io.iohk.atala.pollux.schema + +import io.iohk.atala.pollux.service.SchemaRegistryService.{createSchema, getSchemaById} +import io.iohk.atala.pollux.service.SchemaRegistryService +import sttp.tapir.redoc.RedocUIOptions +import sttp.tapir.redoc.bundle.RedocInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter +import zio.{Task, URIO, ZIO, ZLayer} +import io.iohk.atala.api.http.{FailureResponse, InternalServerError, NotFoundResponse} +import SchemaRegistryEndpoints.{createSchemaEndpoint, getSchemaByIdEndpoint} +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.ztapir.ZServerEndpoint +import sttp.tapir.ztapir.* + +import java.util.UUID + +class SchemaRegistryServerEndpoints( + schemaRegistryService: SchemaRegistryService +) { + val createSchemaServerEndpoint: ZServerEndpoint[Any, Any] = + createSchemaEndpoint.zServerLogic(schemaInput => + schemaRegistryService + .createSchema(schemaInput) + .foldZIO( + throwable => ZIO.fail[FailureResponse](InternalServerError(throwable.getMessage)), + schema => ZIO.succeed(schema) + ) + ) + + val getSchemaByIdServerEndpoint: ZServerEndpoint[Any, Any] = + getSchemaByIdEndpoint.zServerLogic(id => + schemaRegistryService + .getSchemaById(id) + .foldZIO( + throwable => ZIO.fail[FailureResponse](InternalServerError(throwable.getMessage)), + { + case Some(schema) => ZIO.succeed(schema) + case None => ZIO.fail[FailureResponse](NotFoundResponse(s"Schema is not found by $id")) + } + ) + ) + + val all: List[ZServerEndpoint[Any, Any]] = + List(createSchemaServerEndpoint, getSchemaByIdServerEndpoint) +} + +object SchemaRegistryServerEndpoints { + def all: URIO[SchemaRegistryService, List[ZServerEndpoint[Any, Any]]] = { + for { + schemaRegistryService <- ZIO.service[SchemaRegistryService] + schemaRegistryEndpoints = new SchemaRegistryServerEndpoints( + schemaRegistryService + ) + } yield schemaRegistryEndpoints.all + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala new file mode 100644 index 0000000000..f18732d62b --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryService.scala @@ -0,0 +1,21 @@ +package io.iohk.atala.pollux.service + +import zio.{Task, ZIO, ZLayer} +import io.iohk.atala.pollux.schema.{VerifiableCredentialsSchemaInput, VerifiableCredentialsSchema} + +import java.util.UUID + +trait SchemaRegistryService { + def createSchema(in: VerifiableCredentialsSchemaInput): Task[VerifiableCredentialsSchema] + def getSchemaById(id: UUID): Task[Option[VerifiableCredentialsSchema]] +} + +object SchemaRegistryService { + def createSchema( + in: VerifiableCredentialsSchemaInput + ): ZIO[SchemaRegistryService, Throwable, VerifiableCredentialsSchema] = + ZIO.serviceWithZIO[SchemaRegistryService](_.createSchema(in)) + + def getSchemaById(id: UUID): ZIO[SchemaRegistryService, Throwable, Option[VerifiableCredentialsSchema]] = + ZIO.serviceWithZIO[SchemaRegistryService](_.getSchemaById(id)) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala new file mode 100644 index 0000000000..691350b09e --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/service/SchemaRegistryServiceInMemory.scala @@ -0,0 +1,31 @@ +package io.iohk.atala.pollux.service + +import zio.{Task, ZIO, ZLayer, Ref, UIO} +import io.iohk.atala.pollux.schema.{VerifiableCredentialsSchemaInput, VerifiableCredentialsSchema} +import java.util.UUID +import scala.collection.mutable + +class SchemaRegistryServiceInMemory(ref: Ref[Map[UUID, VerifiableCredentialsSchema]]) extends SchemaRegistryService { + + // TODO: Figure out what is the logic for trying to overwrite the schema with the same id (409 Conflict) + // TODO: Other validations (same [schema_name, version], list of the attributes is not empty, etc) + override def createSchema(in: VerifiableCredentialsSchemaInput): Task[VerifiableCredentialsSchema] = { + val schema = VerifiableCredentialsSchema(in) + for { + _ <- ref.update(s => s + (schema.id -> schema)) + } yield schema + } + + override def getSchemaById(id: UUID): Task[Option[VerifiableCredentialsSchema]] = { + for { + storage <- ref.get + schema = storage.get(id) + } yield schema + } +} + +object SchemaRegistryServiceInMemory { + val layer = ZLayer.fromZIO( + Ref.make(Map.empty[UUID, VerifiableCredentialsSchema]).map(SchemaRegistryServiceInMemory(_)) + ) +}