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.http4s.circe.CirceEntityCodec.*
|
||||||
import org.yobble.chat_private_service.api.Routes
|
import org.yobble.chat_private_service.api.Routes
|
||||||
import org.yobble.chat_private_service.config.AppConfig
|
import org.yobble.chat_private_service.config.AppConfig
|
||||||
|
import org.yobble.chat_private_service.middleware.GlobalErrorHandler
|
||||||
|
|
||||||
object Main extends IOApp {
|
object Main extends IOApp {
|
||||||
override def run(args: List[String]): IO[ExitCode] =
|
override def run(args: List[String]): IO[ExitCode] =
|
||||||
@ -21,7 +22,7 @@ object Main extends IOApp {
|
|||||||
.default[IO]
|
.default[IO]
|
||||||
.withHost(host"0.0.0.0")
|
.withHost(host"0.0.0.0")
|
||||||
.withPort(port = Port.fromInt(AppConfig.PORT).get)
|
.withPort(port = Port.fromInt(AppConfig.PORT).get)
|
||||||
.withHttpApp(Routes.all.orNotFound)
|
.withHttpApp(GlobalErrorHandler.withGlobalErrorHandler(Routes.all.orNotFound))
|
||||||
.build
|
.build
|
||||||
.use(_ =>
|
.use(_ =>
|
||||||
IO.println(s"Server running at http://${AppConfig.HOST}:${AppConfig.PORT}") *>
|
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 cats.effect.IO
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
import sttp.tapir.server.http4s._
|
import sttp.tapir.server.http4s.*
|
||||||
import sttp.tapir.swagger.bundle.SwaggerInterpreter
|
import sttp.tapir.swagger.bundle.SwaggerInterpreter
|
||||||
import org.yobble.chat_private_service.api.endpoint.PingEndpoint._
|
import org.yobble.chat_private_service.api.endpoint.PingEndpoint.*
|
||||||
import cats.syntax.semigroupk._
|
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 org.yobble.chat_private_service.api.response.BaseResponse
|
||||||
|
import sttp.model.StatusCode
|
||||||
|
import org.yobble.chat_private_service.api.util.errorByStatus
|
||||||
|
|
||||||
object Routes {
|
object Routes {
|
||||||
private val pingRoute = Http4sServerInterpreter[IO]().toRoutes(
|
private val pingRoute = Http4sServerInterpreter[IO]().toRoutes(
|
||||||
pingEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse("fine", "pong")))
|
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(
|
private val docsRoutes = Http4sServerInterpreter[IO]().toRoutes(
|
||||||
SwaggerInterpreter().fromEndpoints[IO](
|
SwaggerInterpreter().fromEndpoints[IO](
|
||||||
List(pingEndpoint),
|
List(pingEndpoint, errorsEndpoint, testErrorEndpoint),
|
||||||
"chat_private_service API",
|
"chat_private_service API",
|
||||||
"1.0"
|
"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] =
|
val pingEndpoint: PublicEndpoint[Unit, Unit, BaseResponse, Any] =
|
||||||
endpoint.get
|
endpoint.get
|
||||||
.in("ping")
|
.in("ping")
|
||||||
|
.tags(List("Info"))
|
||||||
.out(jsonBody[BaseResponse])
|
.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