Skip to content

Commit

Permalink
refactor: Introduce tapir on Pekko (#2870)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcin Procyk <[email protected]>
  • Loading branch information
seakayone and mpro7 authored Oct 11, 2023
1 parent ff4d3a1 commit 08accab
Show file tree
Hide file tree
Showing 15 changed files with 455 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import org.knora.webapi.responders.v2.ontology.OntologyHelpers
import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive
import org.knora.webapi.routing._
import org.knora.webapi.routing.admin.AuthenticatorService
import org.knora.webapi.routing.admin.ProjectsEndpoints
import org.knora.webapi.routing.admin.ProjectsEndpointsHandlerF
import org.knora.webapi.routing.admin.ProjectsRouteZ
import org.knora.webapi.slice.admin.api.service.ProjectADMRestService
import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive
Expand Down Expand Up @@ -131,11 +133,13 @@ object LayersTest {

private val commonLayersForAllIntegrationTests =
ZLayer.makeSome[CommonR0, CommonR](
HandlerMapperF.layer,
ApiRoutes.layer,
AppRouter.layer,
AuthenticationMiddleware.layer,
AuthenticatorLive.layer,
AuthenticatorService.layer,
BaseEndpoints.layer,
CacheServiceInMemImpl.layer,
CacheServiceRequestMessageHandlerLive.layer,
CardinalityHandlerLive.layer,
Expand All @@ -157,6 +161,7 @@ object LayersTest {
MessageRelayLive.layer,
OntologyCacheLive.layer,
OntologyHelpersLive.layer,
OntologyInferencer.layer,
OntologyRepoLive.layer,
OntologyResponderV2Live.layer,
PermissionUtilADMLive.layer,
Expand All @@ -168,6 +173,8 @@ object LayersTest {
ProjectExportStorageServiceLive.layer,
ProjectImportServiceLive.layer,
ProjectsADMRestServiceLive.layer,
ProjectsEndpoints.layer,
ProjectsEndpointsHandlerF.layer,
ProjectsResponderADMLive.layer,
ProjectsRouteZ.layer,
QueryTraverser.layer,
Expand All @@ -181,7 +188,6 @@ object LayersTest {
RestResourceInfoService.layer,
SearchResponderV2Live.layer,
SipiResponderADMLive.layer,
OntologyInferencer.layer,
StandoffResponderV2Live.layer,
StandoffTagUtilV2Live.layer,
State.layer,
Expand Down
50 changes: 32 additions & 18 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package org.knora

import sbt.*

import scala.collection.immutable.Seq

object Dependencies {

val fusekiImage =
Expand All @@ -30,21 +32,19 @@ object Dependencies {
val ZioVersion = "2.0.18"

// ZIO - all Scala 3 compatible
val zio = "dev.zio" %% "zio" % ZioVersion
val zioConfig = "dev.zio" %% "zio-config" % ZioConfigVersion
val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % ZioConfigVersion
val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % ZioConfigVersion
val zioHttpOld = "io.d11" %% "zhttp" % ZioHttpVersionOld
val zioHttp = "dev.zio" %% "zio-http" % ZioHttpVersion
val zioJson = "dev.zio" %% "zio-json" % "0.6.2"
val zioLogging = "dev.zio" %% "zio-logging" % ZioLoggingVersion
val zioLoggingSlf4jBridge = "dev.zio" %% "zio-logging-slf4j2-bridge" % ZioLoggingVersion
val zioNio = "dev.zio" %% "zio-nio" % ZioNioVersion
val zioMacros = "dev.zio" %% "zio-macros" % ZioVersion
val zioMetricsConnectors = "dev.zio" %% "zio-metrics-connectors" % ZioMetricsConnectorsVersion
val zioMetricsPrometheusConnector = "dev.zio" %% "zio-metrics-connectors-prometheus" % ZioMetricsConnectorsVersion
val zioPrelude = "dev.zio" %% "zio-prelude" % ZioPreludeVersion
val zioSttp = "com.softwaremill.sttp.client3" %% "zio" % "3.9.0"
val zio = "dev.zio" %% "zio" % ZioVersion
val zioConfig = "dev.zio" %% "zio-config" % ZioConfigVersion
val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % ZioConfigVersion
val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % ZioConfigVersion
val zioHttpOld = "io.d11" %% "zhttp" % ZioHttpVersionOld
val zioHttp = "dev.zio" %% "zio-http" % ZioHttpVersion
val zioJson = "dev.zio" %% "zio-json" % "0.6.2"
val zioLogging = "dev.zio" %% "zio-logging" % ZioLoggingVersion
val zioLoggingSlf4jBridge = "dev.zio" %% "zio-logging-slf4j2-bridge" % ZioLoggingVersion
val zioNio = "dev.zio" %% "zio-nio" % ZioNioVersion
val zioMacros = "dev.zio" %% "zio-macros" % ZioVersion
val zioPrelude = "dev.zio" %% "zio-prelude" % ZioPreludeVersion
val zioSttp = "com.softwaremill.sttp.client3" %% "zio" % "3.9.0"

// refined
val refined = Seq(
Expand Down Expand Up @@ -125,6 +125,22 @@ object Dependencies {
// found/added by the plugin but deleted anyway
val commonsLang3 = "org.apache.commons" % "commons-lang3" % "3.13.0"

val tapirVersion = "1.7.6"

val tapir = Seq(
"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion,
// "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-spray" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.2.10"
)
val metrics = Seq(
"dev.zio" %% "zio-metrics-connectors" % ZioMetricsConnectorsVersion,
"dev.zio" %% "zio-metrics-connectors-prometheus" % ZioMetricsConnectorsVersion,
"com.softwaremill.sttp.tapir" %% "tapir-zio-metrics" % tapirVersion
)

val integrationTestDependencies = Seq(
pekkoHttpTestkit,
pekkoStreamTestkit,
Expand Down Expand Up @@ -178,10 +194,8 @@ object Dependencies {
zioLogging,
zioLoggingSlf4jBridge,
zioMacros,
zioMetricsConnectors,
zioMetricsPrometheusConnector,
zioNio,
zioPrelude,
zioSttp
)
) ++ metrics ++ tapir
}
8 changes: 7 additions & 1 deletion webapi/src/main/scala/dsp/errors/Errors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package dsp.errors
import com.typesafe.scalalogging.Logger
import org.apache.commons.lang3.SerializationException
import org.apache.commons.lang3.SerializationUtils
import zio.json.DeriveJsonCodec
import zio.json.JsonCodec

/*
Expand Down Expand Up @@ -90,6 +92,8 @@ object BadRequestException {
BadRequestException(s"Invalid value for query parameter '$key'")
def missingQueryParamValue(key: String): BadRequestException =
BadRequestException(s"Missing query parameter '$key'")

implicit val codec: JsonCodec[BadRequestException] = DeriveJsonCodec.gen[BadRequestException]
}

/**
Expand All @@ -113,7 +117,9 @@ case class ForbiddenException(message: String) extends RequestRejectedException(
*/
case class NotFoundException(message: String) extends RequestRejectedException(message)
object NotFoundException {
val notFound = NotFoundException("The requested data was not found")
val notFound: NotFoundException = NotFoundException("The requested data was not found")

implicit val codec: JsonCodec[NotFoundException] = DeriveJsonCodec.gen[NotFoundException]
}

/**
Expand Down
10 changes: 6 additions & 4 deletions webapi/src/main/scala/dsp/valueobjects/Iri.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package dsp.valueobjects
import com.google.gwt.safehtml.shared.UriUtils.encodeAllowEscapes
import org.apache.commons.lang3.StringUtils
import org.apache.commons.validator.routines.UrlValidator
import zio.json.JsonCodec
import zio.json.JsonDecoder
import zio.json.JsonEncoder
import zio.prelude.Validation
Expand Down Expand Up @@ -233,10 +234,11 @@ object Iri {
*/
sealed abstract case class ProjectIri private (value: String) extends Iri
object ProjectIri { self =>
implicit val decoder: JsonDecoder[ProjectIri] =
JsonDecoder[String].mapOrFail(value => ProjectIri.make(value).toEitherWith(e => e.head.getMessage))
implicit val encoder: JsonEncoder[ProjectIri] =
JsonEncoder[String].contramap((projectIri: ProjectIri) => projectIri.value)

implicit val codec: JsonCodec[ProjectIri] = new JsonCodec[ProjectIri](
JsonEncoder[String].contramap(_.value),
JsonDecoder[String].mapOrFail(ProjectIri.make(_).toEitherWith(e => e.head.getMessage))
)

def make(value: String): Validation[ValidationException, ProjectIri] =
if (value.isEmpty) Validation.fail(ValidationException(IriErrorMessages.ProjectIriMissing))
Expand Down
8 changes: 7 additions & 1 deletion webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import org.knora.webapi.responders.v2.ontology.OntologyHelpers
import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive
import org.knora.webapi.routing._
import org.knora.webapi.routing.admin.AuthenticatorService
import org.knora.webapi.routing.admin.ProjectsEndpoints
import org.knora.webapi.routing.admin.ProjectsEndpointsHandlerF
import org.knora.webapi.routing.admin.ProjectsRouteZ
import org.knora.webapi.slice.admin.api.service.ProjectADMRestService
import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive
Expand Down Expand Up @@ -136,6 +138,7 @@ object LayersLive {
AuthenticationMiddleware.layer,
AuthenticatorLive.layer,
AuthenticatorService.layer,
BaseEndpoints.layer,
CacheServiceInMemImpl.layer,
CacheServiceRequestMessageHandlerLive.layer,
CardinalityHandlerLive.layer,
Expand All @@ -145,6 +148,7 @@ object LayersLive {
DspIngestClientLive.layer,
GravsearchTypeInspectionRunner.layer,
GroupsResponderADMLive.layer,
HandlerMapperF.layer,
HttpServer.layer,
HttpServerZ.layer, // this is the new ZIO HTTP server layer
IIIFRequestMessageHandlerLive.layer,
Expand All @@ -159,6 +163,7 @@ object LayersLive {
MessageRelayLive.layer,
OntologyCacheLive.layer,
OntologyHelpersLive.layer,
OntologyInferencer.layer,
OntologyRepoLive.layer,
OntologyResponderV2Live.layer,
PermissionUtilADMLive.layer,
Expand All @@ -170,6 +175,8 @@ object LayersLive {
ProjectExportStorageServiceLive.layer,
ProjectImportServiceLive.layer,
ProjectsADMRestServiceLive.layer,
ProjectsEndpoints.layer,
ProjectsEndpointsHandlerF.layer,
ProjectsResponderADMLive.layer,
ProjectsRouteZ.layer,
QueryTraverser.layer,
Expand All @@ -183,7 +190,6 @@ object LayersLive {
RestResourceInfoService.layer,
SearchResponderV2Live.layer,
SipiResponderADMLive.layer,
OntologyInferencer.layer,
StandoffResponderV2Live.layer,
StandoffTagUtilV2Live.layer,
State.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ sealed trait ProjectIdentifierADM { self =>

object ProjectIdentifierADM {

def from(projectIri: ProjectIri): ProjectIdentifierADM =
IriIdentifier(projectIri)

/**
* Represents [[IriIdentifier]] identifier.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.knora.webapi.responders.admin
import com.typesafe.scalalogging.LazyLogging
import zio._
import zio.macros.accessible

import java.util.UUID

Expand Down Expand Up @@ -48,6 +49,7 @@ import org.knora.webapi.util.ZioHelper
/**
* Returns information about projects.
*/
@accessible
trait ProjectsResponderADM {

/**
Expand Down Expand Up @@ -390,8 +392,7 @@ final case class ProjectsResponderADMLive(
id <- IriIdentifier.fromString(projectIri.value).toZIO.mapError(e => BadRequestException(e.getMessage))
keywords <- projectService
.findProjectKeywordsBy(id)
.flatMap(ZIO.fromOption(_))
.orElseFail(NotFoundException(s"Project '${projectIri.value}' not found."))
.someOrFail(NotFoundException(s"Project '${projectIri.value}' not found."))
} yield keywords

/**
Expand Down
25 changes: 15 additions & 10 deletions webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ object ApiRoutes {
with KnoraProjectRepo
with MessageRelay
with ProjectADMRestService
with ProjectsEndpointsHandlerF
with RestCardinalityService
with RestResourceInfoService
with StringFormatter
Expand All @@ -68,10 +69,12 @@ object ApiRoutes {
] =
ZLayer {
for {
sys <- ZIO.service[ActorSystem]
router <- ZIO.service[AppRouter]
appConfig <- ZIO.service[AppConfig]
routeData <- ZIO.succeed(KnoraRouteData(sys.system, router.ref, appConfig))
sys <- ZIO.service[ActorSystem]
router <- ZIO.service[AppRouter]
appConfig <- ZIO.service[AppConfig]
projectsHandler <- ZIO.service[ProjectsEndpointsHandlerF]
routeData <- ZIO.succeed(KnoraRouteData(sys.system, router.ref, appConfig))
tapirToPekkoRoute = TapirToPekkoInterpreter()(sys.system.dispatcher)
runtime <- ZIO.runtime[
AppConfig
with IriConverter
Expand All @@ -85,7 +88,7 @@ object ApiRoutes {
with core.State
with routing.Authenticator
]
} yield ApiRoutesImpl(routeData, runtime, appConfig)
} yield ApiRoutesImpl(routeData, projectsHandler, tapirToPekkoRoute, appConfig, runtime)
}
}

Expand All @@ -97,8 +100,11 @@ object ApiRoutes {
* The FIRST matching route is used for handling a request.
*/
private final case class ApiRoutesImpl(
private val routeData: KnoraRouteData,
private implicit val runtime: Runtime[
routeData: KnoraRouteData,
projectsHandler: ProjectsEndpointsHandlerF,
tapirToPekkoRoute: TapirToPekkoInterpreter,
appConfig: AppConfig,
implicit val runtime: Runtime[
AppConfig
with IriConverter
with KnoraProjectRepo
Expand All @@ -110,8 +116,7 @@ private final case class ApiRoutesImpl(
with ValuesResponderV2
with core.State
with routing.Authenticator
],
private val appConfig: AppConfig
]
) extends ApiRoutes
with AroundDirectives {

Expand Down Expand Up @@ -140,7 +145,7 @@ private final case class ApiRoutesImpl(
GroupsRouteADM(routeData, runtime).makeRoute ~
ListsRouteADM(routeData, runtime).makeRoute ~
PermissionsRouteADM(routeData, runtime).makeRoute ~
ProjectsRouteADM().makeRoute ~
ProjectsRouteADM(tapirToPekkoRoute, projectsHandler).makeRoute ~
StoreRouteADM(routeData, runtime).makeRoute ~
UsersRouteADM().makeRoute ~
FilesRouteADM(routeData, runtime).makeRoute
Expand Down
35 changes: 35 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/routing/BaseEndpoints.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package org.knora.webapi.routing

import sttp.model.StatusCode
import sttp.tapir.EndpointOutput
import sttp.tapir.endpoint
import sttp.tapir.generic.auto._
import sttp.tapir.json.zio.jsonBody
import sttp.tapir.oneOf
import sttp.tapir.oneOfVariant
import sttp.tapir.statusCode
import zio.ZLayer

import dsp.errors.BadRequestException
import dsp.errors.NotFoundException
import dsp.errors.RequestRejectedException

final case class BaseEndpoints() {

private val defaultErrorOutputs: EndpointOutput.OneOf[RequestRejectedException, RequestRejectedException] =
oneOf[RequestRejectedException](
oneOfVariant[NotFoundException](statusCode(StatusCode.NotFound).and(jsonBody[NotFoundException])),
oneOfVariant[BadRequestException](statusCode(StatusCode.BadRequest).and(jsonBody[BadRequestException]))
)

val publicEndpoint = endpoint.errorOut(defaultErrorOutputs)
}

object BaseEndpoints {
val layer = ZLayer.derive[BaseEndpoints]
}
Loading

0 comments on commit 08accab

Please sign in to comment.