Skip to content

Commit

Permalink
[ATL-1759] feat(castor): castor services scaffolding (#24)
Browse files Browse the repository at this point in the history
* feat(castor): create empty scala project

* feat(castor): bootstrap castor subprojects

* feat(castor): add castor http server subproject

* feat(castor): add guardrail openapi codegen

* feat(castor): update gitignore

* feat(castor): use OpenAPI-Generator for server codegen

* feat(castor): update dependencies for openapi codegen

* feat(castor): restructor openapi directory

* feat(castor): add sample http-server

* feat(castor): reorganize actorsystem layer

* feat(castor): add template for ApiService generated file

* feat(castor): add AkkaZioSupport

* feat(castor): add mock resonse and adjust openapi spec

* feat(castor): add worker subproject

* feat(castor): add DIDApi api group

* feat(castor): add DIDOperations api group

* feat(castor): add EventConsumer for worker process

* feat(castor): re-organize structure to account for grpc module

* feat(castor): add grpc dependencies

* feat(castor): remove models subproject

* feat(castor): add grpc server

* feat(castor): format sbt definitions

* feat(castor): add grpc services

* feat(castor): fix openapi spec unwanted edits

* feat(castor): add local docker-compose db and migrations

* feat(castor): add repository module

* feat(castor): initializing transactorLayer type can fail

* feat(castor): change doobie Transactor to use hikari pool
  • Loading branch information
patlo-iog authored Sep 15, 2022
1 parent e47242c commit 45a18c9
Show file tree
Hide file tree
Showing 52 changed files with 1,558 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ megalinter-reports
**/project/metals.sbt

target

shell.nix
.envrc
22 changes: 22 additions & 0 deletions castor/api/grpc/castor_api.proto
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"

/did-authentication/v1/challenges:
/did-authentication/challenges:
post:
tags: ["DID Authentication"]
operationId: createDidAuthenticationChallenge
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions castor/service/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
4 changes: 4 additions & 0 deletions castor/service/.scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version = 3.5.8
runner.dialect = scala3

maxColumn = 120
10 changes: 10 additions & 0 deletions castor/service/README.md
Original file line number Diff line number Diff line change
@@ -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
```
17 changes: 17 additions & 0 deletions castor/service/api-server/openapi/generator-config/config.yaml
Original file line number Diff line number Diff line change
@@ -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:
Original file line number Diff line number Diff line change
@@ -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}}
Original file line number Diff line number Diff line change
@@ -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}}
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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}}
Original file line number Diff line number Diff line change
@@ -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}}
}
Loading

0 comments on commit 45a18c9

Please sign in to comment.