diff --git a/agent/src/main/scala/za/co/absa/atum/agent/dispatcher/HttpDispatcher.scala b/agent/src/main/scala/za/co/absa/atum/agent/dispatcher/HttpDispatcher.scala index 13548a18c..cc7ea6623 100644 --- a/agent/src/main/scala/za/co/absa/atum/agent/dispatcher/HttpDispatcher.scala +++ b/agent/src/main/scala/za/co/absa/atum/agent/dispatcher/HttpDispatcher.scala @@ -29,10 +29,11 @@ class HttpDispatcher(config: Config) extends Dispatcher(config: Config) with Log val serverUrl: String = config.getString(UrlKey) - private val currentApiVersion = "/api/v1" - private val createPartitioningEndpoint = Uri.unsafeParse(s"$serverUrl$currentApiVersion/createPartitioning") - private val createCheckpointEndpoint = Uri.unsafeParse(s"$serverUrl$currentApiVersion/createCheckpoint") - private val createAdditionalDataEndpoint = Uri.unsafeParse(s"$serverUrl$currentApiVersion/writeAdditionalData") + private val apiV1 = "/api/v1" + private val apiV2 = "/api/v2" + private val createPartitioningEndpoint = Uri.unsafeParse(s"$serverUrl$apiV1/createPartitioning") + private val createCheckpointEndpoint = Uri.unsafeParse(s"$serverUrl$apiV1/createCheckpoint") + private val createAdditionalDataEndpoint = Uri.unsafeParse(s"$serverUrl$apiV2/writeAdditionalData") private val commonAtumRequest = basicRequest .header("Content-Type", "application/json") diff --git a/server/src/main/scala/za/co/absa/atum/server/api/controller/BaseController.scala b/server/src/main/scala/za/co/absa/atum/server/api/controller/BaseController.scala index 6c91d57d8..1877cc0b4 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/controller/BaseController.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/controller/BaseController.scala @@ -17,7 +17,8 @@ package za.co.absa.atum.server.api.controller import za.co.absa.atum.server.api.exception.ServiceError -import za.co.absa.atum.server.model.{ErrorResponse, GeneralErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.ErrorResponse.{ErrorResponse, GeneralErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.SuccessResponse.{MultiSuccessResponse, SingleSuccessResponse} import za.co.absa.fadb.exceptions.StatusException import zio._ @@ -55,4 +56,16 @@ trait BaseController { } } + + protected def mapToSingleSuccessResponse[A]( + effect: IO[ErrorResponse, A] + ): IO[ErrorResponse, SingleSuccessResponse[A]] = { + effect.map(SingleSuccessResponse(_)) + } + + protected def mapToMultiSuccessResponse[A]( + effect: IO[ErrorResponse, Seq[A]] + ): IO[ErrorResponse, MultiSuccessResponse[A]] = { + effect.map(MultiSuccessResponse(_)) + } } diff --git a/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointController.scala b/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointController.scala index 0e9bca830..402b67884 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointController.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointController.scala @@ -17,12 +17,18 @@ package za.co.absa.atum.server.api.controller import za.co.absa.atum.model.dto.CheckpointDTO -import za.co.absa.atum.server.model.ErrorResponse +import za.co.absa.atum.server.model.ErrorResponse.ErrorResponse +import za.co.absa.atum.server.model.SuccessResponse.SingleSuccessResponse import zio.IO import zio.macros.accessible @accessible trait CheckpointController { - def createCheckpoint(checkpointDTO: CheckpointDTO): IO[ErrorResponse, CheckpointDTO] + + def createCheckpointV1(checkpointDTO: CheckpointDTO): IO[ErrorResponse, CheckpointDTO] + + def createCheckpointV2( + checkpointDTO: CheckpointDTO + ): IO[ErrorResponse, SingleSuccessResponse[CheckpointDTO]] } diff --git a/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointControllerImpl.scala b/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointControllerImpl.scala index f1e3330c0..69a140d55 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointControllerImpl.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/controller/CheckpointControllerImpl.scala @@ -18,19 +18,27 @@ package za.co.absa.atum.server.api.controller import za.co.absa.atum.model.dto.CheckpointDTO import za.co.absa.atum.server.api.service.CheckpointService -import za.co.absa.atum.server.model.ErrorResponse +import za.co.absa.atum.server.model.ErrorResponse.ErrorResponse +import za.co.absa.atum.server.model.SuccessResponse.SingleSuccessResponse import zio._ -class CheckpointControllerImpl(checkpointService: CheckpointService) - extends CheckpointController with BaseController { +class CheckpointControllerImpl(checkpointService: CheckpointService) extends CheckpointController with BaseController { - override def createCheckpoint(checkpointDTO: CheckpointDTO): IO[ErrorResponse, CheckpointDTO] = { + override def createCheckpointV1( + checkpointDTO: CheckpointDTO + ): IO[ErrorResponse, CheckpointDTO] = { serviceCallWithStatus[Unit, CheckpointDTO]( checkpointService.saveCheckpoint(checkpointDTO), _ => checkpointDTO ) } + override def createCheckpointV2( + checkpointDTO: CheckpointDTO + ): IO[ErrorResponse, SingleSuccessResponse[CheckpointDTO]] = { + mapToSingleSuccessResponse(createCheckpointV1(checkpointDTO)) + } + } object CheckpointControllerImpl { diff --git a/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningController.scala b/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningController.scala index d833a8bb9..d8fd2669c 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningController.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningController.scala @@ -17,13 +17,24 @@ package za.co.absa.atum.server.api.controller import za.co.absa.atum.model.dto.{AdditionalDataSubmitDTO, AtumContextDTO, CheckpointDTO, CheckpointQueryDTO, PartitioningSubmitDTO} -import za.co.absa.atum.server.model.ErrorResponse +import za.co.absa.atum.server.model.ErrorResponse.ErrorResponse +import za.co.absa.atum.server.model.SuccessResponse.{MultiSuccessResponse, SingleSuccessResponse} import zio.IO import zio.macros.accessible @accessible trait PartitioningController { - def createPartitioningIfNotExists(partitioningSubmitDTO: PartitioningSubmitDTO): IO[ErrorResponse, AtumContextDTO] - def createOrUpdateAdditionalData(additionalData: AdditionalDataSubmitDTO): IO[ErrorResponse, AdditionalDataSubmitDTO] - def getPartitioningCheckpoints(checkpointQueryDTO: CheckpointQueryDTO): IO[ErrorResponse, Seq[CheckpointDTO]] + def createPartitioningIfNotExistsV1( + partitioningSubmitDTO: PartitioningSubmitDTO + ): IO[ErrorResponse, AtumContextDTO] + + def createPartitioningIfNotExistsV2( + partitioningSubmitDTO: PartitioningSubmitDTO + ): IO[ErrorResponse, SingleSuccessResponse[AtumContextDTO]] + + def createOrUpdateAdditionalDataV2( + additionalData: AdditionalDataSubmitDTO + ): IO[ErrorResponse, SingleSuccessResponse[AdditionalDataSubmitDTO]] + + def getPartitioningCheckpoints(checkpointQueryDTO: CheckpointQueryDTO): IO[ErrorResponse, MultiSuccessResponse[CheckpointDTO]] } diff --git a/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningControllerImpl.scala b/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningControllerImpl.scala index 5e4c79e3d..e9bbcef18 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningControllerImpl.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/controller/PartitioningControllerImpl.scala @@ -16,47 +16,64 @@ package za.co.absa.atum.server.api.controller - -import za.co.absa.atum.model.dto.{AdditionalDataSubmitDTO, AtumContextDTO, CheckpointDTO, CheckpointQueryDTO, PartitioningSubmitDTO} +import za.co.absa.atum.model.dto._ import za.co.absa.atum.server.api.exception.ServiceError import za.co.absa.atum.server.api.service.PartitioningService -import za.co.absa.atum.server.model.{ErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.ErrorResponse.{ErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.SuccessResponse.{MultiSuccessResponse, SingleSuccessResponse} import zio._ class PartitioningControllerImpl(partitioningService: PartitioningService) - extends PartitioningController with BaseController { + extends PartitioningController + with BaseController { - override def createPartitioningIfNotExists( + override def createPartitioningIfNotExistsV1( partitioningSubmitDTO: PartitioningSubmitDTO - ): IO[ErrorResponse, AtumContextDTO] = { - for { - _ <- partitioningService.createPartitioningIfNotExists(partitioningSubmitDTO) + ): IO[ErrorResponse, AtumContextDTO] = { + val atumContextDTOEffect = for { + _ <- partitioningService + .createPartitioningIfNotExists(partitioningSubmitDTO) .mapError(serviceError => InternalServerErrorResponse(serviceError.message)) - measures <- partitioningService.getPartitioningMeasures(partitioningSubmitDTO.partitioning) - .mapError { - serviceError: ServiceError => InternalServerErrorResponse(serviceError.message) - } - additionalData <- partitioningService.getPartitioningAdditionalData(partitioningSubmitDTO.partitioning) - .mapError { - serviceError: ServiceError => InternalServerErrorResponse(serviceError.message) - } + measures <- partitioningService + .getPartitioningMeasures(partitioningSubmitDTO.partitioning) + .mapError { serviceError: ServiceError => + InternalServerErrorResponse(serviceError.message) + } + additionalData <- partitioningService + .getPartitioningAdditionalData(partitioningSubmitDTO.partitioning) + .mapError { serviceError: ServiceError => + InternalServerErrorResponse(serviceError.message) + } } yield AtumContextDTO(partitioningSubmitDTO.partitioning, measures.toSet, additionalData) + + atumContextDTOEffect + } + + override def createPartitioningIfNotExistsV2( + partitioningSubmitDTO: PartitioningSubmitDTO + ): IO[ErrorResponse, SingleSuccessResponse[AtumContextDTO]] = { + mapToSingleSuccessResponse(createPartitioningIfNotExistsV1(partitioningSubmitDTO)) } - override def createOrUpdateAdditionalData( + override def createOrUpdateAdditionalDataV2( additionalData: AdditionalDataSubmitDTO - ): IO[ErrorResponse, AdditionalDataSubmitDTO] = { - serviceCallWithStatus[Unit, AdditionalDataSubmitDTO]( - partitioningService.createOrUpdateAdditionalData(additionalData), - _ => additionalData + ): IO[ErrorResponse, SingleSuccessResponse[AdditionalDataSubmitDTO]] = { + mapToSingleSuccessResponse( + serviceCallWithStatus[Unit, AdditionalDataSubmitDTO]( + partitioningService.createOrUpdateAdditionalData(additionalData), + _ => additionalData + ) ) } - override def getPartitioningCheckpoints(checkpointQueryDTO: CheckpointQueryDTO): - IO[ErrorResponse, Seq[CheckpointDTO]] = { - serviceCall[Seq[CheckpointDTO], Seq[CheckpointDTO]]( - partitioningService.getPartitioningCheckpoints(checkpointQueryDTO), - checkpoints => checkpoints + override def getPartitioningCheckpoints( + checkpointQueryDTO: CheckpointQueryDTO + ): IO[ErrorResponse, MultiSuccessResponse[CheckpointDTO]] = { + mapToMultiSuccessResponse( + serviceCall[Seq[CheckpointDTO], Seq[CheckpointDTO]]( + partitioningService.getPartitioningCheckpoints(checkpointQueryDTO), + checkpoints => checkpoints + ) ) } diff --git a/server/src/main/scala/za/co/absa/atum/server/api/http/BaseEndpoints.scala b/server/src/main/scala/za/co/absa/atum/server/api/http/BaseEndpoints.scala index 9c1367898..797183968 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/http/BaseEndpoints.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/http/BaseEndpoints.scala @@ -19,13 +19,18 @@ package za.co.absa.atum.server.api.http import sttp.model.StatusCode import sttp.tapir.generic.auto.schemaForCaseClass import sttp.tapir.json.play.jsonBody +import sttp.tapir.typelevel.MatchType import sttp.tapir.ztapir._ import sttp.tapir.{EndpointOutput, PublicEndpoint} import za.co.absa.atum.server.Constants.Endpoints.{Api, V1, V2} -import za.co.absa.atum.server.model._ +import za.co.absa.atum.server.model.ErrorResponse._ + +import java.util.UUID trait BaseEndpoints { + implicit val uuidMatchType: MatchType[UUID] = (a: Any) => a.isInstanceOf[UUID] + private val badRequestOneOfVariant: EndpointOutput.OneOfVariant[BadRequestResponse] = { oneOfVariantFromMatchType( StatusCode.BadRequest, diff --git a/server/src/main/scala/za/co/absa/atum/server/api/http/Endpoints.scala b/server/src/main/scala/za/co/absa/atum/server/api/http/Endpoints.scala index a8dfb539a..9adbcd84b 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/http/Endpoints.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/http/Endpoints.scala @@ -17,18 +17,20 @@ package za.co.absa.atum.server.api.http import sttp.model.StatusCode -import sttp.tapir.{PublicEndpoint, endpoint} import sttp.tapir.generic.auto.schemaForCaseClass import sttp.tapir.json.play.jsonBody import sttp.tapir.ztapir._ -import za.co.absa.atum.model.dto.{AdditionalDataSubmitDTO, AtumContextDTO, CheckpointQueryDTO, CheckpointDTO, PartitioningSubmitDTO} +import sttp.tapir.{PublicEndpoint, endpoint} +import za.co.absa.atum.model.dto._ import za.co.absa.atum.server.Constants.Endpoints._ -import za.co.absa.atum.server.model.ErrorResponse +import za.co.absa.atum.server.model.ErrorResponse.ErrorResponse import za.co.absa.atum.server.model.PlayJsonImplicits._ +import za.co.absa.atum.server.model.SuccessResponse.{MultiSuccessResponse, SingleSuccessResponse} trait Endpoints extends BaseEndpoints { - protected val createCheckpointEndpoint: PublicEndpoint[CheckpointDTO, ErrorResponse, CheckpointDTO, Any] = { + protected val createCheckpointEndpointV1 + : PublicEndpoint[CheckpointDTO, ErrorResponse, CheckpointDTO, Any] = { apiV1.post .in(CreateCheckpoint) .in(jsonBody[CheckpointDTO]) @@ -36,7 +38,16 @@ trait Endpoints extends BaseEndpoints { .out(jsonBody[CheckpointDTO]) } - protected val createPartitioningEndpoint + protected val createCheckpointEndpointV2 + : PublicEndpoint[CheckpointDTO, ErrorResponse, SingleSuccessResponse[CheckpointDTO], Any] = { + apiV2.post + .in(CreateCheckpoint) + .in(jsonBody[CheckpointDTO]) + .out(statusCode(StatusCode.Created)) + .out(jsonBody[SingleSuccessResponse[CheckpointDTO]]) + } + + protected val createPartitioningEndpointV1 : PublicEndpoint[PartitioningSubmitDTO, ErrorResponse, AtumContextDTO, Any] = { apiV1.post .in(CreatePartitioning) @@ -45,22 +56,31 @@ trait Endpoints extends BaseEndpoints { .out(jsonBody[AtumContextDTO]) } - protected val createOrUpdateAdditionalDataEndpoint - : PublicEndpoint[AdditionalDataSubmitDTO, ErrorResponse, AdditionalDataSubmitDTO, Any] = { - apiV1.post + protected val createPartitioningEndpointV2 + : PublicEndpoint[PartitioningSubmitDTO, ErrorResponse, SingleSuccessResponse[AtumContextDTO], Any] = { + apiV2.post + .in(CreatePartitioning) + .in(jsonBody[PartitioningSubmitDTO]) + .out(statusCode(StatusCode.Ok)) + .out(jsonBody[SingleSuccessResponse[AtumContextDTO]]) + } + + protected val createOrUpdateAdditionalDataEndpointV2 + : PublicEndpoint[AdditionalDataSubmitDTO, ErrorResponse, SingleSuccessResponse[AdditionalDataSubmitDTO], Any] = { + apiV2.post .in(CreateOrUpdateAdditionalData) .in(jsonBody[AdditionalDataSubmitDTO]) .out(statusCode(StatusCode.Ok)) - .out(jsonBody[AdditionalDataSubmitDTO]) + .out(jsonBody[SingleSuccessResponse[AdditionalDataSubmitDTO]]) } protected val getPartitioningCheckpointsEndpointV2 - : PublicEndpoint[CheckpointQueryDTO, ErrorResponse, Seq[CheckpointDTO], Any] = { + : PublicEndpoint[CheckpointQueryDTO, ErrorResponse, MultiSuccessResponse[CheckpointDTO], Any] = { apiV2.get .in(GetPartitioningCheckpoints) .in(jsonBody[CheckpointQueryDTO]) .out(statusCode(StatusCode.Ok)) - .out(jsonBody[Seq[CheckpointDTO]]) + .out(jsonBody[MultiSuccessResponse[CheckpointDTO]]) } protected val zioMetricsEndpoint: PublicEndpoint[Unit, Unit, String, Any] = { diff --git a/server/src/main/scala/za/co/absa/atum/server/api/http/Routes.scala b/server/src/main/scala/za/co/absa/atum/server/api/http/Routes.scala index b555847ab..70fdf940c 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/http/Routes.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/http/Routes.scala @@ -38,11 +38,13 @@ trait Routes extends Endpoints with ServerOptions { if (httpMonitoringConfig.enabled) Some(HttpMetrics.prometheusMetrics.metricsInterceptor()) else None } val endpoints = List( - createServerEndpoint(createCheckpointEndpoint, CheckpointController.createCheckpoint), - createServerEndpoint(createPartitioningEndpoint, PartitioningController.createPartitioningIfNotExists), - createServerEndpoint(createOrUpdateAdditionalDataEndpoint, PartitioningController.createOrUpdateAdditionalData), + createServerEndpoint(createCheckpointEndpointV1, CheckpointController.createCheckpointV1), + createServerEndpoint(createCheckpointEndpointV2, CheckpointController.createCheckpointV2), + createServerEndpoint(createPartitioningEndpointV1, PartitioningController.createPartitioningIfNotExistsV1), + createServerEndpoint(createPartitioningEndpointV2, PartitioningController.createPartitioningIfNotExistsV2), + createServerEndpoint(createOrUpdateAdditionalDataEndpointV2, PartitioningController.createOrUpdateAdditionalDataV2), createServerEndpoint(getPartitioningCheckpointsEndpointV2, PartitioningController.getPartitioningCheckpoints), - createServerEndpoint(healthEndpoint, (_: Unit) => ZIO.unit), + createServerEndpoint(healthEndpoint, (_: Unit) => ZIO.unit) ) ZHttp4sServerInterpreter[HttpEnv.Env](http4sServerOptions(metricsInterceptorOption)).from(endpoints).toRoutes } @@ -51,7 +53,14 @@ trait Routes extends Endpoints with ServerOptions { Http4sServerInterpreter[HttpEnv.F]().toRoutes(HttpMetrics.prometheusMetrics.metricsEndpoint) private def createSwaggerRoutes: HttpRoutes[HttpEnv.F] = { - val endpoints = List(createCheckpointEndpoint, createPartitioningEndpoint, createOrUpdateAdditionalDataEndpoint, getPartitioningCheckpointsEndpointV2) + val endpoints = List( + createCheckpointEndpointV1, + createCheckpointEndpointV2, + createPartitioningEndpointV1, + createPartitioningEndpointV2, + createOrUpdateAdditionalDataEndpointV2, + getPartitioningCheckpointsEndpointV2 + ) ZHttp4sServerInterpreter[HttpEnv.Env](http4sServerOptions(None)) .from(SwaggerInterpreter().fromEndpoints[HttpEnv.F](endpoints, SwaggerApiName, SwaggerApiVersion)) .toRoutes diff --git a/server/src/main/scala/za/co/absa/atum/server/api/http/ServerOptions.scala b/server/src/main/scala/za/co/absa/atum/server/api/http/ServerOptions.scala index 226cebf8a..f6bbe79cd 100644 --- a/server/src/main/scala/za/co/absa/atum/server/api/http/ServerOptions.scala +++ b/server/src/main/scala/za/co/absa/atum/server/api/http/ServerOptions.scala @@ -27,7 +27,7 @@ import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.r import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.server.model.ValuedEndpointOutput import sttp.tapir.ztapir.{headers, statusCode} -import za.co.absa.atum.server.model.BadRequestResponse +import za.co.absa.atum.server.model.ErrorResponse.BadRequestResponse import zio.interop.catz._ trait ServerOptions { diff --git a/server/src/main/scala/za/co/absa/atum/server/model/ErrorResponse.scala b/server/src/main/scala/za/co/absa/atum/server/model/ErrorResponse.scala index 2fbc0c697..174ff2f2a 100644 --- a/server/src/main/scala/za/co/absa/atum/server/model/ErrorResponse.scala +++ b/server/src/main/scala/za/co/absa/atum/server/model/ErrorResponse.scala @@ -18,32 +18,35 @@ package za.co.absa.atum.server.model import play.api.libs.json.{Json, Reads, Writes} -sealed trait ErrorResponse { - def message: String -} +import java.util.UUID object ErrorResponse { + + sealed trait ErrorResponse extends ResponseEnvelope { + def message: String + } + implicit val reads: Reads[ErrorResponse] = Json.reads[ErrorResponse] implicit val writes: Writes[ErrorResponse] = Json.writes[ErrorResponse] -} -final case class BadRequestResponse(message: String) extends ErrorResponse + final case class BadRequestResponse(message: String, requestId: UUID = UUID.randomUUID()) + extends ErrorResponse -object BadRequestResponse { - implicit val reads: Reads[BadRequestResponse] = Json.reads[BadRequestResponse] - implicit val writes: Writes[BadRequestResponse] = Json.writes[BadRequestResponse] -} + implicit val readsBadRequestResponse: Reads[BadRequestResponse] = Json.reads[BadRequestResponse] + implicit val writesBadRequestResponse: Writes[BadRequestResponse] = Json.writes[BadRequestResponse] -final case class GeneralErrorResponse(message: String) extends ErrorResponse + final case class GeneralErrorResponse(message: String, requestId: UUID = UUID.randomUUID()) + extends ErrorResponse -object GeneralErrorResponse { - implicit val reads: Reads[GeneralErrorResponse] = Json.reads[GeneralErrorResponse] - implicit val writes: Writes[GeneralErrorResponse] = Json.writes[GeneralErrorResponse] -} + implicit val readsGeneralErrorResponse: Reads[GeneralErrorResponse] = Json.reads[GeneralErrorResponse] + implicit val writesGeneralErrorResponse: Writes[GeneralErrorResponse] = Json.writes[GeneralErrorResponse] + + final case class InternalServerErrorResponse(message: String, requestId: UUID = UUID.randomUUID()) + extends ErrorResponse -final case class InternalServerErrorResponse(message: String) extends ErrorResponse + implicit val readsInternalServerErrorResponse: Reads[InternalServerErrorResponse] = + Json.reads[InternalServerErrorResponse] + implicit val writesInternalServerErrorResponse: Writes[InternalServerErrorResponse] = + Json.writes[InternalServerErrorResponse] -object InternalServerErrorResponse { - implicit val reads: Reads[InternalServerErrorResponse] = Json.reads[InternalServerErrorResponse] - implicit val writes: Writes[InternalServerErrorResponse] = Json.writes[InternalServerErrorResponse] } diff --git a/server/src/main/scala/za/co/absa/atum/server/model/PlayJsonImplicits.scala b/server/src/main/scala/za/co/absa/atum/server/model/PlayJsonImplicits.scala index 26aefcfd1..ae98fe973 100644 --- a/server/src/main/scala/za/co/absa/atum/server/model/PlayJsonImplicits.scala +++ b/server/src/main/scala/za/co/absa/atum/server/model/PlayJsonImplicits.scala @@ -20,6 +20,7 @@ import play.api.libs.functional.syntax.toFunctionalBuilderOps import play.api.libs.json._ import za.co.absa.atum.model.dto.MeasureResultDTO.{ResultValueType, TypedValue} import za.co.absa.atum.model.dto._ +import za.co.absa.atum.server.model.SuccessResponse.{MultiSuccessResponse, SingleSuccessResponse} object PlayJsonImplicits { @@ -57,9 +58,6 @@ object PlayJsonImplicits { } } - implicit val readsMeasureDTO: Reads[MeasureDTO] = Json.reads[MeasureDTO] - implicit val writesMeasureDTO: Writes[MeasureDTO] = Json.writes[MeasureDTO] - implicit val readsTypedValue: Reads[MeasureResultDTO.TypedValue] = Json.reads[MeasureResultDTO.TypedValue] implicit val writesTypedValue: Writes[MeasureResultDTO.TypedValue] = Json.writes[MeasureResultDTO.TypedValue] @@ -68,8 +66,12 @@ object PlayJsonImplicits { (__ \ "supportValues").readNullable[Map[String, TypedValue]].map(_.getOrElse(Map.empty)) )(MeasureResultDTO.apply _) } + implicit val writesMeasureResultDTO: Writes[MeasureResultDTO] = Json.writes[MeasureResultDTO] + implicit val readsMeasureDTO: Reads[MeasureDTO] = Json.reads[MeasureDTO] + implicit val writesMeasureDTO: Writes[MeasureDTO] = Json.writes[MeasureDTO] + implicit val readsMeasurementDTO: Reads[MeasurementDTO] = Json.reads[MeasurementDTO] implicit val writesMeasurementDTO: Writes[MeasurementDTO] = Json.writes[MeasurementDTO] @@ -92,6 +94,9 @@ object PlayJsonImplicits { implicit val readsAtumContextDTO: Reads[AtumContextDTO] = Json.reads[AtumContextDTO] implicit val writesAtumContextDTO: Writes[AtumContextDTO] = Json.writes[AtumContextDTO] + implicit def formatSingleSuccessResponse[T: Format]: Format[SingleSuccessResponse[T]] = Json.format[SingleSuccessResponse[T]] + implicit def formatMultiSuccessResponse[T: Format]: Format[MultiSuccessResponse[T]] = Json.format[MultiSuccessResponse[T]] + implicit val readsCheckpointQueryDTO: Reads[CheckpointQueryDTO] = Json.reads[CheckpointQueryDTO] implicit val writesCheckpointQueryDTO: Writes[CheckpointQueryDTO] = Json.writes[CheckpointQueryDTO] diff --git a/server/src/main/scala/za/co/absa/atum/server/model/ResponseEnvelope.scala b/server/src/main/scala/za/co/absa/atum/server/model/ResponseEnvelope.scala new file mode 100644 index 000000000..8fb991d7c --- /dev/null +++ b/server/src/main/scala/za/co/absa/atum/server/model/ResponseEnvelope.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2021 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.atum.server.model + +import java.util.UUID + +trait ResponseEnvelope { + def requestId: UUID +} diff --git a/server/src/main/scala/za/co/absa/atum/server/model/SuccessResponse.scala b/server/src/main/scala/za/co/absa/atum/server/model/SuccessResponse.scala new file mode 100644 index 000000000..8ab99255d --- /dev/null +++ b/server/src/main/scala/za/co/absa/atum/server/model/SuccessResponse.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2021 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.atum.server.model + +import java.util.UUID + +object SuccessResponse { + + sealed trait SuccessResponse extends ResponseEnvelope + + case class SingleSuccessResponse[T](data: T, requestId: UUID = UUID.randomUUID()) + extends SuccessResponse + + case class MultiSuccessResponse[T](data: Seq[T], requestId: UUID = UUID.randomUUID()) + extends SuccessResponse + +} diff --git a/server/src/test/scala/za/co/absa/atum/server/api/TestData.scala b/server/src/test/scala/za/co/absa/atum/server/api/TestData.scala index 10dc092db..65a1bf7bb 100644 --- a/server/src/test/scala/za/co/absa/atum/server/api/TestData.scala +++ b/server/src/test/scala/za/co/absa/atum/server/api/TestData.scala @@ -27,6 +27,8 @@ import MeasureResultDTO.ResultValueType._ trait TestData { + protected val uuid = UUID.randomUUID() + // Partitioning DTO protected val partitioningDTO1: PartitioningDTO = Seq( PartitionDTO("key1", "val1"), diff --git a/server/src/test/scala/za/co/absa/atum/server/api/controller/CheckpointControllerIntegrationTests.scala b/server/src/test/scala/za/co/absa/atum/server/api/controller/CheckpointControllerIntegrationTests.scala index 8afde6632..5c71eb3e9 100644 --- a/server/src/test/scala/za/co/absa/atum/server/api/controller/CheckpointControllerIntegrationTests.scala +++ b/server/src/test/scala/za/co/absa/atum/server/api/controller/CheckpointControllerIntegrationTests.scala @@ -16,21 +16,19 @@ package za.co.absa.atum.server.api.controller -import org.junit.runner.RunWith import org.mockito.Mockito.{mock, when} import za.co.absa.atum.server.api.TestData import za.co.absa.atum.server.api.exception.ServiceError import za.co.absa.atum.server.api.service.CheckpointService -import za.co.absa.atum.server.model.{GeneralErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.ErrorResponse.{GeneralErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.SuccessResponse.SingleSuccessResponse import za.co.absa.fadb.exceptions.ErrorInDataException import za.co.absa.fadb.status.FunctionStatus import zio.test.Assertion.failsWithA import zio._ import zio.test._ -import zio.test.junit.ZTestJUnitRunner -@RunWith(classOf[ZTestJUnitRunner]) -class CheckpointControllerIntegrationTests extends ZIOSpecDefault with TestData { +object CheckpointControllerIntegrationTests extends ZIOSpecDefault with TestData { private val checkpointServiceMock = mock(classOf[CheckpointService]) @@ -48,14 +46,14 @@ class CheckpointControllerIntegrationTests extends ZIOSpecDefault with TestData suite("CreateCheckpointSuite")( test("Returns expected CheckpointDTO") { for { - result <- CheckpointController.createCheckpoint(checkpointDTO1) + result <- CheckpointController.createCheckpointV1(checkpointDTO1) } yield assertTrue(result == checkpointDTO1) }, test("Returns expected InternalServerErrorResponse") { - assertZIO(CheckpointController.createCheckpoint(checkpointDTO3).exit)(failsWithA[InternalServerErrorResponse]) + assertZIO(CheckpointController.createCheckpointV1(checkpointDTO3).exit)(failsWithA[InternalServerErrorResponse]) }, test("Returns expected GeneralErrorResponse") { - assertZIO(CheckpointController.createCheckpoint(checkpointDTO2).exit)(failsWithA[GeneralErrorResponse]) + assertZIO(CheckpointController.createCheckpointV1(checkpointDTO2).exit)(failsWithA[GeneralErrorResponse]) } ) ).provide( diff --git a/server/src/test/scala/za/co/absa/atum/server/api/controller/PartitioningControllerUnitTests.scala b/server/src/test/scala/za/co/absa/atum/server/api/controller/PartitioningControllerUnitTests.scala index 269f89156..ebc7b8d61 100644 --- a/server/src/test/scala/za/co/absa/atum/server/api/controller/PartitioningControllerUnitTests.scala +++ b/server/src/test/scala/za/co/absa/atum/server/api/controller/PartitioningControllerUnitTests.scala @@ -17,12 +17,14 @@ package za.co.absa.atum.server.api.controller import org.mockito.Mockito.{mock, when} +import za.co.absa.atum.model.dto.CheckpointDTO import za.co.absa.atum.server.api.TestData import za.co.absa.atum.server.api.exception.ServiceError import za.co.absa.atum.server.api.service.PartitioningService -import za.co.absa.atum.server.model.InternalServerErrorResponse -import zio.test.Assertion.{equalTo, failsWithA} +import za.co.absa.atum.server.model.ErrorResponse.InternalServerErrorResponse +import za.co.absa.atum.server.model.SuccessResponse.{MultiSuccessResponse, SingleSuccessResponse} import zio._ +import zio.test.Assertion.{equalTo, failsWithA} import zio.test._ object PartitioningControllerUnitTests extends ZIOSpecDefault with TestData { @@ -58,21 +60,25 @@ object PartitioningControllerUnitTests extends ZIOSpecDefault with TestData { suite("CreatePartitioningIfNotExistsSuite")( test("Returns expected AtumContextDTO") { for { - result <- PartitioningController.createPartitioningIfNotExists(partitioningSubmitDTO1) - } yield assertTrue (result == atumContextDTO1) + result <- PartitioningController.createPartitioningIfNotExistsV1(partitioningSubmitDTO1) + } yield assertTrue(result == atumContextDTO1) }, test("Returns expected InternalServerErrorResponse") { - assertZIO(PartitioningController.createPartitioningIfNotExists(partitioningSubmitDTO2).exit)( + assertZIO(PartitioningController.createPartitioningIfNotExistsV1(partitioningSubmitDTO2).exit)( failsWithA[InternalServerErrorResponse] ) } ), suite("CreateOrUpdateAdditionalDataSuite")( test("Returns expected AdditionalDataSubmitDTO") { - assertZIO(PartitioningController.createOrUpdateAdditionalData(additionalDataSubmitDTO1))(equalTo(additionalDataSubmitDTO1)) + for { + result <- PartitioningController.createOrUpdateAdditionalDataV2(additionalDataSubmitDTO1) + expected = SingleSuccessResponse(additionalDataSubmitDTO1, uuid) + actual = result.copy(requestId = uuid) + } yield assert(actual)(equalTo(expected)) }, test("Returns expected InternalServerErrorResponse") { - assertZIO(PartitioningController.createOrUpdateAdditionalData(additionalDataSubmitDTO2).exit)( + assertZIO(PartitioningController.createOrUpdateAdditionalDataV2(additionalDataSubmitDTO2).exit)( failsWithA[InternalServerErrorResponse] ) } @@ -82,12 +88,12 @@ object PartitioningControllerUnitTests extends ZIOSpecDefault with TestData { test("Returns expected Seq[MeasureDTO]") { for { result <- PartitioningController.getPartitioningCheckpoints(checkpointQueryDTO1) - } yield assertTrue(result == Seq(checkpointDTO1, checkpointDTO2)) + } yield assertTrue(result.data == Seq(checkpointDTO1, checkpointDTO2)) }, test("Returns expected empty sequence") { for { result <- PartitioningController.getPartitioningCheckpoints(checkpointQueryDTO2) - } yield assertTrue(result == Seq.empty) + } yield assertTrue(result.data == Seq.empty[CheckpointDTO]) }, test("Returns expected InternalServerErrorResponse") { assertZIO(PartitioningController.getPartitioningCheckpoints(checkpointQueryDTO3).exit)( diff --git a/server/src/test/scala/za/co/absa/atum/server/api/http/CreateCheckpointEndpointIntegrationTest.scala b/server/src/test/scala/za/co/absa/atum/server/api/http/CreateCheckpointEndpointIntegrationTests.scala similarity index 76% rename from server/src/test/scala/za/co/absa/atum/server/api/http/CreateCheckpointEndpointIntegrationTest.scala rename to server/src/test/scala/za/co/absa/atum/server/api/http/CreateCheckpointEndpointIntegrationTests.scala index c53a2db82..6568ae944 100644 --- a/server/src/test/scala/za/co/absa/atum/server/api/http/CreateCheckpointEndpointIntegrationTest.scala +++ b/server/src/test/scala/za/co/absa/atum/server/api/http/CreateCheckpointEndpointIntegrationTests.scala @@ -18,35 +18,36 @@ package za.co.absa.atum.server.api.http import org.mockito.Mockito.{mock, when} import sttp.client3._ -import sttp.client3.testing.SttpBackendStub import sttp.client3.playJson._ +import sttp.client3.testing.SttpBackendStub import sttp.model.StatusCode import sttp.tapir.server.stub.TapirStubInterpreter import sttp.tapir.ztapir.{RIOMonadError, RichZEndpoint} import za.co.absa.atum.model.dto.CheckpointDTO import za.co.absa.atum.server.api.TestData import za.co.absa.atum.server.api.controller.CheckpointController -import za.co.absa.atum.server.model.{GeneralErrorResponse, InternalServerErrorResponse} -import zio.test._ +import za.co.absa.atum.server.model.ErrorResponse.{GeneralErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.PlayJsonImplicits._ +import za.co.absa.atum.server.model.SuccessResponse.SingleSuccessResponse import zio._ -import za.co.absa.atum.server.model.PlayJsonImplicits.{readsCheckpointDTO, writesCheckpointDTO} import zio.test.Assertion.equalTo +import zio.test._ object CreateCheckpointEndpointIntegrationTests extends ZIOSpecDefault with Endpoints with TestData { private val checkpointControllerMock = mock(classOf[CheckpointController]) - when(checkpointControllerMock.createCheckpoint(checkpointDTO1)) - .thenReturn(ZIO.succeed(checkpointDTO1)) - when(checkpointControllerMock.createCheckpoint(checkpointDTO2)) + when(checkpointControllerMock.createCheckpointV2(checkpointDTO1)) + .thenReturn(ZIO.succeed(SingleSuccessResponse(checkpointDTO1, uuid))) + when(checkpointControllerMock.createCheckpointV2(checkpointDTO2)) .thenReturn(ZIO.fail(GeneralErrorResponse("error"))) - when(checkpointControllerMock.createCheckpoint(checkpointDTO3)) + when(checkpointControllerMock.createCheckpointV2(checkpointDTO3)) .thenReturn(ZIO.fail(InternalServerErrorResponse("error"))) private val checkpointControllerMockLayer = ZLayer.succeed(checkpointControllerMock) private val createCheckpointServerEndpoint = - createCheckpointEndpoint.zServerLogic(CheckpointController.createCheckpoint) + createCheckpointEndpointV2.zServerLogic(CheckpointController.createCheckpointV2) def spec: Spec[TestEnvironment with Scope, Any] = { val backendStub = TapirStubInterpreter(SttpBackendStub.apply(new RIOMonadError[CheckpointController])) @@ -55,8 +56,8 @@ object CreateCheckpointEndpointIntegrationTests extends ZIOSpecDefault with Endp .backend() val request = basicRequest - .post(uri"https://test.com/api/v1/createCheckpoint") - .response(asJson[CheckpointDTO]) + .post(uri"https://test.com/api/v2/createCheckpoint") + .response(asJson[SingleSuccessResponse[CheckpointDTO]]) suite("CreateCheckpointEndpointSuite")( test("Returns expected CheckpointDTO") { @@ -67,7 +68,7 @@ object CreateCheckpointEndpointIntegrationTests extends ZIOSpecDefault with Endp val body = response.map(_.body) val statusCode = response.map(_.code) - assertZIO(body <&> statusCode)(equalTo(Right(checkpointDTO1), StatusCode.Created)) + assertZIO(body <&> statusCode)(equalTo(Right(SingleSuccessResponse(checkpointDTO1, uuid)), StatusCode.Created)) }, test("Returns expected BadRequest") { val response = request diff --git a/server/src/test/scala/za/co/absa/atum/server/api/http/CreatePartitioningEndpointIntegrationTests.scala b/server/src/test/scala/za/co/absa/atum/server/api/http/CreatePartitioningEndpointIntegrationTests.scala index b7a3de799..9e4d60484 100644 --- a/server/src/test/scala/za/co/absa/atum/server/api/http/CreatePartitioningEndpointIntegrationTests.scala +++ b/server/src/test/scala/za/co/absa/atum/server/api/http/CreatePartitioningEndpointIntegrationTests.scala @@ -26,27 +26,30 @@ import sttp.tapir.ztapir.{RIOMonadError, RichZEndpoint} import za.co.absa.atum.model.dto.AtumContextDTO import za.co.absa.atum.server.api.TestData import za.co.absa.atum.server.api.controller.PartitioningController -import za.co.absa.atum.server.model.{GeneralErrorResponse, InternalServerErrorResponse} -import zio.test.Assertion.equalTo +import za.co.absa.atum.server.model.ErrorResponse.{GeneralErrorResponse, InternalServerErrorResponse} +import za.co.absa.atum.server.model.PlayJsonImplicits._ +import za.co.absa.atum.server.model.SuccessResponse.SingleSuccessResponse import zio._ +import zio.test.Assertion.equalTo import zio.test._ -import za.co.absa.atum.server.model.PlayJsonImplicits.{readsAtumContextDTO, writesPartitioningSubmitDTO} + +import java.util.UUID object CreatePartitioningEndpointIntegrationTests extends ZIOSpecDefault with Endpoints with TestData { private val createPartitioningEndpointMock = mock(classOf[PartitioningController]) - when(createPartitioningEndpointMock.createPartitioningIfNotExists(partitioningSubmitDTO1)) - .thenReturn(ZIO.succeed(createAtumContextDTO(partitioningSubmitDTO1))) - when(createPartitioningEndpointMock.createPartitioningIfNotExists(partitioningSubmitDTO2)) + when(createPartitioningEndpointMock.createPartitioningIfNotExistsV2(partitioningSubmitDTO1)) + .thenReturn(ZIO.succeed(SingleSuccessResponse(createAtumContextDTO(partitioningSubmitDTO1), uuid))) + when(createPartitioningEndpointMock.createPartitioningIfNotExistsV2(partitioningSubmitDTO2)) .thenReturn(ZIO.fail(GeneralErrorResponse("error"))) - when(createPartitioningEndpointMock.createPartitioningIfNotExists(partitioningSubmitDTO3)) + when(createPartitioningEndpointMock.createPartitioningIfNotExistsV2(partitioningSubmitDTO3)) .thenReturn(ZIO.fail(InternalServerErrorResponse("error"))) private val createPartitioningEndpointMockLayer = ZLayer.succeed(createPartitioningEndpointMock) private val createPartitioningServerEndpoint = - createPartitioningEndpoint.zServerLogic(PartitioningController.createPartitioningIfNotExists) + createPartitioningEndpointV2.zServerLogic(PartitioningController.createPartitioningIfNotExistsV2) def spec: Spec[TestEnvironment with Scope, Any] = { val backendStub = TapirStubInterpreter(SttpBackendStub.apply(new RIOMonadError[PartitioningController])) @@ -55,8 +58,8 @@ object CreatePartitioningEndpointIntegrationTests extends ZIOSpecDefault with En .backend() val request = basicRequest - .post(uri"https://test.com/api/v1/createPartitioning") - .response(asJson[AtumContextDTO]) + .post(uri"https://test.com/api/v2/createPartitioning") + .response(asJson[SingleSuccessResponse[AtumContextDTO]]) suite("CreatePartitioningEndpointSuite")( test("Returns expected AtumContextDTO") { @@ -67,7 +70,9 @@ object CreatePartitioningEndpointIntegrationTests extends ZIOSpecDefault with En val body = response.map(_.body) val statusCode = response.map(_.code) - assertZIO(body <&> statusCode)(equalTo(Right(createAtumContextDTO(partitioningSubmitDTO1)), StatusCode.Ok)) + assertZIO(body <&> statusCode)( + equalTo(Right(SingleSuccessResponse(createAtumContextDTO(partitioningSubmitDTO1), uuid)), StatusCode.Ok) + ) }, test("Returns expected BadRequest") { val response = request