Refactor config validation

This commit is contained in:
eikek
2021-10-25 11:27:06 +02:00
parent 9af61cb4a4
commit 668cd7d974
5 changed files with 147 additions and 77 deletions

View File

@ -28,20 +28,21 @@ object ConfigFactory {
* the default config
*/
def default[F[_]: Async, C: ClassTag: ConfigReader](logger: Logger[F], atPath: String)(
args: List[String]
args: List[String],
validation: Validation[C]
): F[C] =
findFileFromArgs(args).flatMap {
case Some(file) =>
logger.info(s"Using config file: $file") *>
readFile[F, C](file, atPath)
readFile[F, C](file, atPath).map(validation.validOrThrow)
case None =>
checkSystemProperty.value.flatMap {
case Some(file) =>
logger.info(s"Using config file from system property: $file") *>
readConfig(atPath)
readConfig(atPath).map(validation.validOrThrow)
case None =>
logger.info("Using config from environment variables!") *>
readEnv(atPath)
readEnv(atPath).map(validation.validOrThrow)
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.config
import cats._
import cats.data.{NonEmptyChain, Validated, ValidatedNec}
import cats.implicits._
final case class Validation[C](run: C => ValidatedNec[String, C]) {
def validOrThrow(c: C): C =
run(c) match {
case Validated.Valid(cfg) => cfg
case Validated.Invalid(errs) =>
val msg = errs.toList.mkString("- ", "\n- ", "\n")
throw sys.error(s"\n\n$msg")
}
def andThen(next: Validation[C]): Validation[C] =
Validation(c =>
run(c) match {
case Validated.Valid(c2) => next.run(c2)
case f: Validated.Invalid[NonEmptyChain[String]] =>
next.run(c) match {
case Validated.Valid(_) => f
case Validated.Invalid(errs2) =>
Validation.invalid(f.e ++ errs2)
}
}
)
}
object Validation {
def flatten[C](run: C => Validation[C]): Validation[C] =
Validation(c => run(c).run(c))
def failWhen[C](isInvalid: C => Boolean, msg: => String): Validation[C] =
Validation(c => if (isInvalid(c)) invalid(msg) else valid(c))
def okWhen[C](isValid: C => Boolean, msg: => String): Validation[C] =
Validation(c => if (isValid(c)) valid(c) else invalid(msg))
def valid[C](c: C): ValidatedNec[String, C] =
Validated.validNec(c)
def invalid[C](msgs: NonEmptyChain[String]): ValidatedNec[String, C] =
Validated.Invalid(msgs)
def invalid[C](msg: String, msgs: String*): ValidatedNec[String, C] =
Validated.Invalid(NonEmptyChain(msg, msgs: _*))
def asValid[C]: Validation[C] =
Validation(c => valid(c))
def insert[C](c: C): Validation[C] =
Validation(_ => valid(c))
def error[C](msg: String, msgs: String*): Validation[C] =
Validation(_ => invalid(msg, msgs: _*))
implicit def validationMonoid[C]: Monoid[Validation[C]] =
Monoid.instance(asValid, (v1, v2) => v1.andThen(v2))
def of[C](v1: Validation[C], vn: Validation[C]*): Validation[C] =
Monoid[Validation[C]].combineAll(v1 :: vn.toList)
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.config
import munit.FunSuite
class ValidationTest extends FunSuite {
test("thread value through validations") {
val v1 = Validation[Int](n => Validation.valid(n + 1))
assertEquals(v1.validOrThrow(0), 1)
assertEquals(Validation.of(v1, v1, v1).validOrThrow(0), 3)
}
test("fail if there is at least one error") {
val v1 = Validation[Int](n => Validation.valid(n + 1))
val v2 = Validation.error[Int]("error")
assertEquals(Validation.of(v1, v2).run(0), Validation.invalid("error"))
assertEquals(Validation.of(v2, v1).run(0), Validation.invalid("error"))
}
}