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

org.virtuslab.ideprobe.ConfigFormat.scala Maven / Gradle / Ivy

There is a newer version: 0.53.0
Show newest version
package org.virtuslab.ideprobe

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import com.typesafe.config.ConfigValueFactory
import pureconfig.ConfigReader.Result
import pureconfig._
import pureconfig.error.CannotConvert
import pureconfig.error.ConfigReaderFailures
import pureconfig.error.ConvertFailure
import pureconfig.generic.ProductHint
import scala.reflect.ClassTag
import scala.util.{Success, Try}

// This trait should always be mixed-in to object where we create ConfigReaders
// so that we use consistent ProductHint that is responsible for prohibiting
// unknown keys and case class name mapping (to keep camelCase, default is kebab-case)
trait ConfigFormat {

  private val dateTimeFormat = DateTimeFormatter.ISO_LOCAL_DATE_TIME
  implicit val localDateTimeReader: ConfigReader[LocalDateTime] =
    ConfigReader.fromNonEmptyStringTry(s => Try(LocalDateTime.parse(s, dateTimeFormat)))
  implicit val localDateTimeWriter: ConfigWriter[LocalDateTime] =
    ConfigWriter.fromFunction(l => ConfigValueFactory.fromAnyRef(l.format(dateTimeFormat)))

  implicit val unitReader: ConfigReader[Unit] = ConfigReader
    .fromCursor(c =>
      if (c.isNull) Right(())
      else Left(ConfigReaderFailures(ConvertFailure(CannotConvert(c.toString, "Unit", "Expected null"), c)))
    )
  implicit val unitWriter: ConfigWriter[Unit] = ConfigWriter.fromFunction(_ => ConfigValueFactory.fromAnyRef(null))

  // Default derivation of ADT adds `type` field, which would require us to specify
  // the type each time e.g. { type = Direct, id = "org.intellij.scala", version = "2019.3.1" }
  // or { type = filesystem, path = "/tmp" }. It is more convenient for users, especially for such
  // common classes if we can differentiate what class it is by set of fields. We just have to be sure
  // to not introduce e.g. 2 classes with single field 'path'.
  protected def possiblyAmbiguousAdtReader[Base](
      readers: ConfigReader[_]*
  )(implicit ct: ClassTag[Base]): ConfigReader[Base] =
    (cur: ConfigCursor) => {
      val read = readers.map(_.from(cur)).asInstanceOf[Seq[Result[Base]]]

      read
        .reduceLeft { (acc, that) =>
          if (acc.isRight) acc else that
        }
        .left
        .map { _ =>
          def errors[A](res: Result[A]): List[ConfigReaderFailures] = {
            res.left.toSeq.toList
          }
          val cls = ct.runtimeClass.getName
          val header = ConvertFailure(CannotConvert("", cls, "None of the below alternatives matched"), cur)
          read.flatMap(errors).foldLeft(ConfigReaderFailures(header))(_ ++ _)
        }
    }

  def possiblyAmbiguousAdtWriter[Base](
      writers: ConfigWriter[_]*
  ): ConfigWriter[Base] =
    (obj: Base) => {
      val written = writers.map(writer => Try(writer.asInstanceOf[ConfigWriter[Base]].to(obj)))
      written.collectFirst { case Success(value) => value }.getOrElse {
        throw new RuntimeException(s"Missing writer for ${obj.getClass}")
      }
    }

  implicit def hint[T]: ProductHint[T] =
    ProductHint[T](ConfigFieldMapping(CamelCase, CamelCase), useDefaultArgs = true, allowUnknownKeys = true)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy