All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.malliina.config.ConfigReadable.scala Maven / Gradle / Ivy

The newest version!
package com.malliina.config

import cats.data.NonEmptyList
import com.malliina.config.ConfigReadable.append
import com.malliina.values.{ErrorMessage, Readable}
import com.typesafe.config.{Config, ConfigException}

import java.nio.file.{InvalidPathException, Path, Paths}

sealed abstract class ConfigError(
  val message: ErrorMessage,
  val path: NonEmptyList[String],
  inner: Option[Exception]
) extends Exception(message.message, inner.orNull) {
  def key: String = path.last
}
class MissingValue(path: NonEmptyList[String])
  extends ConfigError(
    ErrorMessage(s"Missing: ${path.toList.mkString(".")}."),
    path,
    None
  )
class InvalidValue(
  message: ErrorMessage,
  path: NonEmptyList[String],
  e: Option[Exception]
) extends ConfigError(message, path, e) {
  def this(path: NonEmptyList[String], e: Option[Exception]) =
    this(ErrorMessage(s"Failed to read '${path.last}' at ${path.toList.mkString(".")}."), path, e)
}

trait ConfigReadable[T] {
  def parseOpt(key: String, c: ConfigNode): Either[ConfigError, Option[T]]
  def read(key: String, c: ConfigNode): Either[ErrorMessage, T] =
    parse(key, c).left.map(_.message)
  def parse(key: String, c: ConfigNode): Either[ConfigError, T] =
    parseOpt(key, c).flatMap { opt =>
      opt.toRight(new MissingValue(append(key, to = c.position)))
    }
  def parseOrElse(key: String, c: ConfigNode, orElse: => T): Either[ConfigError, T] =
    parseOpt(key, c).map { opt =>
      opt.getOrElse(orElse)
    }
  def flatMap[U](f: T => ConfigReadable[U]): ConfigReadable[U] = {
    val parent = this
    (key: String, c: ConfigNode) =>
      parent
        .parseOpt(key, c)
        .flatMap(opt => opt.map(t => f(t).parseOpt(key, c)).getOrElse(Right(None)))
  }
  def emap[U](f: T => Either[ConfigError, U]): ConfigReadable[U] =
    flatMap { t => (key, _) =>
      f(t).map(u => Option(u))
    }
  def emapParsed[U](f: T => Either[ErrorMessage, U]): ConfigReadable[U] =
    flatMap { t => (key, c) =>
      f(t)
        .map(u => Option(u))
        .left
        .map(err => new InvalidValue(err, append(key, c.position), None))
    }
  def map[U](f: T => U): ConfigReadable[U] =
    emapParsed(t => Right(f(t)))
}

object ConfigReadable {
  implicit class ConfigOps(val c: Config) extends AnyVal {
    def read[T](key: String)(implicit r: ConfigReadable[T]): Either[ErrorMessage, T] =
      r.read(key, ConfigNode.root(c))
    def parse[T](key: String)(implicit r: ConfigReadable[T]): Either[ConfigError, T] =
      r.parse(key, ConfigNode.root(c))
    def opt[T](key: String)(implicit r: ConfigReadable[T]): Either[ConfigError, Option[T]] =
      r.parseOpt(key, ConfigNode.root(c))

    def bool(key: String, position: List[String]) = recovered(key, position, c.getBoolean)
    def int(key: String, position: List[String]) = recovered(key, position, c.getInt)
    def string(key: String, position: List[String]) = recovered(key, position, c.getString)
    def config(key: String, position: List[String]) = recovered(key, position, c.getConfig)
  }
  implicit val string: ConfigReadable[String] = (key: String, c: ConfigNode) =>
    c.conf.string(key, c.position)
  implicit val int: ConfigReadable[Int] = (key: String, c: ConfigNode) =>
    c.conf.int(key, c.position)
  implicit val bool: ConfigReadable[Boolean] = (key: String, c: ConfigNode) =>
    c.conf.bool(key, c.position)
  implicit val node: ConfigReadable[ConfigNode] = (key: String, c: ConfigNode) =>
    c.conf
      .config(key, c.position)
      .map(optConf => optConf.map(next => new ConfigNode(next, c.position ++ key.split('.'))))
  implicit val path: ConfigReadable[Path] = ConfigReadable.string.emapParsed { s =>
    try Right(Paths.get(s))
    catch {
      case ipe: InvalidPathException => Left(ErrorMessage(s"Invalid path: '$s'."))
    }
  }
  implicit def readable[T](implicit r: Readable[T]): ConfigReadable[T] =
    string.emapParsed(s => r.read(s))

  def recovered[T](
    key: String,
    position: List[String],
    unsafe: String => T
  ): Either[ConfigError, Option[T]] =
    try {
      Right(Option(unsafe(key)))
    } catch {
      case m: ConfigException.Missing =>
        Right(None)
      case e: Exception =>
        Left(new InvalidValue(append(key, position), Option(e)))
    }

  private def append(key: String, to: List[String]) =
    NonEmptyList.fromList(to ++ key.split('.')).getOrElse(NonEmptyList.of(key))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy