Skip to content

Commit

Permalink
feat: added or error handler to control generic errors
Browse files Browse the repository at this point in the history
  • Loading branch information
rodobarcaaa committed Oct 23, 2023
1 parent c1bb2f8 commit 5e99abc
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
16 changes: 5 additions & 11 deletions src/main/scala/com/example/books/infrastructure/http/BookApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,34 @@ 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"))

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)
Expand Down
18 changes: 9 additions & 9 deletions src/main/scala/com/example/shared/infrastructure/http/Fail.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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
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
Expand Down Expand Up @@ -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))
}

}

0 comments on commit 5e99abc

Please sign in to comment.