I'm struggling a bit with how to translate an imperative style to a functional style.
In a imperative web request I'm used to saying something like the following psudo code:
public Response controllerAction(Request request) {
val (req, parserErrors) = parser.parseRequest(request);
if (parserErrors.any()) {
return FourHundredError(parserErrors);
}
val businessErrors = model.validate(req);
if (businessErrors.any()){
return FourOhFour(businessErrors);
}
val (response, errorsWithOurStuff) = model.doBusinessLogicStuff(req);
if (errorsWithOurStuff.any()) {
return FiveHundredError(errorsWithOurStuff);
}
return OK(response)
}
I'm trying to translate this into a functional style using http4s.
def businessRoutes[F[_]: Sync](BL: BusinessLogic[F]): HttpRoutes[F] = {
val dsl = new Http4sDsl[F]{}
import dsl._
HttpRoutes.of[F] {
case req @ POST -> Root / "sms" =>
for {
request <- req.as[BL.BuisnessRequest]
requestErrors <- BL.validateRequest(request)
response <- if (requestErrors.isEmpty) {
BL.processRequest(request) match {
case Failure(e) => InternalServerError(e)
case Success(response) => Ok(response)
}
} else {
BadRequest(requestErrors)
}
} yield response
}
}
The above code just looks... bad to me and I don't know how to make it better. My goal for this is to keep all the http style abstractions contained here, because I don't want to leak http4s or circe down into a business layer. I feel like I've got a for
then an if
, then a match
and the responses are all jumbled together out of order. I'm writing hard to understand code here and I'm hoping some scala guru could show me how to clean this up and make this readable.
CodePudding user response:
IMHO, the problem is rooted in the way you are modeling the data; mainly with validateRequest
Always remember, parse, don't validate.
Additionally, I would go the untyped errors route with a main handler like this:
import cats.syntax.all._
import io.circe.{Error => CirceError}
object model {
final case class RawRequest(...)
final case class BuisnessRequest(...)
final case class BuisnessResponse(...)
}
object errors {
// Depending on how you end up using those,
// it may be good to use scala.util.control.NoStackTrace with these.
// They may also be case classes to hold some context.
final case object ValidationError extends Throwable
final case object BusinessError extends Throwable
}
trait BusinessLogic {
def validateRequest(rawRequest: RawRequest): IO[BusinessRequets]
def processRequest(request: BusinessRequets): IO[BuisnessResponse]
}
final class HttpLayer(bl: BusinessLogic) extends Http4sDsl[IO] {
private final val errorHanlder: PartialFunction[Throwable, IO[Response[IO]] = {
case circeError: CirceError =>
BadRequest(...)
case ValidationError =>
NotFound(...)
case BusinessError =>
InternalServerError(...)
}
val routes: HttpRoutes[IO] = HttpRoutes[F] {
case req @ POST -> Root / "sms" =>
req
.as[RawRequest] // This may fail with CirceError.
.flatMap(bl.validateRequest) // This may fail with ValidationError.
.flatMap(bl.processRequest) // This may fail with BusinessError.
.redeemWith(recover = errorHandler, response => Ok(response))
}
}
I used concrete
IO
here for simplicity, you may useF[_]
if you please.