Skip to content

Commit

Permalink
ATL-1334 feat(pollux): add sandbox project to play with Tapir and sch…
Browse files Browse the repository at this point in the history
…ema-registry (#62)

feat(pollux): add sandbox project to play with Tapir and schema-registry
  • Loading branch information
yshyn-iohk committed Oct 14, 2022
1 parent f0d1745 commit 496337b
Show file tree
Hide file tree
Showing 14 changed files with 486 additions and 0 deletions.
3 changes: 3 additions & 0 deletions sandbox/prism-agent-tapir/.scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
version = 3.5.8
maxColumn = 140
runner.dialect = scala3
19 changes: 19 additions & 0 deletions sandbox/prism-agent-tapir/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Quick start

This is a sandbox project based on tapir template for Scala 3 and ZIO/ZIO-HTTP.

You can play there with different Tapir features or even run the in-memory schema service.

You can use the standard commands:

```shell
sbt compile # build the project
sbt test # run the tests
sbt run # run the application (Main)
```

## Links:

* [tapir documentation](https://tapir.softwaremill.com/en/latest/)
* [tapir github](https://github.com/softwaremill/tapir)
* [bootzooka: template microservice using tapir](https://softwaremill.github.io/bootzooka/)
22 changes: 22 additions & 0 deletions sandbox/prism-agent-tapir/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
val tapirVersion = "1.1.0"

lazy val rootProject = (project in file(".")).settings(
Seq(
name := "pollux",
version := "0.1.0-SNAPSHOT",
organization := "iohk",
scalaVersion := "3.2.0",
libraryDependencies ++= Seq(
"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion,
"ch.qos.logback" % "logback-classic" % "1.4.1",
"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.1.1",
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % tapirVersion % Test,
"dev.zio" %% "zio-test" % "2.0.0" % Test,
"dev.zio" %% "zio-test-sbt" % "2.0.0" % Test,
"com.softwaremill.sttp.client3" %% "zio-json" % "3.8.0" % Test
),
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
)
1 change: 1 addition & 0 deletions sandbox/prism-agent-tapir/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.7.1
1 change: 1 addition & 0 deletions sandbox/prism-agent-tapir/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.iohk.atala.pollux

import io.iohk.atala.pollux.stub.SchemaEndpointsInMemory
import org.slf4j.LoggerFactory
import sttp.tapir.server.interceptor.log.DefaultServerLog
import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions}
import zhttp.http.HttpApp
import zhttp.service.server.ServerChannelFactory
import zhttp.service.{EventLoopGroup, Server}
import zio.{Console, Scope, Task, ZIO, ZIOAppArgs, ZIOAppDefault}

// Here serverEndpoints are interpreted by Tapir into HTTP Server based on ZIO-HTTP Interpreter
object Main extends ZIOAppDefault:
val log = LoggerFactory.getLogger(ZioHttpInterpreter.getClass.getName)

override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] =
val serverOptions: ZioHttpServerOptions[Any] =
ZioHttpServerOptions.customiseInterceptors
.serverLog(
DefaultServerLog[Task](
doLogWhenReceived = msg => ZIO.succeed(log.debug(msg)),
doLogWhenHandled = (msg, error) => ZIO.succeed(error.fold(log.debug(msg))(err => log.debug(msg, err))),
doLogAllDecodeFailures = (msg, error) => ZIO.succeed(error.fold(log.debug(msg))(err => log.debug(msg, err))),
doLogExceptions = (msg: String, ex: Throwable) => ZIO.succeed(log.debug(msg, ex)),
noLog = ZIO.unit
)
)
.options
val app: HttpApp[Any, Throwable] = ZioHttpInterpreter(serverOptions)
.toHttp(SchemaEndpointsInMemory.all)

val port = sys.env.get("http.port").map(_.toInt).getOrElse(8080)

(for
serverStart <- Server(app).withPort(port).make
_ <- Console.printLine(s"Go to http://localhost:${serverStart.port}/docs to open SwaggerUI. Press ENTER key to exit.")
_ <- Console.readLine
yield serverStart)
.provideSomeLayer(EventLoopGroup.auto(0) ++ ServerChannelFactory.auto ++ Scope.default)
.exitCode
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.iohk.atala.pollux.api

import io.iohk.atala.pollux.models.{BadRequest, FailureResponse, InternalServerError}
import sttp.model.StatusCode
import sttp.tapir.EndpointOutput.OneOf
import sttp.model.StatusCode
import sttp.tapir.EndpointOutput.OneOf
import sttp.tapir.generic.auto.*
import sttp.tapir.json.zio.jsonBody
import sttp.tapir.ztapir.{oneOf, oneOfVariant}
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder}

object CommonEndpointDefinitions {

// This is an example of the common output definition for the endpoint that can be reused in the endpoint definition
val httpErrors: OneOf[FailureResponse, FailureResponse] = oneOf[FailureResponse](
oneOfVariant(StatusCode.InternalServerError, jsonBody[InternalServerError]),
oneOfVariant(StatusCode.BadRequest, jsonBody[BadRequest])
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.iohk.atala.pollux.api

import io.iohk.atala.pollux.models.{
BadRequest,
ErrorResponse,
FailureResponse,
InternalServerError,
NotFoundResponse,
UnauthorizedResponse,
UnknownResponse,
VerifiableCredentialsSchema,
VerifiableCredentialsSchemaInput
}
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 VerifiableCredentialsSchemaEndpoints {

val createSchemaEndpoint: PublicEndpoint[VerifiableCredentialsSchemaInput, Unit, 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])
.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 getSchemaById: 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")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.iohk.atala.pollux.models

import sttp.model.StatusCode
import sttp.tapir.EndpointOutput.OneOf
import sttp.tapir.generic.auto.*
import sttp.tapir.json.zio.jsonBody
import sttp.tapir.ztapir.{oneOf, oneOfVariant}
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]
}

case class UnauthorizedResponse(message: String) extends FailureResponse

object UnauthorizedResponse {
given encoder: zio.json.JsonEncoder[UnauthorizedResponse] = DeriveJsonEncoder.gen[UnauthorizedResponse]
given decoder: zio.json.JsonDecoder[UnauthorizedResponse] = DeriveJsonDecoder.gen[UnauthorizedResponse]
}

case class UnknownResponse(message: String) extends FailureResponse

object UnknownResponse {
given encoder: zio.json.JsonEncoder[UnknownResponse] = DeriveJsonEncoder.gen[UnknownResponse]
given decoder: zio.json.JsonDecoder[UnknownResponse] = DeriveJsonDecoder.gen[UnknownResponse]
}

// ErrorResponse:
// type: object
// description: An RFC-7807 compliant data structure for reporting errors to the client
// required:
// - type
// - title
// - status
// - instance
// properties:
// type:
// type: string
// description: A URI reference that identifies the problem type.
// example: https://example.org/doc/#model-MalformedEmail
// title:
// type: string
// example: "Malformed email"
// description: |-
// A short, human-readable summary of the problem type. It does not
// change from occurrence to occurrence of the problem.
// status:
// type: integer
// format: int32
// example: 400
// description: |-
// The HTTP status code for this occurrence of the problem.
// detail:
// type: string
// description: |-
// A human-readable explanation specific to this occurrence of the problem.
// example: "The received '{}à!è@!.b}' email does not conform to the email format"
// instance:
// type: string
// example: "/problems/d914e"
// description: |-
// A URI reference that identifies the specific occurrence of the problem.
// It may or may not yield further information if dereferenced.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.iohk.atala.pollux.models

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]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.iohk.atala.pollux.services

import io.iohk.atala.pollux.models.*
import zio.Task
import java.util.UUID

trait SchemaService {
def createSchema(in: VerifiableCredentialsSchemaInput): Task[VerifiableCredentialsSchema]
def getSchemaById(id: UUID): Task[Option[VerifiableCredentialsSchema]]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.iohk.atala.pollux.services
import io.iohk.atala.pollux.models.{VerifiableCredentialsSchema, VerifiableCredentialsSchemaInput}

import java.util.UUID
import scala.collection.mutable
import zio.{Task, ZIO}

class SchemaServiceInMemory() extends SchemaService {
val storage = mutable.Map[UUID, VerifiableCredentialsSchema]()

override def createSchema(in: VerifiableCredentialsSchemaInput): Task[VerifiableCredentialsSchema] = {
val schema = VerifiableCredentialsSchema(in)
storage.put(schema.id, schema)
ZIO.succeed(schema)
}

override def getSchemaById(id: UUID): Task[Option[VerifiableCredentialsSchema]] = {
ZIO.succeed(storage.get(id))
}
}

object SchemaServiceInMemory {
val instance = SchemaServiceInMemory()
}

0 comments on commit 496337b

Please sign in to comment.