diff --git a/.gitignore b/.gitignore index b95abc9fe8..f8d0bd8dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ megalinter-reports **/project/metals.sbt target + +shell.nix +.envrc \ No newline at end of file diff --git a/castor/api/grpc/castor_api.proto b/castor/api/grpc/castor_api.proto new file mode 100644 index 0000000000..94bf73f522 --- /dev/null +++ b/castor/api/grpc/castor_api.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +import "scalapb/scalapb.proto"; + +option (scalapb.options) = { + no_default_values_in_constructor: true + package_name: "io.iohk.atala.castor.proto" +}; + + +message Ping { + string message = 1; + float delaySeconds = 2; +} + +message Pong { + string message = 1; +} + +service DIDService { + rpc SendPing(Ping) returns (Pong) {} +} \ No newline at end of file diff --git a/castor/api/castor-openapi-spec.yaml b/castor/api/http/castor-openapi-spec.yaml similarity index 98% rename from castor/api/castor-openapi-spec.yaml rename to castor/api/http/castor-openapi-spec.yaml index 840723e761..c8037c7493 100644 --- a/castor/api/castor-openapi-spec.yaml +++ b/castor/api/http/castor-openapi-spec.yaml @@ -302,7 +302,7 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" - /did-authentication/v1/challenges: + /did-authentication/challenges: post: tags: ["DID Authentication"] operationId: createDidAuthenticationChallenge @@ -330,7 +330,7 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" - /did-authentication/v1/challenge-submissions: + /did-authentication/challenge-submissions: post: tags: ["DID Authentication"] operationId: createDidAuthenticationChallengeSubmission @@ -514,10 +514,6 @@ components: items: $ref: "#/components/schemas/Service" - DIDRef: - type: string - example: "did:example:123456789abcdefghi" - VerificationMethodRef: type: string example: "did:example:123456789abcdefghi#keys-1" @@ -843,7 +839,7 @@ components: - ttl properties: ttl: - type: number + type: integer description: A number of seconds that challenge will be considered valid. example: 900 state: @@ -898,11 +894,10 @@ components: AuthenticationChallengeSubject: description: | A challenged subject that must complete the challenge. - If VerificationMethodRef is used, it must be a verification method - in the authentication relationship. - oneOf: - - $ref: "#/components/schemas/DIDRef" - - $ref: "#/components/schemas/VerificationMethodRef" + May refer to DID or VerificationMethod inside a DID. If VerificationMethod + is used, it must be inside the authentication verification relationship. + type: string + example: "did:example:123456789abcdefghi" AuthenticationChallengeJwt: type: string diff --git a/castor/service/.gitignore b/castor/service/.gitignore new file mode 100644 index 0000000000..9e79245eef --- /dev/null +++ b/castor/service/.gitignore @@ -0,0 +1,32 @@ +# macOS +.DS_Store + +# sbt specific +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +project/local-plugins.sbt +.history +.ensime +.ensime_cache/ +.sbt-scripted/ +local.sbt + +# Bloop +.bsp + +# VS Code +.vscode/ + +# Metals +.bloop/ +.metals/ +metals.sbt + +# IDEA +.idea +.idea_modules +/.worksheet/ diff --git a/castor/service/.scalafmt.conf b/castor/service/.scalafmt.conf new file mode 100644 index 0000000000..688e6cbf01 --- /dev/null +++ b/castor/service/.scalafmt.conf @@ -0,0 +1,4 @@ +version = 3.5.8 +runner.dialect = scala3 + +maxColumn = 120 \ No newline at end of file diff --git a/castor/service/README.md b/castor/service/README.md new file mode 100644 index 0000000000..e080e50a41 --- /dev/null +++ b/castor/service/README.md @@ -0,0 +1,10 @@ +# Castor BB service + +## Quickstart + +__Running Castor service locally for development__ + +```bash +docker-compose -f docker/docker-compose-local.yaml up -d +sbt api-server/run +``` diff --git a/castor/service/api-server/openapi/generator-config/config.yaml b/castor/service/api-server/openapi/generator-config/config.yaml new file mode 100644 index 0000000000..06e8175d76 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/config.yaml @@ -0,0 +1,17 @@ +# The folder where templates can be found +templateDir: "api-server/openapi/generator-config/scala-akka-http-server" + +# Only generate output for templates registered with the 'Apis' or 'Models' types +globalProperties: + apis: "" + models: "" + +# Define output packages for generated classes +apiPackage: "io.iohk.atala.castor.openapi.api" +modelPackage: "io.iohk.atala.castor.openapi.model" + +# The generator to use +generatorName: "scala-akka-http-server" + +# Use handwritten classes for the types below. This is useful to customize the (de)serialization +#importMappings: \ No newline at end of file diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/README.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/README.mustache new file mode 100644 index 0000000000..ff7d54bcb1 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/README.mustache @@ -0,0 +1,32 @@ +# {{&appName}} + +{{&appDescription}} + +{{^hideGenerationTimestamp}} + This Scala akka-http framework project was generated by the OpenAPI generator tool at {{generatedDate}}. +{{/hideGenerationTimestamp}} + +{{#generateApis}} + ## API + + {{#apiInfo}} + {{#apis}} + ### {{baseName}} + + |Name|Role| + |----|----| + |`{{importPath}}Controller`|akka-http API controller| + |`{{importPath}}Api`|Representing trait| + {{^skipStubs}} + |`{{importPath}}ApiImpl`|Default implementation| + {{/skipStubs}} + + {{#operations}} + {{#operation}} + * `{{httpMethod}} {{contextPath}}{{path}}{{#queryParams.0}}?{{/queryParams.0}}{{#queryParams}}{{paramName}}=[value]{{^-last}}&{{/-last}}{{/queryParams}}` - {{summary}} + {{/operation}} + {{/operations}} + + {{/apis}} + {{/apiInfo}} +{{/generateApis}} diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/api.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/api.mustache new file mode 100644 index 0000000000..4160a1aacf --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/api.mustache @@ -0,0 +1,96 @@ +package {{package}} + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.model.StatusCodes +{{^pathMatcherPatterns.isEmpty}}import akka.http.scaladsl.server.{PathMatcher, PathMatcher1} +{{/pathMatcherPatterns.isEmpty}} +{{#hasMarshalling}}import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import akka.http.scaladsl.unmarshalling.FromStringUnmarshaller +{{/hasMarshalling}} +{{#hasCookieParams}}import akka.http.scaladsl.model.headers.HttpCookiePair +{{/hasCookieParams}} +{{#hasMultipart}}import {{invokerPackage}}.StringDirectives +import {{invokerPackage}}.MultipartDirectives +import {{invokerPackage}}.FileField +import {{invokerPackage}}.PartsAndFiles +{{/hasMultipart}} +{{#imports}}import {{import}} +{{/imports}} +{{#hasMultipart}}import scala.util.Try +import akka.http.scaladsl.server.MalformedRequestContentRejection +import akka.http.scaladsl.server.directives.FileInfo +{{/hasMultipart}} + + +{{#operations}} +class {{classname}}( + {{classVarName}}Service: {{classname}}Service{{#hasMarshalling}}, + {{classVarName}}Marshaller: {{classname}}Marshaller{{/hasMarshalling}} +) {{#hasMultipart}} extends MultipartDirectives with StringDirectives {{/hasMultipart}}{ + + {{#pathMatcherPatterns}}import {{classname}}Patterns.{{pathMatcherVarName}} + {{/pathMatcherPatterns}} + + {{#hasMarshalling}}import {{classVarName}}Marshaller._ + {{/hasMarshalling}} + + lazy val route: Route = + {{#operation}} + path({{#vendorExtensions.x-paths}}{{#isText}}"{{/isText}}{{value}}{{#isText}}"{{/isText}}{{^-last}} / {{/-last}}{{/vendorExtensions.x-paths}}) { {{^pathParams.isEmpty}}({{#pathParams}}{{paramName}}{{^-last}}, {{/-last}}{{/pathParams}}) => {{/pathParams.isEmpty}} + {{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} { {{^queryParams.isEmpty}} + parameters({{#queryParams}}"{{baseName}}".as[{{dataType}}]{{^required}}.?{{#vendorExtensions.x-has-default-value}}({{{defaultValue}}}){{/vendorExtensions.x-has-default-value}}{{/required}}{{^-last}}, {{/-last}}{{/queryParams}}) { ({{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}}) =>{{/queryParams.isEmpty}} {{^headerParams.isEmpty}} + {{#headerParams}}{{#required}}headerValueByName{{/required}}{{^required}}optionalHeaderValueByName{{/required}}("{{baseName}}") { {{paramName}} => {{/headerParams}}{{/headerParams.isEmpty}}{{^cookieParams.isEmpty}} + {{#cookieParams}}{{#required}}cookie({{/required}}{{^required}}optionalCookie({{/required}}"{{baseName}}"){ {{paramName}} => {{/cookieParams}}{{/cookieParams.isEmpty}}{{#isMultipart}} +{{> multipart}}{{/isMultipart}}{{^isMultipart}}{{> noMultipart}}{{/isMultipart}}{{^cookieParams.isEmpty}} + }{{/cookieParams.isEmpty}}{{^headerParams.isEmpty}} + }{{/headerParams.isEmpty}}{{^queryParams.isEmpty}} + }{{/queryParams.isEmpty}} + } + }{{^-last}} ~{{/-last}} + {{/operation}} +} + +{{^pathMatcherPatterns.isEmpty}} +object {{classname}}Patterns { + + {{#pathMatcherPatterns}}val {{pathMatcherVarName}}: PathMatcher1[String] = PathMatcher("{{pattern}}".r) + {{/pathMatcherPatterns}} +} +{{/pathMatcherPatterns.isEmpty}} + +trait {{classname}}Service { + +{{#operation}} +{{#responses}} def {{operationId}}{{#vendorExtensions.x-is-default}}Default{{/vendorExtensions.x-is-default}}{{^vendorExtensions.x-is-default}}{{code}}{{/vendorExtensions.x-is-default}}{{#baseType}}({{#vendorExtensions.x-is-default}}statusCode: Int, {{/vendorExtensions.x-is-default}}response{{baseType}}{{containerType}}: {{dataType}})(implicit toEntityMarshaller{{baseType}}{{containerType}}: ToEntityMarshaller[{{dataType}}]){{/baseType}}{{^baseType}}{{#vendorExtensions.x-is-default}}(statusCode: Int){{/vendorExtensions.x-is-default}}{{/baseType}}: Route ={{#vendorExtensions.x-empty-response}} + complete({{#vendorExtensions.x-is-default}}statusCode{{/vendorExtensions.x-is-default}}{{^vendorExtensions.x-is-default}}StatusCodes.getForKey({{code}}){{/vendorExtensions.x-is-default}}){{/vendorExtensions.x-empty-response}}{{^vendorExtensions.x-empty-response}} + complete(({{#vendorExtensions.x-is-default}}statusCode{{/vendorExtensions.x-is-default}}{{^vendorExtensions.x-is-default}}{{code}}{{/vendorExtensions.x-is-default}}, {{#baseType}}response{{baseType}}{{containerType}}{{/baseType}}{{^baseType}}"{{message}}"{{/baseType}})){{/vendorExtensions.x-empty-response}} +{{/responses}} + /** +{{#responses}} * {{#code}}Code: {{.}}{{/code}}{{#message}}, Message: {{.}}{{/message}}{{#dataType}}, DataType: {{.}}{{/dataType}} + {{/responses}} + */ + def {{operationId}}({{> operationParam}}){{^vendorExtensions.x-specific-marshallers.isEmpty}} + (implicit {{#vendorExtensions.x-specific-marshallers}}toEntityMarshaller{{varName}}: ToEntityMarshaller[{{dataType}}]{{^-last}}, {{/-last}}{{/vendorExtensions.x-specific-marshallers}}){{/vendorExtensions.x-specific-marshallers.isEmpty}}: Route + +{{/operation}} +} + +{{#hasMarshalling}} +trait {{classname}}Marshaller { +{{#entityUnmarshallers}} implicit def fromEntityUnmarshaller{{varName}}: FromEntityUnmarshaller[{{dataType}}] + +{{/entityUnmarshallers}} + +{{#stringUnmarshallers}} implicit def fromStringUnmarshaller{{varName}}: FromStringUnmarshaller[{{dataType}}] + +{{/stringUnmarshallers}} + +{{#entityMarshallers}} implicit def toEntityMarshaller{{varName}}: ToEntityMarshaller[{{dataType}}] + +{{/entityMarshallers}} +} +{{/hasMarshalling}} + +{{/operations}} diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/build.sbt.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/build.sbt.mustache new file mode 100644 index 0000000000..69788d7e7b --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/build.sbt.mustache @@ -0,0 +1,16 @@ +version := "{{artifactVersion}}" +name := "{{artifactId}}" +organization := "{{groupId}}" +scalaVersion := "2.12.8" + +val AkkaVersion = "2.6.19" +val AkkaHttpVersion = "10.2.9" +val LogbackVersion = "1.2.11" + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion, + "com.typesafe.akka" %% "akka-stream" % AkkaVersion, + "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion, + "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion, + "ch.qos.logback" % "logback-classic" % LogbackVersion % Runtime +) diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/controller.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/controller.mustache new file mode 100644 index 0000000000..344a9112e3 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/controller.mustache @@ -0,0 +1,15 @@ +package {{invokerPackage}} + +import akka.actor.typed.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Route +{{#apiInfo}}{{#apis}}{{#operations}}import {{package}}.{{classname}} +{{/operations}}{{/apis}}{{/apiInfo}} +import akka.http.scaladsl.server.Directives._ + +class Controller[T]({{#apiInfo}}{{#apis}}{{#operations}}{{classVarName}}: {{classname}}{{^-last}}, {{/-last}}{{/operations}}{{/apis}}{{/apiInfo}})(implicit system: ActorSystem[T]) { + + lazy val routes: Route = {{#apiInfo}}{{#apis}}{{#operations}}{{classVarName}}.route {{^-last}}~ {{/-last}}{{/operations}}{{/apis}}{{/apiInfo}} + + Http().newServerAt("0.0.0.0", 9000).bind(routes) +} \ No newline at end of file diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/helper.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/helper.mustache new file mode 100644 index 0000000000..8aa3c0f8c2 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/helper.mustache @@ -0,0 +1,34 @@ +package {{invokerPackage}} + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.{PathMatcher, PathMatcher1} +import scala.util.{Failure, Success, Try} +import scala.util.control.NoStackTrace + +object AkkaHttpHelper { + def optToTry[T](opt: Option[T], err: => String): Try[T] = + opt.map[Try[T]](Success(_)) getOrElse Failure(new RuntimeException(err) with NoStackTrace) + + /** + * A PathMatcher that matches and extracts a Float value. The matched string representation is the pure decimal, + * optionally signed form of a float value, i.e. without exponent. + * + * @group pathmatcher + */ + val FloatNumber: PathMatcher1[Float] = + PathMatcher("""[+-]?\d*\.?\d*""".r) flatMap { string => + try Some(java.lang.Float.parseFloat(string)) + catch { case _: NumberFormatException => None } + } + + /** + * A PathMatcher that matches and extracts a Boolean value. + * + * @group pathmatcher + */ + val Boolean: PathMatcher1[Boolean] = + Segment.flatMap { string => + try Some(string.toBoolean) + catch { case _: IllegalArgumentException => None } + } +} diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/model.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/model.mustache new file mode 100644 index 0000000000..68cafc5f2c --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/model.mustache @@ -0,0 +1,27 @@ +package {{package}} + +{{#imports}} +import {{import}} +{{/imports}} + +{{#models}} +{{#model}} +/** +{{#title}} * = {{{.}}} = + * +{{/title}} +{{#description}} * {{{.}}} + * +{{/description}} +{{#vars}} + * @param {{{name}}} {{{description}}}{{#example}} for example: ''{{{.}}}''{{/example}} +{{/vars}} +*/ +final case class {{classname}} ( + {{#vars}} + {{{name}}}: {{^required}}Option[{{/required}}{{datatype}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} + {{/vars}} +) + +{{/model}} +{{/models}} diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/multipart.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/multipart.mustache new file mode 100644 index 0000000000..1da79aef03 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/multipart.mustache @@ -0,0 +1,12 @@ + formAndFiles({{#vendorExtensions.x-file-params}}FileField("{{baseName}}")){{/vendorExtensions.x-file-params}}{{^-last}}, {{/-last}} { partsAndFiles => {{^vendorExtensions.x-file-params.isEmpty}} + val _____ : Try[Route] = for { + {{#vendorExtensions.x-file-params}}{{baseName}} <- optToTry(partsAndFiles.files.get("{{baseName}}"), s"File {{baseName}} missing") + {{/vendorExtensions.x-file-params}} + } yield { {{/vendorExtensions.x-file-params.isEmpty}} + implicit val vp: StringValueProvider = partsAndFiles.form{{^vendorExtensions.x-non-file-params.isEmpty}} + stringFields({{#vendorExtensions.x-non-file-params}}"{{baseName}}".as[{{dataType}}]{{^required}}.?{{#vendorExtensions.x-has-default-value}}({{defaultValue}}){{/vendorExtensions.x-has-default-value}}{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-file-params}}) { ({{#vendorExtensions.x-non-file-params}}{{paramName}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-file-params}}) =>{{/vendorExtensions.x-non-file-params.isEmpty}} + {{classVarName}}Service.{{operationId}}({{#allParams}}{{paramName}} = {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}){{^vendorExtensions.nonFileFormParams.isEmpty}} + }{{/vendorExtensions.nonFileFormParams.isEmpty}}{{^vendorExtensions.x-file-params.isEmpty}} + } + _____.fold[Route](t => reject(MalformedRequestContentRejection("Missing file.", t)), identity){{/vendorExtensions.x-file-params.isEmpty}} + } \ No newline at end of file diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/multipartDirectives.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/multipartDirectives.mustache new file mode 100644 index 0000000000..6e802204c5 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/multipartDirectives.mustache @@ -0,0 +1,89 @@ +package {{invokerPackage}} + +import java.io.File +import java.nio.file.Files + +import akka.annotation.ApiMayChange +import akka.http.scaladsl.model.Multipart.FormData +import akka.http.scaladsl.model.{ContentType, HttpEntity, Multipart} +import akka.http.scaladsl.server.Directive1 +import akka.http.scaladsl.server.directives._ +import akka.stream.Materializer +import akka.stream.scaladsl._ + +import scala.collection.immutable +import scala.concurrent.{ExecutionContextExecutor, Future} + +trait MultipartDirectives { + + import akka.http.scaladsl.server.directives.BasicDirectives._ + import akka.http.scaladsl.server.directives.FutureDirectives._ + import akka.http.scaladsl.server.directives.MarshallingDirectives._ + + @ApiMayChange + def formAndFiles(fileFields: FileField*): Directive1[PartsAndFiles] = + entity(as[Multipart.FormData]).flatMap { + formData => + extractRequestContext.flatMap { ctx => + implicit val mat: Materializer = ctx.materializer + implicit val ec: ExecutionContextExecutor = ctx.executionContext + + val uploadingSink: Sink[FormData.BodyPart, Future[PartsAndFiles]] = + Sink.foldAsync[PartsAndFiles, Multipart.FormData.BodyPart](PartsAndFiles.Empty) { + (acc, part) => + def discard(p: Multipart.FormData.BodyPart): Future[PartsAndFiles] = { + p.entity.discardBytes() + Future.successful(acc) + } + + part.filename.map { + fileName => + fileFields.find(_.fieldName == part.name) + .map { + case FileField(_, destFn) => + val fileInfo = FileInfo(part.name, fileName, part.entity.contentType) + val dest = destFn(fileInfo) + + part.entity.dataBytes.runWith(FileIO.toPath(dest.toPath)).map { _ => + acc.addFile(fileInfo, dest) + } + }.getOrElse(discard(part)) + } getOrElse { + part.entity match { + case HttpEntity.Strict(ct: ContentType.NonBinary, data) => + val charsetName = ct.charset.nioCharset.name + val partContent = data.decodeString(charsetName) + + Future.successful(acc.addForm(part.name, partContent)) + case _ => + discard(part) + } + } + } + + val uploadedF = formData.parts.runWith(uploadingSink) + + onSuccess(uploadedF) + } + } +} + +object MultipartDirectives extends MultipartDirectives with FileUploadDirectives { + val tempFileFromFileInfo: FileInfo => File = { + file: FileInfo => Files.createTempFile(file.fileName, ".tmp").toFile() + } +} + +final case class FileField(fieldName: String, fileNameF: FileInfo => File = MultipartDirectives.tempFileFromFileInfo) + +final case class PartsAndFiles(form: immutable.Map[String, String], files: Map[String, (FileInfo, File)]) { + def addForm(fieldName: String, content: String): PartsAndFiles = this.copy(form.updated(fieldName, content)) + + def addFile(info: FileInfo, file: File): PartsAndFiles = this.copy( + files = files.updated(info.fieldName, (info, file)) + ) +} + +object PartsAndFiles { + val Empty: PartsAndFiles = PartsAndFiles(immutable.Map.empty, immutable.Map.empty) +} \ No newline at end of file diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/noMultipart.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/noMultipart.mustache new file mode 100644 index 0000000000..1d45522d0c --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/noMultipart.mustache @@ -0,0 +1,7 @@ +{{^formParams.isEmpty}} + + formFields({{#formParams}}"{{baseName}}".as[{{#isPrimitiveType}}{{dataType}}{{/isPrimitiveType}}{{^isPrimitiveType}}String{{/isPrimitiveType}}]{{^required}}.?{{#vendorExtensions.x-has-default-value}}({{defaultValue}}){{/vendorExtensions.x-has-default-value}}{{/required}}{{^-last}}, {{/-last}}{{/formParams}}) { ({{#formParams}}{{paramName}}{{^-last}}, {{/-last}}{{/formParams}}) =>{{/formParams.isEmpty}} + {{#bodyParam}}{{^isPrimitiveType}}entity(as[{{dataType}}]){ {{paramName}} => + {{/isPrimitiveType}}{{/bodyParam}}{{classVarName}}Service.{{operationId}}({{#allParams}}{{paramName}} = {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}){{#bodyParam}}{{^isPrimitiveType}} + }{{/isPrimitiveType}}{{/bodyParam}}{{^formParams.isEmpty}} + }{{/formParams.isEmpty}} \ No newline at end of file diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/operationParam.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/operationParam.mustache new file mode 100644 index 0000000000..fc0339da10 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/operationParam.mustache @@ -0,0 +1 @@ +{{#allParams}}{{paramName}}: {{#isFile}}(FileInfo, File){{/isFile}}{{^isFile}}{{^required}}{{^vendorExtensions.x-has-default-value}}Option[{{/vendorExtensions.x-has-default-value}}{{/required}}{{dataType}}{{^required}}{{^vendorExtensions.x-has-default-value}}]{{/vendorExtensions.x-has-default-value}}{{/required}}{{/isFile}}{{^-last}}, {{/-last}}{{/allParams}} \ No newline at end of file diff --git a/castor/service/api-server/openapi/generator-config/scala-akka-http-server/stringDirectives.mustache b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/stringDirectives.mustache new file mode 100644 index 0000000000..5640a9d454 --- /dev/null +++ b/castor/service/api-server/openapi/generator-config/scala-akka-http-server/stringDirectives.mustache @@ -0,0 +1,127 @@ +package {{invokerPackage}} + +import akka.http.scaladsl.common._ +import akka.http.scaladsl.server.{Directive, Directive0, Directive1, InvalidRequiredValueForQueryParamRejection, MalformedFormFieldRejection, MissingFormFieldRejection, MissingQueryParamRejection, UnsupportedRequestContentTypeRejection} +import akka.http.scaladsl.server.directives.BasicDirectives +import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException + +import scala.concurrent.Future +import scala.util.{Failure, Success} + +trait StringDirectives { + implicit def _symbol2NR(symbol: Symbol): NameReceptacle[String] = new NameReceptacle[String](symbol.name) + implicit def _string2NR(string: String): NameReceptacle[String] = new NameReceptacle[String](string) + + import StringDirectives._ + type StringValueProvider = Map[String, String] + + def stringField(pdm: StringMagnet): pdm.Out = pdm() + + def stringFields(pdm: StringMagnet): pdm.Out = pdm() + +} + +object StringDirectives extends StringDirectives { + + sealed trait StringMagnet { + type Out + def apply(): Out + } + object StringMagnet { + implicit def apply[T](value: T)(implicit sdef: StringDef[T]): StringMagnet { type Out = sdef.Out } = + new StringMagnet { + type Out = sdef.Out + def apply(): sdef.Out = sdef(value) + } + } + + type StringDefAux[A, B] = StringDef[A] { type Out = B } + sealed trait StringDef[T] { + type Out + def apply(value: T): Out + } + object StringDef { + protected def stringDef[A, B](f: A => B): StringDefAux[A, B] = + new StringDef[A] { + type Out = B + + def apply(value: A): B = f(value) + } + + import akka.http.scaladsl.server.directives.BasicDirectives._ + import akka.http.scaladsl.server.directives.FutureDirectives._ + import akka.http.scaladsl.server.directives.RouteDirectives._ + import akka.http.scaladsl.unmarshalling._ + + type FSU[T] = FromStringUnmarshaller[T] + type FSOU[T] = Unmarshaller[Option[String], T] + type SFVP = StringValueProvider + + protected def extractField[A, B](f: A => Directive1[B]): StringDefAux[A, Directive1[B]] = stringDef(f) + + protected def handleFieldResult[T](fieldName: String, result: Future[T]): Directive1[T] = onComplete(result).flatMap { + case Success(x) => provide(x) + case Failure(Unmarshaller.NoContentException) => reject(MissingFormFieldRejection(fieldName)){{#akkaHttp10_1_10_plus}} + case Failure(x: UnsupportedContentTypeException) => reject(UnsupportedRequestContentTypeRejection(x.supported, x.actualContentType)){{/akkaHttp10_1_10_plus}}{{^akkaHttp10_1_10_plus}} + case Failure(x: UnsupportedContentTypeException) => reject(UnsupportedRequestContentTypeRejection(x.supported)){{/akkaHttp10_1_10_plus}} + case Failure(x) => reject(MalformedFormFieldRejection(fieldName, if (x.getMessage == null) "" else x.getMessage, Option(x.getCause))) + } + + private def filter[T](paramName: String, fsou: FSOU[T])(implicit vp: SFVP): Directive1[T] = { + extract { ctx => + import ctx.{executionContext, materializer} + handleFieldResult(paramName, fsou(vp.get(paramName))) + }.flatMap(identity) + } + + implicit def forString(implicit fsu: FSU[String], vp: SFVP): StringDefAux[String, Directive1[String]] = + extractField[String, String] { string => filter(string, fsu) } + implicit def forSymbol(implicit fsu: FSU[String], vp: SFVP): StringDefAux[Symbol, Directive1[String]] = + extractField[Symbol, String] { symbol => filter(symbol.name, fsu) } + implicit def forNR[T](implicit fsu: FSU[T], vp: SFVP): StringDefAux[NameReceptacle[T], Directive1[T]] = + extractField[NameReceptacle[T], T] { nr => filter(nr.name, fsu) } + implicit def forNUR[T](implicit vp: SFVP): StringDefAux[NameUnmarshallerReceptacle[T], Directive1[T]] = + extractField[NameUnmarshallerReceptacle[T], T] { nr => filter(nr.name, nr.um) } + implicit def forNOR[T](implicit fsou: FSOU[T], vp: SFVP): StringDefAux[NameOptionReceptacle[T], Directive1[Option[T]]] = + extractField[NameOptionReceptacle[T], Option[T]] { nr => filter[Option[T]](nr.name, fsou) } + implicit def forNDR[T](implicit fsou: FSOU[T], vp: SFVP): StringDefAux[NameDefaultReceptacle[T], Directive1[T]] = + extractField[NameDefaultReceptacle[T], T] { nr => filter[T](nr.name, fsou withDefaultValue nr.default) } + implicit def forNOUR[T](implicit vp: SFVP): StringDefAux[NameOptionUnmarshallerReceptacle[T], Directive1[Option[T]]] = + extractField[NameOptionUnmarshallerReceptacle[T], Option[T]] { nr => filter(nr.name, nr.um: FSOU[T]) } + implicit def forNDUR[T](implicit vp: SFVP): StringDefAux[NameDefaultUnmarshallerReceptacle[T], Directive1[T]] = + extractField[NameDefaultUnmarshallerReceptacle[T], T] { nr => filter[T](nr.name, (nr.um: FSOU[T]) withDefaultValue nr.default) } + + //////////////////// required parameter support //////////////////// + + private def requiredFilter[T](paramName: String, fsou: FSOU[T], requiredValue: Any)(implicit vp: SFVP): Directive0 = { + extract { ctx => + import ctx.{executionContext, materializer} + onComplete(fsou(vp.get(paramName))) flatMap { + case Success(value) if value == requiredValue => pass + case Success(value) => reject(InvalidRequiredValueForQueryParamRejection(paramName, requiredValue.toString, value.toString)).toDirective[Unit] + case _ => reject(MissingQueryParamRejection(paramName)).toDirective[Unit] + } + }.flatMap(identity) + } + + implicit def forRVR[T](implicit fsu: FSU[T], vp: SFVP): StringDefAux[RequiredValueReceptacle[T], Directive0] = + stringDef[RequiredValueReceptacle[T], Directive0] { rvr => requiredFilter(rvr.name, fsu, rvr.requiredValue) } + + implicit def forRVDR[T](implicit vp: SFVP): StringDefAux[RequiredValueUnmarshallerReceptacle[T], Directive0] = + stringDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr => requiredFilter(rvr.name, rvr.um, rvr.requiredValue) } + + //////////////////// tuple support //////////////////// + + import akka.http.scaladsl.server.util.BinaryPolyFunc + import akka.http.scaladsl.server.util.TupleOps._ + + implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertStringDefAndConcatenate.type]): StringDefAux[T, fold.Out] = + stringDef[T, fold.Out](fold(BasicDirectives.pass, _)) + + object ConvertStringDefAndConcatenate extends BinaryPolyFunc { + implicit def from[P, TA, TB](implicit sdef: StringDef[P] {type Out = Directive[TB]}, ev: Join[TA, TB]): BinaryPolyFunc.Case[Directive[TA], P, ConvertStringDefAndConcatenate.type] {type Out = Directive[ev.Out]} = + at[Directive[TA], P] { (a, t) => a & sdef(t) } + } + + } +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/Main.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/Main.scala new file mode 100644 index 0000000000..bfd9a68e44 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/Main.scala @@ -0,0 +1,7 @@ +package io.iohk.atala.castor.apiserver + +import zio.* + +object Main extends ZIOAppDefault { + override def run = Modules.app +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/Modules.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/Modules.scala new file mode 100644 index 0000000000..fb37414dcd --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/Modules.scala @@ -0,0 +1,121 @@ +package io.iohk.atala.castor.apiserver + +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.castor.apiserver.grpc.service.DIDServiceGrpcImpl +import io.iohk.atala.castor.apiserver.grpc.{GrpcServer, GrpcServices} +import io.iohk.atala.castor.apiserver.http.{HttpRoutes, HttpServer} +import io.iohk.atala.castor.core.service.{ + DIDAuthenticationService, + DIDOperationService, + DIDService, + MockDIDAuthenticationService, + MockDIDOperationService, + MockDIDService +} +import io.iohk.atala.castor.apiserver.http.marshaller.{ + DIDApiMarshallerImpl, + DIDAuthenticationApiMarshallerImpl, + DIDOperationsApiMarshallerImpl +} +import io.iohk.atala.castor.apiserver.http.service.{ + DIDApiServiceImpl, + DIDAuthenticationApiServiceImpl, + DIDOperationsApiServiceImpl +} +import io.iohk.atala.castor.core.repository.DIDOperationRepository +import io.iohk.atala.castor.openapi.api.{ + DIDApi, + DIDAuthenticationApi, + DIDAuthenticationApiMarshaller, + DIDAuthenticationApiService, + DIDOperationsApi +} +import io.iohk.atala.castor.proto.castor_api.DIDServiceGrpc +import io.iohk.atala.castor.sql.repository.{JdbcDIDOperationRepository, TransactorLayer} +import zio.* +import zio.interop.catz.* +import cats.effect.std.Dispatcher + +object Modules { + + val actorSystemLayer: TaskLayer[ActorSystem[Nothing]] = ZLayer.scoped( + ZIO.acquireRelease(ZIO.attempt(ActorSystem(Behaviors.empty, "actor-system")))(system => + ZIO.attempt(system.terminate()).orDie + ) + ) + + val app: Task[Unit] = { + val httpServerApp = HttpRoutes.routes.flatMap(HttpServer.start(8080, _)) + val grpcServerApp = GrpcServices.services.flatMap(GrpcServer.start(8081, _)) + + (httpServerApp <&> grpcServerApp) + .provideLayer(actorSystemLayer ++ HttpModule.layers ++ GrpcModule.layers) + .unit + } + +} + +// TODO: replace with actual implementation +object AppModule { + val didServiceLayer: ULayer[DIDService] = MockDIDService.layer + val didAuthenticationServiceLayer: ULayer[DIDAuthenticationService] = MockDIDAuthenticationService.layer + val didOperationServiceLayer: ULayer[DIDOperationService] = MockDIDOperationService.layer +} + +object HttpModule { + val didApiLayer: ULayer[DIDApi] = { + val serviceLayer = AppModule.didServiceLayer + val apiServiceLayer = serviceLayer >>> DIDApiServiceImpl.layer + val apiMarshallerLayer = DIDApiMarshallerImpl.layer + (apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new DIDApi(_, _)) + } + + val didOperationsApiLayer: ULayer[DIDOperationsApi] = { + val serviceLayer = AppModule.didOperationServiceLayer + val apiServiceLayer = serviceLayer >>> DIDOperationsApiServiceImpl.layer + val apiMarshallerLayer = DIDOperationsApiMarshallerImpl.layer + (apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new DIDOperationsApi(_, _)) + } + + val didAuthenticationApiLayer: ULayer[DIDAuthenticationApi] = { + val serviceLayer = AppModule.didAuthenticationServiceLayer + val apiServiceLayer = serviceLayer >>> DIDAuthenticationApiServiceImpl.layer + val apiMarshallerLayer = DIDAuthenticationApiMarshallerImpl.layer + (apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new DIDAuthenticationApi(_, _)) + } + + val layers = didApiLayer ++ didOperationsApiLayer ++ didAuthenticationApiLayer +} + +object GrpcModule { + val didServiceGrpcLayer: ULayer[DIDServiceGrpc.DIDService] = { + val serviceLayer = AppModule.didServiceLayer + serviceLayer >>> DIDServiceGrpcImpl.layer + } + + val layers = didServiceGrpcLayer +} + +object RepoModule { + val transactorLayer: TaskLayer[Transactor[Task]] = + ZLayer.fromZIO { + Dispatcher[Task].allocated.map { case (dispatcher, _) => + given Dispatcher[Task] = dispatcher + TransactorLayer.hikari[Task]( + TransactorLayer.DbConfig( + username = "postgres", + password = "postgres", + jdbcUrl = "jdbc:postgresql://localhost:5432/castor" + ) + ) + } + }.flatten + + val didOperationRepoLayer: TaskLayer[DIDOperationRepository[Task]] = + transactorLayer >>> JdbcDIDOperationRepository.layer + + val layers = didOperationRepoLayer +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/GrpcServer.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/GrpcServer.scala new file mode 100644 index 0000000000..d8f16b744a --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/GrpcServer.scala @@ -0,0 +1,38 @@ +package io.iohk.atala.castor.apiserver.grpc + +import io.grpc.{ServerBuilder, ServerServiceDefinition} +import io.grpc.protobuf.services.ProtoReflectionService +import io.iohk.atala.castor.proto.castor_api.DIDServiceGrpc.DIDService +import zio.* + +object GrpcServer { + + def start(port: Int, services: Seq[ServerServiceDefinition]): Task[Unit] = { + val managedServer = ZIO.acquireRelease( + for { + _ <- ZIO.logInfo(s"starting grpc server on port $port") + server <- ZIO.attempt { + val builder = ServerBuilder.forPort(port) + services.foreach(s => builder.addService(s)) + builder.addService(ProtoReflectionService.newInstance()) + builder.build().start() + } + _ <- ZIO.logInfo(s"grpc server listening on port $port") + } yield server + )(server => + for { + _ <- ZIO.logInfo("stopping grpc server") + _ <- ZIO.attempt(server.shutdown()).orDie + _ <- ZIO.logInfo("grpc server stopped successfully") + } yield () + ) + + ZIO.scoped { + for { + _ <- managedServer + _ <- ZIO.never + } yield () + } + } + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/GrpcServices.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/GrpcServices.scala new file mode 100644 index 0000000000..d50d0ec387 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/GrpcServices.scala @@ -0,0 +1,16 @@ +package io.iohk.atala.castor.apiserver.grpc + +import akka.actor.typed.ActorSystem +import io.grpc.ServerServiceDefinition +import io.iohk.atala.castor.proto.castor_api.DIDServiceGrpc +import zio.* + +object GrpcServices { + + def services: URIO[DIDServiceGrpc.DIDService, Seq[ServerServiceDefinition]] = + for { + ec <- ZIO.executor.map(_.asExecutionContext) + didService <- ZIO.serviceWith[DIDServiceGrpc.DIDService](DIDServiceGrpc.bindService(_, ec)) + } yield Seq(didService) + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/service/DIDServiceGrpcImpl.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/service/DIDServiceGrpcImpl.scala new file mode 100644 index 0000000000..64532c1056 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/grpc/service/DIDServiceGrpcImpl.scala @@ -0,0 +1,24 @@ +package io.iohk.atala.castor.apiserver.grpc.service + +import io.iohk.atala.castor.core.service.DIDService +import io.iohk.atala.castor.proto.castor_api.{DIDServiceGrpc, Ping, Pong} +import zio.* + +import scala.concurrent.Future + +class DIDServiceGrpcImpl(service: DIDService)(using runtime: Runtime[Any]) extends DIDServiceGrpc.DIDService { + + override def sendPing(request: Ping): Future[Pong] = Unsafe.unsafe { implicit unsafe => + runtime.unsafe.runToFuture(ZIO.succeed(Pong("hello world"))) + } + +} + +object DIDServiceGrpcImpl { + val layer: URLayer[DIDService, DIDServiceGrpc.DIDService] = ZLayer.fromZIO { + for { + rt <- ZIO.runtime[Any] + svc <- ZIO.service[DIDService] + } yield DIDServiceGrpcImpl(svc)(using rt) + } +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/HttpRoutes.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/HttpRoutes.scala new file mode 100644 index 0000000000..b9bfb474f2 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/HttpRoutes.scala @@ -0,0 +1,17 @@ +package io.iohk.atala.castor.apiserver.http + +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.Directives.* +import io.iohk.atala.castor.openapi.api.{DIDApi, DIDAuthenticationApi, DIDOperationsApi} +import zio.* + +object HttpRoutes { + + def routes: URIO[DIDApi & DIDOperationsApi & DIDAuthenticationApi, Route] = + for { + didApi <- ZIO.service[DIDApi] + didOperationsApi <- ZIO.service[DIDOperationsApi] + didAuthApi <- ZIO.service[DIDAuthenticationApi] + } yield didApi.route ~ didOperationsApi.route ~ didAuthApi.route + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/HttpServer.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/HttpServer.scala new file mode 100644 index 0000000000..fadf0582b2 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/HttpServer.scala @@ -0,0 +1,41 @@ +package io.iohk.atala.castor.apiserver.http + +import akka.actor.typed.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Route +import zio.* + +object HttpServer { + + def start(port: Int, routes: Route): RIO[ActorSystem[Nothing], Unit] = { + val managedBinding = + ZIO + .acquireRelease( + for { + system <- ZIO.service[ActorSystem[Nothing]] + _ <- ZIO.logInfo(s"starting http server on port $port") + binding <- ZIO.fromFuture { _ => + given ActorSystem[Nothing] = system + Http().newServerAt("0.0.0.0", port).bind(routes) + } + _ <- ZIO.logInfo(s"http server listening on port $port") + } yield binding + )(binding => + import scala.concurrent.duration.* + for { + _ <- ZIO.logInfo("stopping http server") + _ <- ZIO.fromFuture(_ => binding.terminate(10.seconds)).orDie + _ <- ZIO.logInfo("http server stopped successfully") + } yield () + ) + + ZIO + .scoped { + for { + _ <- managedBinding + _ <- ZIO.never + } yield () + } + } + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDApiMarshallerImpl.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDApiMarshallerImpl.scala new file mode 100644 index 0000000000..eca6c1bffb --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDApiMarshallerImpl.scala @@ -0,0 +1,62 @@ +package io.iohk.atala.castor.apiserver.http.marshaller + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import io.iohk.atala.castor.openapi.api.DIDApiMarshaller +import io.iohk.atala.castor.openapi.model.{ + CreateDIDOperation, + CreateDIDWithProof, + DeactivateDIDOperation, + DeactivateDIDResponse, + ErrorResponse, + PublishDIDResponse, + RecoverDIDOperation, + RecoverDIDWithProof, + ResolveDIDResponse, + UpdateDIDOperation, + UpdateDIDWithProof +} +import spray.json.RootJsonFormat +import zio.* + +object DIDApiMarshallerImpl extends JsonSupport { + + val layer: ULayer[DIDApiMarshaller] = ZLayer.succeed { + // TODO: replace with actual implementation + new DIDApiMarshaller { + implicit def fromEntityUnmarshallerCreateDIDWithProof: FromEntityUnmarshaller[CreateDIDWithProof] = + ??? + + implicit def fromEntityUnmarshallerCreateDIDOperation: FromEntityUnmarshaller[CreateDIDOperation] = + ??? + + implicit def fromEntityUnmarshallerRecoverDIDWithProof: FromEntityUnmarshaller[RecoverDIDWithProof] = + ??? + + implicit def fromEntityUnmarshallerDeactivateDIDOperation: FromEntityUnmarshaller[DeactivateDIDOperation] = + ??? + + implicit def fromEntityUnmarshallerUpdateDIDWithProof: FromEntityUnmarshaller[UpdateDIDWithProof] = + ??? + + implicit def fromEntityUnmarshallerRecoverDIDOperation: FromEntityUnmarshaller[RecoverDIDOperation] = + ??? + + implicit def fromEntityUnmarshallerUpdateDIDOperation: FromEntityUnmarshaller[UpdateDIDOperation] = + ??? + + implicit def toEntityMarshallerDeactivateDIDResponse: ToEntityMarshaller[DeactivateDIDResponse] = + ??? + + implicit def toEntityMarshallerResolveDIDResponse: ToEntityMarshaller[ResolveDIDResponse] = + ??? + + implicit def toEntityMarshallerPublishDIDResponse: ToEntityMarshaller[PublishDIDResponse] = + ??? + + implicit def toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] = + ??? + } + } + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDAuthenticationApiMarshallerImpl.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDAuthenticationApiMarshallerImpl.scala new file mode 100644 index 0000000000..e4abdf53fa --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDAuthenticationApiMarshallerImpl.scala @@ -0,0 +1,35 @@ +package io.iohk.atala.castor.apiserver.http.marshaller + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import io.iohk.atala.castor.openapi.api.DIDAuthenticationApiMarshaller +import io.iohk.atala.castor.openapi.model.* +import spray.json.RootJsonFormat +import zio.* + +object DIDAuthenticationApiMarshallerImpl extends JsonSupport { + + val layer: ULayer[DIDAuthenticationApiMarshaller] = ZLayer.succeed { + new DIDAuthenticationApiMarshaller { + implicit def fromEntityUnmarshallerAuthenticationChallengeSubmissionRequest + : FromEntityUnmarshaller[AuthenticationChallengeSubmissionRequest] = + implicitly[RootJsonFormat[AuthenticationChallengeSubmissionRequest]] + + implicit def fromEntityUnmarshallerCreateAuthenticationChallengeRequest + : FromEntityUnmarshaller[CreateAuthenticationChallengeRequest] = + implicitly[RootJsonFormat[CreateAuthenticationChallengeRequest]] + + implicit def toEntityMarshallerAuthenticationChallengeSubmissionResponse + : ToEntityMarshaller[AuthenticationChallengeSubmissionResponse] = + implicitly[RootJsonFormat[AuthenticationChallengeSubmissionResponse]] + + implicit def toEntityMarshallerCreateAuthenticationChallengeResponse + : ToEntityMarshaller[CreateAuthenticationChallengeResponse] = + implicitly[RootJsonFormat[CreateAuthenticationChallengeResponse]] + + implicit def toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] = + implicitly[RootJsonFormat[ErrorResponse]] + } + } + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDOperationsApiMarshallerImpl.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDOperationsApiMarshallerImpl.scala new file mode 100644 index 0000000000..fb50aa36b7 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/DIDOperationsApiMarshallerImpl.scala @@ -0,0 +1,26 @@ +package io.iohk.atala.castor.apiserver.http.marshaller + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import io.iohk.atala.castor.openapi.api.DIDOperationsApiMarshaller +import io.iohk.atala.castor.openapi.model.{ + ErrorResponse, + GetDIDOperationResponse, + GetDIDOperationsByDIDRefResponseInner +} +import zio.* + +object DIDOperationsApiMarshallerImpl { + + val layer: ULayer[DIDOperationsApiMarshaller] = ZLayer.succeed { + // TODO: replace with actual implementation + new DIDOperationsApiMarshaller { + override implicit def toEntityMarshallerGetDIDOperationsByDIDRefResponseInnerarray + : ToEntityMarshaller[Seq[GetDIDOperationsByDIDRefResponseInner]] = ??? + + override implicit def toEntityMarshallerGetDIDOperationResponse: ToEntityMarshaller[GetDIDOperationResponse] = ??? + + override implicit def toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] = ??? + } + } + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/JsonSupport.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/JsonSupport.scala new file mode 100644 index 0000000000..cbc780dbe5 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/marshaller/JsonSupport.scala @@ -0,0 +1,25 @@ +package io.iohk.atala.castor.apiserver.http.marshaller + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import io.iohk.atala.castor.openapi.model.{ + AuthenticationChallengeSubmissionRequest, + AuthenticationChallengeSubmissionResponse, + CreateAuthenticationChallengeRequest, + CreateAuthenticationChallengeResponse, + ErrorResponse +} +import spray.json.{DefaultJsonProtocol, RootJsonFormat} + +trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { + + given RootJsonFormat[AuthenticationChallengeSubmissionRequest] = jsonFormat3( + AuthenticationChallengeSubmissionRequest.apply + ) + given RootJsonFormat[CreateAuthenticationChallengeRequest] = jsonFormat3(CreateAuthenticationChallengeRequest.apply) + given RootJsonFormat[AuthenticationChallengeSubmissionResponse] = jsonFormat2( + AuthenticationChallengeSubmissionResponse.apply + ) + given RootJsonFormat[CreateAuthenticationChallengeResponse] = jsonFormat2(CreateAuthenticationChallengeResponse.apply) + given RootJsonFormat[ErrorResponse] = jsonFormat5(ErrorResponse.apply) + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/AkkaZioSupport.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/AkkaZioSupport.scala new file mode 100644 index 0000000000..fa4f393cf7 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/AkkaZioSupport.scala @@ -0,0 +1,15 @@ +package io.iohk.atala.castor.apiserver.http.service + +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.Directives.* +import zio.* + +trait AkkaZioSupport { + + protected def onZioSuccess[A](z: UIO[A])(f: A => Route)(using runtime: Runtime[Any]): Route = { + Unsafe.unsafe { implicit unsafe => + onSuccess(runtime.unsafe.runToFuture(z))(f) + } + } + +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDApiServiceImpl.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDApiServiceImpl.scala new file mode 100644 index 0000000000..e83562abd1 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDApiServiceImpl.scala @@ -0,0 +1,73 @@ +package io.iohk.atala.castor.apiserver.http.service + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.server.Route +import io.iohk.atala.castor.core.service.DIDService +import io.iohk.atala.castor.openapi.api.DIDApiService +import io.iohk.atala.castor.openapi.model.{ + CreateDIDOperation, + CreateDIDWithProof, + DeactivateDIDOperation, + DeactivateDIDResponse, + ErrorResponse, + PublishDIDResponse, + RecoverDIDOperation, + RecoverDIDWithProof, + ResolveDIDResponse, + UpdateDIDOperation, + UpdateDIDWithProof +} +import zio.* + +// TODO: replace with actual implementation +class DIDApiServiceImpl(service: DIDService)(using runtime: Runtime[Any]) extends DIDApiService with AkkaZioSupport { + + override def createPublishedDid(createDIDOperation: CreateDIDOperation)(implicit + toEntityMarshallerPublishDIDResponse: ToEntityMarshaller[PublishDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + override def createUnpublishedDid(createDIDWithProof: CreateDIDWithProof)(implicit + toEntityMarshallerPublishDIDResponse: ToEntityMarshaller[PublishDIDResponse] + ): Route = ??? + + override def deactivateDID(didRef: String, deactivateDIDOperation: DeactivateDIDOperation)(implicit + toEntityMarshallerDeactivateDIDResponse: ToEntityMarshaller[DeactivateDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + override def getDID(didRef: String)(implicit + toEntityMarshallerResolveDIDResponse: ToEntityMarshaller[ResolveDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + override def recoverPublishedDid(didRef: String, recoverDIDOperation: RecoverDIDOperation)(implicit + toEntityMarshallerResolveDIDResponse: ToEntityMarshaller[ResolveDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + override def recoverUnpublishedDid(didRef: String, recoverDIDWithProof: RecoverDIDWithProof)(implicit + toEntityMarshallerResolveDIDResponse: ToEntityMarshaller[ResolveDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + override def updatePublishedDid(didRef: String, updateDIDOperation: UpdateDIDOperation)(implicit + toEntityMarshallerResolveDIDResponse: ToEntityMarshaller[ResolveDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + override def updateUnpublishedDid(didRef: String, updateDIDWithProof: UpdateDIDWithProof)(implicit + toEntityMarshallerPublishDIDResponse: ToEntityMarshaller[PublishDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + +} + +object DIDApiServiceImpl { + val layer: URLayer[DIDService, DIDApiService] = ZLayer.fromZIO { + for { + rt <- ZIO.runtime[Any] + svc <- ZIO.service[DIDService] + } yield DIDApiServiceImpl(svc)(using rt) + } +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDAuthenticationApiServiceImpl.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDAuthenticationApiServiceImpl.scala new file mode 100644 index 0000000000..b8eb6b4dd6 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDAuthenticationApiServiceImpl.scala @@ -0,0 +1,67 @@ +package io.iohk.atala.castor.apiserver.http.service + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.Directives.* +import io.iohk.atala.castor.core.service.DIDAuthenticationService +import io.iohk.atala.castor.openapi.api.DIDAuthenticationApiService +import io.iohk.atala.castor.openapi.model.{ + AuthenticationChallengeSubmissionRequest, + AuthenticationChallengeSubmissionResponse, + CreateAuthenticationChallengeRequest, + CreateAuthenticationChallengeResponse, + ErrorResponse +} +import zio.* + +// TODO: replace with actual implementation +class DIDAuthenticationApiServiceImpl(service: DIDAuthenticationService)(using runtime: Runtime[Any]) + extends DIDAuthenticationApiService + with AkkaZioSupport { + + override def createDidAuthenticationChallenge( + createAuthenticationChallengeRequest: CreateAuthenticationChallengeRequest + )(implicit + toEntityMarshallerCreateAuthenticationChallengeResponse: ToEntityMarshaller[ + CreateAuthenticationChallengeResponse + ], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = { + onZioSuccess(ZIO.unit) { _ => + createDidAuthenticationChallenge200( + CreateAuthenticationChallengeResponse( + challenge = "eyJhbGciOiJIUzI1NiIsInR5c...0eu8Ri_WSPSsBTlCes2YMpuB1mHU", + subject = Some("did:example:123456789abcdefghi") + ) + ) + } + } + + override def createDidAuthenticationChallengeSubmission( + authenticationChallengeSubmissionRequest: AuthenticationChallengeSubmissionRequest + )(implicit + toEntityMarshallerAuthenticationChallengeSubmissionResponse: ToEntityMarshaller[ + AuthenticationChallengeSubmissionResponse + ], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = { + onZioSuccess(ZIO.unit) { _ => + createDidAuthenticationChallengeSubmission200( + AuthenticationChallengeSubmissionResponse( + success = true, + state = Some("af72a673-7fb5-463d-9966-6a8c6a2cc2e8") + ) + ) + } + } + +} + +object DIDAuthenticationApiServiceImpl { + val layer: URLayer[DIDAuthenticationService, DIDAuthenticationApiService] = ZLayer.fromZIO { + for { + rt <- ZIO.runtime[Any] + svc <- ZIO.service[DIDAuthenticationService] + } yield DIDAuthenticationApiServiceImpl(svc)(using rt) + } +} diff --git a/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDOperationsApiServiceImpl.scala b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDOperationsApiServiceImpl.scala new file mode 100644 index 0000000000..abac632866 --- /dev/null +++ b/castor/service/api-server/src/main/scala/io/iohk/atala/castor/apiserver/http/service/DIDOperationsApiServiceImpl.scala @@ -0,0 +1,40 @@ +package io.iohk.atala.castor.apiserver.http.service + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.server.Route +import io.iohk.atala.castor.core.service.DIDOperationService +import io.iohk.atala.castor.openapi.api.DIDOperationsApiService +import io.iohk.atala.castor.openapi.model.{ + ErrorResponse, + GetDIDOperationResponse, + GetDIDOperationsByDIDRefResponseInner +} +import zio.* + +// TODO: replace with actual implementation +class DIDOperationsApiServiceImpl(service: DIDOperationService)(using runtime: Runtime[Any]) + extends DIDOperationsApiService + with AkkaZioSupport { + + override def getDidOperation(didOperationRef: String)(implicit + toEntityMarshallerGetDIDOperationResponse: ToEntityMarshaller[GetDIDOperationResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + override def getDidOperationsByDidRef(didRef: String)(implicit + toEntityMarshallerGetDIDOperationsByDIDRefResponseInnerarray: ToEntityMarshaller[ + Seq[GetDIDOperationsByDIDRefResponseInner] + ], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + +} + +object DIDOperationsApiServiceImpl { + val layer: URLayer[DIDOperationService, DIDOperationsApiService] = ZLayer.fromZIO { + for { + rt <- ZIO.runtime[Any] + svc <- ZIO.service[DIDOperationService] + } yield DIDOperationsApiServiceImpl(svc)(using rt) + } +} diff --git a/castor/service/build.sbt b/castor/service/build.sbt new file mode 100644 index 0000000000..f86fee68fd --- /dev/null +++ b/castor/service/build.sbt @@ -0,0 +1,53 @@ +import Dependencies._ + +ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / scalaVersion := "3.1.3" +ThisBuild / organization := "io.iohk.atala" + +// Custom keys +val apiBaseDirectory = settingKey[File]("The base directory for Castor API specifications") +ThisBuild / apiBaseDirectory := baseDirectory.value / "../api" + +// Project definitions +lazy val root = project + .in(file(".")) + .aggregate(core, sql, `api-server`, worker) + +lazy val core = project + .in(file("core")) + .settings( + name := "castor-core", + libraryDependencies ++= coreDependencies + ) + +lazy val sql = project + .in(file("sql")) + .settings( + name := "castor-sql", + libraryDependencies ++= sqlDependencies + ) + .dependsOn(core) + +lazy val `api-server` = project + .in(file("api-server")) + .settings( + name := "castor-api-server", + libraryDependencies ++= apiServerDependencies, + // OpenAPI settings + Compile / sourceGenerators += openApiGenerateClasses, + openApiGeneratorSpec := apiBaseDirectory.value / "http/castor-openapi-spec.yaml", + openApiGeneratorConfig := baseDirectory.value / "openapi/generator-config/config.yaml", + // gRPC settings + Compile / PB.targets := Seq(scalapb.gen() -> (Compile / sourceManaged).value / "scalapb"), + Compile / PB.protoSources := Seq(apiBaseDirectory.value / "grpc") + ) + .enablePlugins(OpenApiGeneratorPlugin) + .dependsOn(core, sql) + +lazy val worker = project + .in(file("worker")) + .settings( + name := "castor-worker", + libraryDependencies ++= workerDependencies + ) + .dependsOn(core, sql) diff --git a/castor/service/core/src/main/scala/io/iohk/atala/castor/core/model/Models.scala b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/model/Models.scala new file mode 100644 index 0000000000..95ced2bdab --- /dev/null +++ b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/model/Models.scala @@ -0,0 +1,6 @@ +package io.iohk.atala.castor.core.model + +// TODO: replace with actual implementation +final case class IrisNotification(foo: String) + +final case class PublishedDIDOperation(foo: String) diff --git a/castor/service/core/src/main/scala/io/iohk/atala/castor/core/repository/DIDOperationRepository.scala b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/repository/DIDOperationRepository.scala new file mode 100644 index 0000000000..a26cb409ab --- /dev/null +++ b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/repository/DIDOperationRepository.scala @@ -0,0 +1,9 @@ +package io.iohk.atala.castor.core.repository + +import io.iohk.atala.castor.core.model.PublishedDIDOperation +import zio.* + +// TODO: replace with actual implementation +trait DIDOperationRepository[F[_]] { + def getPublishedOperations: F[Seq[PublishedDIDOperation]] +} diff --git a/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDAuthenticationService.scala b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDAuthenticationService.scala new file mode 100644 index 0000000000..6b0f53b207 --- /dev/null +++ b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDAuthenticationService.scala @@ -0,0 +1,20 @@ +package io.iohk.atala.castor.core.service + +import zio.* + +import scala.concurrent.duration.FiniteDuration + +// TODO: replace with actual implementation +trait DIDAuthenticationService { + def createAuthChallenge(state: Option[String], ttl: FiniteDuration): UIO[Unit] + def createAuthChallengeSubmission(challenge: String, signature: String): UIO[Unit] +} + +object MockDIDAuthenticationService { + val layer: ULayer[DIDAuthenticationService] = ZLayer.succeed { + new DIDAuthenticationService { + override def createAuthChallenge(state: Option[String], ttl: FiniteDuration): UIO[Unit] = ZIO.unit + override def createAuthChallengeSubmission(challenge: String, signature: String): UIO[Unit] = ZIO.unit + } + } +} diff --git a/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDOperationService.scala b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDOperationService.scala new file mode 100644 index 0000000000..580e9e8b8b --- /dev/null +++ b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDOperationService.scala @@ -0,0 +1,16 @@ +package io.iohk.atala.castor.core.service + +import zio.* + +// TODO: replace with actual implementation +trait DIDOperationService { + def getDIDOperations(did: String): UIO[Unit] +} + +object MockDIDOperationService { + val layer: ULayer[DIDOperationService] = ZLayer.succeed { + new DIDOperationService { + override def getDIDOperations(did: String): UIO[Unit] = ZIO.unit + } + } +} diff --git a/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDService.scala b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDService.scala new file mode 100644 index 0000000000..7b101e4fa7 --- /dev/null +++ b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/DIDService.scala @@ -0,0 +1,16 @@ +package io.iohk.atala.castor.core.service + +import zio.* + +// TODO: replace with actual implementation +trait DIDService { + def resolveDID(did: String): UIO[Unit] +} + +object MockDIDService { + val layer: ULayer[DIDService] = ZLayer.succeed { + new DIDService { + override def resolveDID(did: String): UIO[Unit] = ZIO.unit + } + } +} diff --git a/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/IrisNotificationService.scala b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/IrisNotificationService.scala new file mode 100644 index 0000000000..9dff248dd3 --- /dev/null +++ b/castor/service/core/src/main/scala/io/iohk/atala/castor/core/service/IrisNotificationService.scala @@ -0,0 +1,17 @@ +package io.iohk.atala.castor.core.service + +import io.iohk.atala.castor.core.model.IrisNotification +import zio.* + +// TODO: replace with actual implementation +trait IrisNotificationService { + def processNotification(notification: IrisNotification): UIO[Unit] +} + +object MockIrisNotificationService { + val layer: ULayer[IrisNotificationService] = ZLayer.succeed { + new IrisNotificationService { + override def processNotification(notification: IrisNotification): UIO[Unit] = ZIO.unit + } + } +} diff --git a/castor/service/docker/docker-compose-local.yaml b/castor/service/docker/docker-compose-local.yaml new file mode 100644 index 0000000000..2835e5b37e --- /dev/null +++ b/castor/service/docker/docker-compose-local.yaml @@ -0,0 +1,34 @@ +version: "3.9" + +services: + db: + image: postgres:13 + restart: always + environment: + POSTGRES_DB: castor + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + volumes: + - pg_data_castor_db:/var/lib/postgresql/data + + # delay to ensure DB is up before applying migrations + db_init_delay: + image: alpine:3 + command: sleep 5 + depends_on: + db: + condition: service_started + + db_init: + image: flyway/flyway:9.3.0-alpine + volumes: + - $PWD/migrations/sql:/flyway/sql + command: -url=jdbc:postgresql://db:5432/castor?user=postgres&password=postgres migrate + depends_on: + db_init_delay: + condition: service_completed_successfully + +volumes: + pg_data_castor_db: diff --git a/castor/service/migrations/sql/V1__init_tables.sql b/castor/service/migrations/sql/V1__init_tables.sql new file mode 100644 index 0000000000..847f024493 --- /dev/null +++ b/castor/service/migrations/sql/V1__init_tables.sql @@ -0,0 +1,3 @@ +CREATE TABLE public.published_did_operations( + "foo" VARCHAR(100) NOT NULL +); diff --git a/castor/service/project/Dependencies.scala b/castor/service/project/Dependencies.scala new file mode 100644 index 0000000000..9a7fe0c79a --- /dev/null +++ b/castor/service/project/Dependencies.scala @@ -0,0 +1,40 @@ +import sbt._ + +object Dependencies { + object Versions { + val zio = "2.0.2" + val akka = "2.6.19" + val akkaHttp = "10.2.9" + val doobie = "1.0.0-RC2" + val zioCatsInterop = "3.3.0" + } + + private lazy val zio = "dev.zio" %% "zio" % Versions.zio + private lazy val zioStream = "dev.zio" %% "zio-streams" % Versions.zio + private lazy val zioCatsInterop = "dev.zio" %% "zio-interop-cats" % Versions.zioCatsInterop + + private lazy val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % Versions.akka + private lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % Versions.akka + private lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % Versions.akkaHttp + private lazy val akkaSprayJson = "com.typesafe.akka" %% "akka-http-spray-json" % Versions.akkaHttp + + private lazy val grpcNetty = "io.grpc" % "grpc-netty" % scalapb.compiler.Version.grpcJavaVersion + private lazy val grpcServices = "io.grpc" % "grpc-services" % scalapb.compiler.Version.grpcJavaVersion + private lazy val scalaPbProto = "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf" + private lazy val scalaPbGrpc = "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion + + private lazy val doobiePostgres = "org.tpolecat" %% "doobie-postgres" % Versions.doobie + private lazy val doobieHikari = "org.tpolecat" %% "doobie-hikari" % Versions.doobie + + // Dependency Modules + private lazy val baseDependencies: Seq[ModuleID] = Seq(zio) + private lazy val akkaHttpDependencies: Seq[ModuleID] = Seq(akkaTyped, akkaStream, akkaHttp, akkaSprayJson).map(_.cross(CrossVersion.for3Use2_13)) + private lazy val grpcDependencies: Seq[ModuleID] = Seq(grpcNetty, grpcServices, scalaPbProto, scalaPbGrpc) + private lazy val doobieDependencies: Seq[ModuleID] = Seq(doobiePostgres, doobieHikari) + + // Project Dependencies + lazy val coreDependencies: Seq[ModuleID] = baseDependencies + lazy val sqlDependencies: Seq[ModuleID] = baseDependencies ++ doobieDependencies ++ Seq(zioCatsInterop) + lazy val apiServerDependencies: Seq[ModuleID] = baseDependencies ++ akkaHttpDependencies ++ grpcDependencies + lazy val workerDependencies: Seq[ModuleID] = baseDependencies ++ Seq(zioStream) +} diff --git a/castor/service/project/OpenApiGeneratorPlugin.scala b/castor/service/project/OpenApiGeneratorPlugin.scala new file mode 100644 index 0000000000..92cfbdc4db --- /dev/null +++ b/castor/service/project/OpenApiGeneratorPlugin.scala @@ -0,0 +1,29 @@ +import sbt.Keys.sourceManaged +import sbt.io.syntax._ +import sbt.{AutoPlugin, Compile, Def, File, settingKey, taskKey} + +object OpenApiGeneratorPlugin extends AutoPlugin { + + object autoImport { + val openApiGeneratorSpec = settingKey[File]("The OpenAPI specification file.") + val openApiGeneratorConfig = settingKey[File]("The generator config file.") + val openApiGenerateClasses = taskKey[Seq[File]]("Generate API & model classes.") + } + + import autoImport._ + override def projectSettings: Seq[Def.Setting[_]] = Seq( + openApiGenerateClasses := { + import org.openapitools.codegen.DefaultGenerator + import org.openapitools.codegen.config.CodegenConfigurator + + import scala.collection.JavaConverters._ + val configurator = CodegenConfigurator.fromFile(openApiGeneratorConfig.value.getPath) + configurator.setInputSpec(openApiGeneratorSpec.value.getPath) + configurator.setOutputDir(((Compile / sourceManaged).value / "openapi").getPath) + configurator.setValidateSpec(true) + val gen = new DefaultGenerator() + gen.opts(configurator.toClientOptInput) + gen.generate().asScala + } + ) +} diff --git a/castor/service/project/build.properties b/castor/service/project/build.properties new file mode 100644 index 0000000000..22af2628c4 --- /dev/null +++ b/castor/service/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.7.1 diff --git a/castor/service/project/build.sbt b/castor/service/project/build.sbt new file mode 100644 index 0000000000..2625ab2264 --- /dev/null +++ b/castor/service/project/build.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") + +libraryDependencies ++= Seq("org.openapitools" % "openapi-generator" % "6.0.0") diff --git a/castor/service/project/scalapb.sbt b/castor/service/project/scalapb.sbt new file mode 100644 index 0000000000..151b8585cb --- /dev/null +++ b/castor/service/project/scalapb.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.3") + +libraryDependencies ++= Seq("com.thesamet.scalapb" %% "compilerplugin" % "0.11.11") diff --git a/castor/service/sql/src/main/scala/io/iohk/atala/castor/sql/repository/JdbcDIDOperationRepository.scala b/castor/service/sql/src/main/scala/io/iohk/atala/castor/sql/repository/JdbcDIDOperationRepository.scala new file mode 100644 index 0000000000..8869f79e2a --- /dev/null +++ b/castor/service/sql/src/main/scala/io/iohk/atala/castor/sql/repository/JdbcDIDOperationRepository.scala @@ -0,0 +1,28 @@ +package io.iohk.atala.castor.sql.repository + +import doobie.* +import doobie.implicits.* +import io.iohk.atala.castor.core.model.PublishedDIDOperation +import io.iohk.atala.castor.core.repository.DIDOperationRepository +import zio.* +import zio.interop.catz.* + +// TODO: replace with actual implementation +class JdbcDIDOperationRepository(xa: Transactor[Task]) extends DIDOperationRepository[Task] { + + override def getPublishedOperations: Task[Seq[PublishedDIDOperation]] = { + val cxnIO = sql""" + |SELECT foo FROM public.published_did_operations + |""".stripMargin.query[String].to[Seq] + + cxnIO + .transact(xa) + .map(_.map(PublishedDIDOperation.apply)) + } + +} + +object JdbcDIDOperationRepository { + val layer: URLayer[Transactor[Task], DIDOperationRepository[Task]] = + ZLayer.fromFunction(new JdbcDIDOperationRepository(_)) +} diff --git a/castor/service/sql/src/main/scala/io/iohk/atala/castor/sql/repository/TransactorLayer.scala b/castor/service/sql/src/main/scala/io/iohk/atala/castor/sql/repository/TransactorLayer.scala new file mode 100644 index 0000000000..0e6326fe89 --- /dev/null +++ b/castor/service/sql/src/main/scala/io/iohk/atala/castor/sql/repository/TransactorLayer.scala @@ -0,0 +1,63 @@ +package io.iohk.atala.castor.sql.repository + +import cats.effect.{Async, Resource} +import doobie.util.transactor.Transactor +import com.zaxxer.hikari.HikariConfig +import doobie.util.ExecutionContexts +import doobie.hikari.HikariTransactor +import zio.interop.catz.* +import zio.* +import cats.effect.std.Dispatcher + +object TransactorLayer { + + case class DbConfig( + username: String, + password: String, + jdbcUrl: String, + awaitConnectionThreads: Int = 8 + ) + + def hikari[A[_]: Async: Dispatcher](config: DbConfig)(using tag: Tag[Transactor[A]]): TaskLayer[Transactor[A]] = { + val transactorLayerZio = ZIO + .attempt { + // https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + val poolSize = (config.awaitConnectionThreads * 2) + 1 + val hikariConfig = makeHikariConfig(config) + hikariConfig.setPoolName("DBPool") + hikariConfig.setLeakDetectionThreshold(300000) // 5 mins + hikariConfig.setMinimumIdle(poolSize) + hikariConfig.setMaximumPoolSize(poolSize) // Both Pool size amd Minimum Idle should same and is recommended + hikariConfig + } + .map { hikariConfig => + val pool: Resource[A, Transactor[A]] = for { + // Resource yielding a transactor configured with a bounded connect EC and an unbounded + // transaction EC. Everything will be closed and shut down cleanly after use. + ec <- ExecutionContexts.fixedThreadPool[A](config.awaitConnectionThreads) // our connect EC + xa <- HikariTransactor.fromHikariConfig[A](hikariConfig, ec) + } yield xa + + pool.toManaged.toLayer[Transactor[A]] + } + + ZLayer.fromZIO(transactorLayerZio).flatten + } + + private def makeHikariConfig(config: DbConfig): HikariConfig = { + val hikariConfig = HikariConfig() + + hikariConfig.setJdbcUrl(config.jdbcUrl) + hikariConfig.setUsername(config.username) + hikariConfig.setPassword(config.password) + hikariConfig.setAutoCommit(false) + + hikariConfig.setDriverClassName("org.postgresql.Driver") + hikariConfig.addDataSourceProperty("cachePrepStmts", "true") + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250") + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048") + + hikariConfig + } + +} diff --git a/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/Main.scala b/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/Main.scala new file mode 100644 index 0000000000..4a522a322f --- /dev/null +++ b/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/Main.scala @@ -0,0 +1,7 @@ +package io.iohk.atala.castor.worker + +import zio.* + +object Main extends ZIOAppDefault { + override def run = Modules.app +} diff --git a/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/Modules.scala b/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/Modules.scala new file mode 100644 index 0000000000..9fe3c0d2de --- /dev/null +++ b/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/Modules.scala @@ -0,0 +1,33 @@ +package io.iohk.atala.castor.worker + +import io.iohk.atala.castor.core.model.IrisNotification +import io.iohk.atala.castor.core.service.MockIrisNotificationService +import io.iohk.atala.castor.worker.app.EventConsumer +import zio.* +import zio.stream.ZStream + +object Modules { + + // TODO: replace with actual implementation + val irisNotificationSource: UIO[ZStream[Any, Nothing, IrisNotification]] = ZIO.succeed { + ZStream + .tick(1.seconds) + .as(IrisNotification(foo = "bar")) + } + + val eventConsumerLayer: ULayer[EventConsumer] = { + val serviceLayer = MockIrisNotificationService.layer // TODO: replace with actual implementation + serviceLayer >>> EventConsumer.layer + } + + val app: UIO[Unit] = { + val consumerApp = for { + source <- irisNotificationSource + consumer <- ZIO.service[EventConsumer] + _ <- consumer.consumeIrisNotification(source) + } yield () + + consumerApp.provideLayer(eventConsumerLayer) + } + +} diff --git a/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/app/EventConsumer.scala b/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/app/EventConsumer.scala new file mode 100644 index 0000000000..20c29b8430 --- /dev/null +++ b/castor/service/worker/src/main/scala/io/iohk/atala/castor/worker/app/EventConsumer.scala @@ -0,0 +1,19 @@ +package io.iohk.atala.castor.worker.app + +import io.iohk.atala.castor.core.model.IrisNotification +import io.iohk.atala.castor.core.service.IrisNotificationService +import zio.* +import zio.stream.{ZSink, ZStream} + +class EventConsumer(irisNotificationService: IrisNotificationService) { + + // TODO: replace with actual implementation + def consumeIrisNotification(source: ZStream[Any, Nothing, IrisNotification]): UIO[Unit] = { + source.foreach(irisNotificationService.processNotification) + } + +} + +object EventConsumer { + val layer: URLayer[IrisNotificationService, EventConsumer] = ZLayer.fromFunction(new EventConsumer(_)) +}