Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Introduce tapir on Pekko DEV-2806 #2870

Merged
merged 10 commits into from Oct 11, 2023
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
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
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
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
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
Expand Up @@ -478,6 +478,9 @@ sealed trait ProjectIdentifierADM { self =>

object ProjectIdentifierADM {

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

/**
* Represents [[IriIdentifier]] identifier.
*
Expand Down
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 @@ -47,6 +48,7 @@ import org.knora.webapi.util.ZioHelper
/**
* Returns information about projects.
*/
@accessible
trait ProjectsResponderADM {

/**
Expand Down Expand Up @@ -389,7 +391,7 @@ final case class ProjectsResponderADMLive(
id <- IriIdentifier.fromString(projectIri.value).toZIO.mapError(e => BadRequestException(e.getMessage))
keywords <- projectService
.findProjectKeywordsBy(id)
.flatMap(ZIO.fromOption(_))
.some
.orElseFail(NotFoundException(s"Project '${projectIri.value}' not found."))
seakayone marked this conversation as resolved.
Show resolved Hide resolved
} yield keywords

Expand Down
25 changes: 15 additions & 10 deletions webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala
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
@@ -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]
}