From 5e99abc41659fb5687c8ee1d76a8c293bba80e4e Mon Sep 17 00:00:00 2001 From: Rodo Date: Mon, 23 Oct 2023 19:27:20 -0300 Subject: [PATCH] feat: added or error handler to control generic errors --- .../books/infrastructure/http/AuthorApi.scala | 16 +++------- .../books/infrastructure/http/BookApi.scala | 16 +++------- .../infrastructure/http/PublisherApi.scala | 16 +++------- .../shared/infrastructure/http/Fail.scala | 18 +++++------ .../http/HasTapirResource.scala | 32 +++++++++++++++++-- 5 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/main/scala/com/example/books/infrastructure/http/AuthorApi.scala b/src/main/scala/com/example/books/infrastructure/http/AuthorApi.scala index de26fec..32532a0 100644 --- a/src/main/scala/com/example/books/infrastructure/http/AuthorApi.scala +++ b/src/main/scala/com/example/books/infrastructure/http/AuthorApi.scala @@ -16,26 +16,20 @@ class AuthorApi(service: AuthorService) extends HasTapirResource with AuthorCode private val post = base.post .in(jsonBody[Author]) .out(statusCode(Created)) - .serverLogicSuccess { author => - service.create(author) - } + .serverLogic { author => service.create(author).orError } // Update a existing author private val put = base.put .in(pathId) .in(jsonBody[Author]) .out(statusCode(NoContent)) - .serverLogicSuccess { case (id, author) => - service.update(Id(id), author) - } + .serverLogic { case (id, author) => service.update(Id(id), author).orError } // Get a author by id private val get = base.get .in(pathId) .out(jsonBody[Author]) - .serverLogic { id => - service.find(Id(id)).map(_.toRight(Fail.NotFound(s"Author for id: $id Not Found"): Fail)) - } + .serverLogic { id => service.find(Id(id)).orError(s"Author for id: $id Not Found") } // List authors private val sortPageFields: EndpointInput[PageRequest] = sortPage( @@ -45,13 +39,13 @@ class AuthorApi(service: AuthorService) extends HasTapirResource with AuthorCode private val list = base.get .in(sortPageFields / filter) .out(jsonBody[PageResponse[Author]]) - .serverLogicSuccess { case (pr, filter) => service.list(pr, filter) } + .serverLogic { case (pr, filter) => service.list(pr, filter).orError } // Delete a author by id private val delete = base.delete .in(pathId) .out(statusCode(NoContent)) - .serverLogicSuccess { id => service.delete(Id(id)) } + .serverLogic { id => service.delete(Id(id)).orError } // Endpoints to Expose override val endpoints: ServerEndpoints = List(post, put, get, list, delete) diff --git a/src/main/scala/com/example/books/infrastructure/http/BookApi.scala b/src/main/scala/com/example/books/infrastructure/http/BookApi.scala index 8c1728c..22872e6 100644 --- a/src/main/scala/com/example/books/infrastructure/http/BookApi.scala +++ b/src/main/scala/com/example/books/infrastructure/http/BookApi.scala @@ -18,26 +18,20 @@ class BookApi(service: BookService) extends HasTapirResource with BookCodecs wit private val post = base.post .in(jsonBody[Book]) .out(statusCode(Created)) - .serverLogicSuccess { book => - service.create(book) - } + .serverLogic { book => service.create(book).orError } // Update a existing book private val put = base.put .in(pathId) .in(jsonBody[Book]) .out(statusCode(NoContent)) - .serverLogicSuccess { case (id, book) => - service.update(Id(id), book) - } + .serverLogic { case (id, book) => service.update(Id(id), book).orError } // Get a book by id private val get = base.get .in(pathId) .out(jsonBody[Book]) - .serverLogic { id => - service.find(Id(id)).map(_.toRight(Fail.NotFound(s"Book for id: $id Not Found"): Fail)) - } + .serverLogic { id => service.find(Id(id)).orError(s"Book for id: $id Not Found") } // List books private val sortPageFields: EndpointInput[PageRequest] = sortPage( @@ -56,13 +50,13 @@ class BookApi(service: BookService) extends HasTapirResource with BookCodecs wit private val list = base.get .in(sortPageFields / filterFields) .out(jsonBody[PageResponse[Book]]) - .serverLogicSuccess { case (pr, filters) => service.list(pr, filters) } + .serverLogic { case (pr, filters) => service.list(pr, filters).orError } // Delete a book by id private val delete = base.delete .in(pathId) .out(statusCode(NoContent)) - .serverLogicSuccess { id => service.delete(Id(id)) } + .serverLogic { id => service.delete(Id(id)).orError } // Endpoints to Expose override val endpoints: ServerEndpoints = List(post, put, get, list, delete) diff --git a/src/main/scala/com/example/books/infrastructure/http/PublisherApi.scala b/src/main/scala/com/example/books/infrastructure/http/PublisherApi.scala index 62eee09..00b7269 100644 --- a/src/main/scala/com/example/books/infrastructure/http/PublisherApi.scala +++ b/src/main/scala/com/example/books/infrastructure/http/PublisherApi.scala @@ -16,26 +16,20 @@ class PublisherApi(service: PublisherService) extends HasTapirResource with Publ private val post = base.post .in(jsonBody[Publisher]) .out(statusCode(Created)) - .serverLogicSuccess { publisher => - service.create(publisher) - } + .serverLogic { publisher => service.create(publisher).orError } // Update a existing publisher private val put = base.put .in(pathId) .in(jsonBody[Publisher]) .out(statusCode(NoContent)) - .serverLogicSuccess { case (id, publisher) => - service.update(Id(id), publisher) - } + .serverLogic { case (id, publisher) => service.update(Id(id), publisher).orError } // Get a publisher by id private val get = base.get .in(pathId) .out(jsonBody[Publisher]) - .serverLogic { id => - service.find(Id(id)).map(_.toRight(Fail.NotFound(s"Publisher for id: $id Not Found"): Fail)) - } + .serverLogic { id => service.find(Id(id)).orError(s"Publisher for id: $id Not Found") } // List publishers private val sortPageFields: EndpointInput[PageRequest] = sortPage(Seq("name", "url")) @@ -43,13 +37,13 @@ class PublisherApi(service: PublisherService) extends HasTapirResource with Publ private val list = base.get .in(sortPageFields / filter) .out(jsonBody[PageResponse[Publisher]]) - .serverLogicSuccess { case (pr, filter) => service.list(pr, filter) } + .serverLogic { case (pr, filter) => service.list(pr, filter).orError } // Delete a publisher by id private val delete = base.delete .in(pathId) .out(statusCode(NoContent)) - .serverLogicSuccess { id => service.delete(Id(id)) } + .serverLogic { id => service.delete(Id(id)).orError } // Endpoints to Expose override val endpoints: ServerEndpoints = List(post, put, get, list, delete) diff --git a/src/main/scala/com/example/shared/infrastructure/http/Fail.scala b/src/main/scala/com/example/shared/infrastructure/http/Fail.scala index 7c0b3ad..b5f80f3 100644 --- a/src/main/scala/com/example/shared/infrastructure/http/Fail.scala +++ b/src/main/scala/com/example/shared/infrastructure/http/Fail.scala @@ -3,15 +3,15 @@ package com.example.shared.infrastructure.http abstract class Fail extends Exception object Fail { - case class NotFound(msg: String) extends Fail - case class Conflict(msg: String) extends Fail - case class IncorrectInput(msg: String) extends Fail - case class Unauthorized(msg: String) extends Fail - case object BadRequest extends Fail - case object Forbidden extends Fail - case object UnprocessableEntity extends Fail - case object InternalServerError extends Fail - case object NotImplemented extends Fail + case class NotFound(msg: String) extends Fail + case class Conflict(msg: String) extends Fail + case class IncorrectInput(msg: String) extends Fail + case class Unauthorized(msg: String) extends Fail + case class BadRequest(msg: String) extends Fail + case class InternalServerError(msg: String) extends Fail + case object Forbidden extends Fail + case object UnprocessableEntity extends Fail + case object NotImplemented extends Fail } //import com.alejandrohdezma.tapir._ diff --git a/src/main/scala/com/example/shared/infrastructure/http/HasTapirResource.scala b/src/main/scala/com/example/shared/infrastructure/http/HasTapirResource.scala index 53956e8..232ef6b 100644 --- a/src/main/scala/com/example/shared/infrastructure/http/HasTapirResource.scala +++ b/src/main/scala/com/example/shared/infrastructure/http/HasTapirResource.scala @@ -1,5 +1,6 @@ package com.example.shared.infrastructure.http +import cats.effect.IO import com.example.shared.domain.page.PageRequest import io.circe.generic.AutoDerivation import sttp.model.StatusCodes @@ -7,6 +8,7 @@ import sttp.tapir.generic.auto.SchemaDerivation import sttp.tapir.json.circe.TapirJsonCirce import sttp.tapir.{Tapir, TapirAliases} +import java.sql.SQLException import java.util.UUID trait HasTapirResource @@ -48,13 +50,39 @@ trait HasTapirResource oneOf[Fail]( oneOfVariant(NotFound, jsonBody[Fail.NotFound]), oneOfVariant(Conflict, jsonBody[Fail.Conflict]), - oneOfVariant(BadRequest, jsonBody[Fail.BadRequest.type]), + oneOfVariant(BadRequest, jsonBody[Fail.BadRequest]), oneOfVariant(BadRequest, jsonBody[Fail.IncorrectInput]), oneOfVariant(Unauthorized, jsonBody[Fail.Unauthorized]), + oneOfVariant(InternalServerError, jsonBody[Fail.InternalServerError]), oneOfVariant(Forbidden, jsonBody[Fail.Forbidden.type]), oneOfVariant(UnprocessableEntity, jsonBody[Fail.UnprocessableEntity.type]), - oneOfVariant(InternalServerError, jsonBody[Fail.InternalServerError.type]), oneOfVariant(NotImplemented, jsonBody[Fail.NotImplemented.type]) ) ) + + private def ioOrError[T](io: IO[T]): IO[Either[Fail, T]] = io.map(Right(_)).handleError { + case th: Fail => Left(th) + case _: NotImplementedError => Left(Fail.NotImplemented) + case iae: IllegalArgumentException => Left(Fail.BadRequest(iae.getMessage)) + case sqlEx: SQLException => + val message = sqlEx.getMessage.split("Detail:").lastOption.getOrElse("SQL Conflict") + Left(Fail.Conflict(message)) + case th: Throwable => + if (th.getMessage != null && th.getMessage.contains("requirement failed")) { + Left(Fail.BadRequest(th.getMessage)) + } else { + val msg = s"Error[${UUID.randomUUID().toString.take(10)}]: contact support" + Left(Fail.InternalServerError(msg)) + } + + } + + implicit class ServerEndpointsLogicOps[T](io: IO[T]) { + def orError: IO[Either[Fail, T]] = ioOrError(io) + } + + implicit class OptionEndpointsLogicOps[T](io: IO[Option[T]]) { + def orError(msg: String): IO[Either[Fail, T]] = io.map(_.toRight(Fail.NotFound(msg): Fail)) + } + }