add docs errors
This commit is contained in:
parent
5180c78e8f
commit
f3f458a1b0
@ -14,6 +14,7 @@ import com.comcast.ip4s.{Port, host}
|
||||
import org.http4s.circe.CirceEntityCodec.*
|
||||
import org.yobble.chat_private_service.api.Routes
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import org.yobble.chat_private_service.middleware.GlobalErrorHandler
|
||||
|
||||
object Main extends IOApp {
|
||||
override def run(args: List[String]): IO[ExitCode] =
|
||||
@ -21,7 +22,7 @@ object Main extends IOApp {
|
||||
.default[IO]
|
||||
.withHost(host"0.0.0.0")
|
||||
.withPort(port = Port.fromInt(AppConfig.PORT).get)
|
||||
.withHttpApp(Routes.all.orNotFound)
|
||||
.withHttpApp(GlobalErrorHandler.withGlobalErrorHandler(Routes.all.orNotFound))
|
||||
.build
|
||||
.use(_ =>
|
||||
IO.println(s"Server running at http://${AppConfig.HOST}:${AppConfig.PORT}") *>
|
||||
|
||||
@ -2,24 +2,38 @@ package org.yobble.chat_private_service.api
|
||||
|
||||
import cats.effect.IO
|
||||
import org.http4s.HttpRoutes
|
||||
import sttp.tapir.server.http4s._
|
||||
import sttp.tapir.server.http4s.*
|
||||
import sttp.tapir.swagger.bundle.SwaggerInterpreter
|
||||
import org.yobble.chat_private_service.api.endpoint.PingEndpoint._
|
||||
import cats.syntax.semigroupk._
|
||||
import org.yobble.chat_private_service.api.endpoint.PingEndpoint.*
|
||||
import org.yobble.chat_private_service.api.endpoint.ErrorsEndpoint.*
|
||||
import org.yobble.chat_private_service.api.endpoint.TestErrorEndpoint.*
|
||||
import cats.syntax.semigroupk.*
|
||||
import org.yobble.chat_private_service.api.response.BaseResponse
|
||||
import sttp.model.StatusCode
|
||||
import org.yobble.chat_private_service.api.util.errorByStatus
|
||||
|
||||
object Routes {
|
||||
private val pingRoute = Http4sServerInterpreter[IO]().toRoutes(
|
||||
pingEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse("fine", "pong")))
|
||||
)
|
||||
|
||||
private val errorsRoute = Http4sServerInterpreter[IO]().toRoutes(
|
||||
errorsEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse("fine", "errors")))
|
||||
)
|
||||
|
||||
private val testErrorRoute = Http4sServerInterpreter[IO]().toRoutes(
|
||||
testErrorEndpoint.serverLogic((code: Int) =>
|
||||
IO.pure(Left(errorByStatus(StatusCode.safeApply(code).getOrElse(StatusCode.InternalServerError))))
|
||||
)
|
||||
)
|
||||
|
||||
private val docsRoutes = Http4sServerInterpreter[IO]().toRoutes(
|
||||
SwaggerInterpreter().fromEndpoints[IO](
|
||||
List(pingEndpoint),
|
||||
List(pingEndpoint, errorsEndpoint, testErrorEndpoint),
|
||||
"chat_private_service API",
|
||||
"1.0"
|
||||
)
|
||||
)
|
||||
|
||||
val all: HttpRoutes[IO] = pingRoute <+> docsRoutes
|
||||
val all: HttpRoutes[IO] = pingRoute <+> errorsRoute <+> testErrorRoute <+> docsRoutes
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package org.yobble.chat_private_service.api.endpoint
|
||||
|
||||
import io.circe.generic.auto.*
|
||||
import org.yobble.chat_private_service.api.response.BaseResponse
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import sttp.tapir.*
|
||||
import sttp.tapir.generic.auto.*
|
||||
import sttp.tapir.json.circe.*
|
||||
import org.yobble.chat_private_service.api.response.ErrorResponse
|
||||
import org.yobble.chat_private_service.api.util.ErrorExamples
|
||||
import sttp.model.StatusCode
|
||||
|
||||
|
||||
object ErrorsEndpoint {
|
||||
|
||||
val errorsEndpoint: PublicEndpoint[Unit, ErrorResponse, BaseResponse, Any] =
|
||||
endpoint.get
|
||||
.in("errors")
|
||||
.tags(List("Info"))
|
||||
.errorOut(
|
||||
oneOf[ErrorResponse](
|
||||
oneOfVariant(StatusCode.BadRequest, jsonBody[ErrorResponse].description("Bad Request").example(ErrorExamples.badRequest)),
|
||||
oneOfVariant(StatusCode.Unauthorized, jsonBody[ErrorResponse].description("Unauthorized").example(ErrorExamples.unauthorized)),
|
||||
oneOfVariant(StatusCode.Forbidden, jsonBody[ErrorResponse].description("Forbidden").example(ErrorExamples.forbidden)),
|
||||
oneOfVariant(StatusCode.NotFound, jsonBody[ErrorResponse].description("Not Found").example(ErrorExamples.notFound)),
|
||||
oneOfVariant(StatusCode.MethodNotAllowed, jsonBody[ErrorResponse].description("Method Not Allowed").example(ErrorExamples.notAllowed)),
|
||||
oneOfVariant(StatusCode.Conflict, jsonBody[ErrorResponse].description("Conflict").example(ErrorExamples.conflict)),
|
||||
oneOfVariant(StatusCode(418), jsonBody[ErrorResponse].description("I'm a teapot (In Development)").example(ErrorExamples.teapot)),
|
||||
oneOfVariant(StatusCode.UnprocessableEntity, jsonBody[ErrorResponse].description("Validation Error").example(ErrorExamples.validation)),
|
||||
oneOfVariant(StatusCode.InternalServerError, jsonBody[ErrorResponse].description("Internal Server Error").example(ErrorExamples.internal))
|
||||
)
|
||||
)
|
||||
.out(jsonBody[BaseResponse])
|
||||
}
|
||||
@ -12,5 +12,6 @@ object PingEndpoint {
|
||||
val pingEndpoint: PublicEndpoint[Unit, Unit, BaseResponse, Any] =
|
||||
endpoint.get
|
||||
.in("ping")
|
||||
.tags(List("Info"))
|
||||
.out(jsonBody[BaseResponse])
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
package org.yobble.chat_private_service.api.endpoint
|
||||
|
||||
import io.circe.generic.auto.*
|
||||
import org.yobble.chat_private_service.api.response.{BaseResponse, ErrorResponse}
|
||||
import org.yobble.chat_private_service.api.util.ErrorExamples
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import sttp.model.StatusCode
|
||||
import sttp.tapir.*
|
||||
import sttp.tapir.generic.auto.*
|
||||
import sttp.tapir.json.circe.*
|
||||
|
||||
|
||||
object TestErrorEndpoint {
|
||||
|
||||
val testErrorEndpoint: Endpoint[Unit, Int, ErrorResponse, BaseResponse, Any] =
|
||||
endpoint.get
|
||||
.in("test-error")
|
||||
.in(query[Int]("code").description("HTTP status code to simulate (e.g., 401, 404)"))
|
||||
.errorOut(
|
||||
oneOf[ErrorResponse](
|
||||
oneOfVariant(StatusCode.BadRequest, jsonBody[ErrorResponse].description("Bad Request").example(ErrorExamples.badRequest)),
|
||||
oneOfVariant(StatusCode.Unauthorized, jsonBody[ErrorResponse].description("Unauthorized").example(ErrorExamples.unauthorized)),
|
||||
oneOfVariant(StatusCode.Forbidden, jsonBody[ErrorResponse].description("Forbidden").example(ErrorExamples.forbidden)),
|
||||
oneOfVariant(StatusCode.NotFound, jsonBody[ErrorResponse].description("Not Found").example(ErrorExamples.notFound)),
|
||||
oneOfVariant(StatusCode.MethodNotAllowed, jsonBody[ErrorResponse].description("Method Not Allowed").example(ErrorExamples.notAllowed)),
|
||||
oneOfVariant(StatusCode.Conflict, jsonBody[ErrorResponse].description("Conflict").example(ErrorExamples.conflict)),
|
||||
oneOfVariant(StatusCode(418), jsonBody[ErrorResponse].description("I'm a teapot (In Development)").example(ErrorExamples.teapot)),
|
||||
oneOfVariant(StatusCode.UnprocessableEntity, jsonBody[ErrorResponse].description("Validation Error").example(ErrorExamples.validation)),
|
||||
oneOfVariant(StatusCode.InternalServerError, jsonBody[ErrorResponse].description("Internal Server Error").example(ErrorExamples.internal))
|
||||
)
|
||||
)
|
||||
.out(jsonBody[BaseResponse])
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package org.yobble.chat_private_service.api.response
|
||||
|
||||
final case class ErrorDetail(field: String, message: String)
|
||||
final case class ErrorResponse(status: String = "error", errors: List[ErrorDetail])
|
||||
@ -0,0 +1,96 @@
|
||||
package org.yobble.chat_private_service.api.util
|
||||
|
||||
import sttp.model.StatusCode
|
||||
import org.yobble.chat_private_service.api.response.{ErrorDetail, ErrorResponse}
|
||||
|
||||
object ErrorUtils {
|
||||
def badRequest(message: String = "Bad request"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("general", message)))
|
||||
|
||||
def unauthorized(message: String = "Unauthorized"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("login", message)))
|
||||
|
||||
def forbidden(message: String = "Forbidden"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("permission", message)))
|
||||
|
||||
def notFound(message: String = "Not found"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("resource", message)))
|
||||
|
||||
def methodNotAllowed(message: String = "Method not allowed"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("method", message)))
|
||||
|
||||
def conflict(message: String = "Conflict"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("conflict", message)))
|
||||
|
||||
def imATeapot(message: String = "This feature is under development"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("debug", message)))
|
||||
|
||||
def validation(errors: List[(String, String)]): ErrorResponse =
|
||||
ErrorResponse(errors = errors.map((ErrorDetail.apply _).tupled))
|
||||
|
||||
def internalServerError(message: String = "Internal Server Error"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("server", message)))
|
||||
}
|
||||
|
||||
object ErrorExamples {
|
||||
val unauthorized: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("login", "Invalid login or password"))
|
||||
)
|
||||
|
||||
val badRequest: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("general", "Bad request syntax or invalid parameters"))
|
||||
)
|
||||
|
||||
val forbidden: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("permission", "You don't have access to this resource"))
|
||||
)
|
||||
|
||||
val notFound: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("resource", "Requested resource not found"))
|
||||
)
|
||||
|
||||
val notAllowed: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("resource", "Method not allowed on this endpoint"))
|
||||
)
|
||||
|
||||
|
||||
val conflict: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("conflict", "Resource already exists or conflict occurred"))
|
||||
)
|
||||
|
||||
val teapot: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("debug", "This feature is under development"))
|
||||
)
|
||||
|
||||
val validation: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("login", "Login must not contain whitespace characters"))
|
||||
)
|
||||
|
||||
val internal: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("server", "An unexpected error occurred. Please try again later."))
|
||||
)
|
||||
}
|
||||
|
||||
// example use
|
||||
// IO.pure(Left(errorByStatus(StatusCode.Unauthorized)))
|
||||
def errorByStatus(code: StatusCode): ErrorResponse = code match {
|
||||
case c if c == StatusCode.BadRequest => ErrorExamples.badRequest
|
||||
case c if c == StatusCode.Unauthorized => ErrorExamples.unauthorized
|
||||
case c if c == StatusCode.Forbidden => ErrorExamples.forbidden
|
||||
case c if c == StatusCode.NotFound => ErrorExamples.notFound
|
||||
case c if c == StatusCode.MethodNotAllowed => ErrorExamples.notAllowed
|
||||
case c if c == StatusCode.Conflict => ErrorExamples.conflict
|
||||
case c if c.code == 418 => ErrorExamples.teapot
|
||||
case c if c == StatusCode.UnprocessableEntity => ErrorExamples.validation
|
||||
case c if c == StatusCode.InternalServerError => ErrorExamples.internal
|
||||
case _ => ErrorExamples.internal
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package org.yobble.chat_private_service.middleware
|
||||
|
||||
|
||||
import cats.effect.Sync
|
||||
import org.http4s.HttpApp
|
||||
import org.http4s.server.middleware.ErrorAction
|
||||
import org.yobble.chat_private_service.api.util.ErrorUtils
|
||||
|
||||
object GlobalErrorHandler {
|
||||
def withGlobalErrorHandler[F[_] : Sync](httpApp: HttpApp[F]): HttpApp[F] = {
|
||||
val messageFailureLogAction: (Throwable, => String) => F[Unit] =
|
||||
(t, msg) => Sync[F].delay(println(s"Message failure: $msg"))
|
||||
|
||||
val serviceErrorLogAction: (Throwable, => String) => F[Unit] =
|
||||
(t, msg) => Sync[F].delay(println(s"Unhandled error: ${t.getMessage}\n$msg"))
|
||||
|
||||
ErrorAction.log(
|
||||
httpApp,
|
||||
messageFailureLogAction = messageFailureLogAction,
|
||||
serviceErrorLogAction = serviceErrorLogAction
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user