Ref
+ Deferred
you actually already made one !
Bracket
instance
Bracket
for any MonadError
type ValidationResult[A] = ValidatedNec[JobPostError, A]
override def createJobPost(
newJobPost: JobPost
): IO[ValidatedNec[String, JobPost]] = {
// TODO: Add logger here on every field validation
val localValidation: ValidationResult[JobPost] = (
validateNoop(newJobPost.id),
validateEmptiness("Job post title".some, newJobPost.title),
validateEmptiness("Job post description".some,
newJobPost.description),
validateNoop(newJobPost.createdAt),
validateNoop(newJobPost.modifiedAt)
).mapN(JobPost)
val databaseValidations: IO[ValidationResult[JobPost]] = (for {
_ <- Logger[ConnectionIO].info("Validating job title")
title <- validateTitleExistence(newJobPost)
_ <- Logger[ConnectionIO].info("Validating job description")
description <- validateNoop(newJobPost.description).pure[ConnectionIO]
createdAt <- validateNoop(newJobPost.createdAt).pure[ConnectionIO]
modifiedAt <- validateNoop(newJobPost.modifiedAt).pure[ConnectionIO]
id <- validateNoop(newJobPost.id).pure[ConnectionIO]
} yield {
(id, title, description, createdAt, modifiedAt).mapN(JobPost)
}).transact(xa)
val response: IO[ValidatedNec[String, JobPost]] =
localValidation match {
case Valid(_) => databaseValidations.map(_.leftMap(_.map(_.errorMessage)))
case Invalid(e) => e.map(_.errorMessage).invalid[JobPost].pure[IO]
}
response
}
Valid
case in your result, so you don't even need the last clause
Band
IO.parSequence
or parSequenceN
. It's not immediately obvious where that behavior lives if you're using IO
directly and not the typeclasses. Even with the typeclasses, parSequence
and parSequenceN
for example exist in different typeclasses, and it's not necessarily obvious to a newcomer why that might be
IO
and some cases like that make things less discoverable than I'd want it to be
IO
as direct methods; things like >>
or adaptError