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

polynote.config.package.scala Maven / Gradle / Ivy

The newest version!
package polynote

import java.io.File
import java.net.URI
import java.nio.file.{Path, Paths}
import java.util.regex.Pattern

import cats.data.{NonEmptyList, Validated, ValidatedNel}
import io.circe.{CursorOp, Decoder, DecodingFailure, Encoder, HCursor, Json}
import polynote.messages.{TinyList, TinyMap, TinyString}
import scodec.codecs.{Discriminated, Discriminator, byte}
import io.circe.generic.extras.{Configuration, JsonKey}
import io.circe.generic.extras.semiauto._
import cats.syntax.traverse._
import cats.syntax.either._
import cats.instances.list._
import cats.instances.either._
import io.circe.Decoder.Result
import io.circe.generic.extras.decoding.{ConfiguredDecoder, ReprDecoder}
import io.circe.generic.extras.util.RecordToMap
import shapeless.labelled.FieldType
import shapeless.ops.hlist.{Mapped, Mapper, RightFolder, ToTraversable, Zip, ZipWithKeys}
import shapeless.ops.record.{Keys, Values}
import shapeless.{::, Annotations, Default, HList, HNil, LabelledGeneric, Lazy, Poly1, Poly2, Witness}

package object config {
  //                               lang      , deps
  type DependencyConfigs = TinyMap[TinyString, TinyList[TinyString]]

  sealed trait RepositoryConfig
  final case class ivy(
    base: String,
    @JsonKey("artifact_pattern") artifactPatternOpt: Option[String] = None,
    @JsonKey("metadata_pattern") metadataPatternOpt: Option[String] = None,
    changing: Option[Boolean] = None
  ) extends RepositoryConfig {

    def artifactPattern: String = artifactPatternOpt.filter(s => s.nonEmpty).getOrElse("[orgPath]/[module](_[scalaVersion])(_[sbtVersion])/[revision]/[artifact]-[revision](-[classifier]).[ext]")
    def metadataPattern: String = metadataPatternOpt.filter(s => s.nonEmpty).getOrElse("[orgPath]/[module](_[scalaVersion])(_[sbtVersion])/[revision]/[module](_[scalaVersion])(_[sbtVersion])-[revision]-ivy.xml")

  }

  object ivy {
    implicit val discriminator: Discriminator[RepositoryConfig, ivy, Byte] = Discriminator(0)
  }

  final case class maven(
    base: String,
    changing: Option[Boolean] = None
  ) extends RepositoryConfig

  object maven {
    implicit val discriminator: Discriminator[RepositoryConfig, maven, Byte] = Discriminator(1)
  }

  final case class pip(
    url: String
  ) extends RepositoryConfig

  object pip {
    implicit val discriminator: Discriminator[RepositoryConfig, pip, Byte] = Discriminator(2)
  }

  object RepositoryConfig {

    implicit val discriminated: Discriminated[RepositoryConfig, Byte] = Discriminated(byte)
    implicit val encoder: Encoder.AsObject[RepositoryConfig] = deriveEncoder
    implicit val decoder: Decoder[RepositoryConfig] = deriveDecoder
  }

  implicit val circeConfig: Configuration =
    Configuration.default.withSnakeCaseConstructorNames.withSnakeCaseMemberNames.withDefaults

  abstract class ValidatedConfigDecoder[A] extends ConfiguredDecoder[A](circeConfig.copy(useDefaults = false))

  object ValidatedConfigDecoder {
    object optionalOrDefault extends Poly2 {
      implicit def someDefault[K, A, OutT <: HList]: Case.Aux[(FieldType[K, Option[A]], Some[A]), ValidatedNel[DecodingFailure, OutT], ValidatedNel[DecodingFailure, A :: OutT]] =
        at[(FieldType[K, Option[A]], Some[A]), ValidatedNel[DecodingFailure, OutT]] {
          case ((in, default), accum) => accum.andThen(outT => Validated.validNel(in.getOrElse(default.get) :: outT))
        }

      implicit def noDefault[K <: Symbol, A, OutT <: HList](implicit name: Witness.Aux[K]): Case.Aux[(FieldType[K, Option[A]], None.type), ValidatedNel[DecodingFailure, OutT], ValidatedNel[DecodingFailure, A :: OutT]] =
        at[(FieldType[K, Option[A]], None.type), ValidatedNel[DecodingFailure, OutT]] {
          (in, accum) => in._1.asInstanceOf[Option[A]] match {
            case Some(a) => accum.andThen(outT => Validated.Valid(a :: outT))
            case None =>
              val failure = DecodingFailure(s"Missing non-optional field ${name.value.name}", List(CursorOp.Field(name.value.name)))
              Validated.Invalid(accum.swap.map(failure :: _).getOrElse(NonEmptyList(failure, Nil)))
          }
        }
    }

    /**
      * This complex machine is for allowing us to keep our default values for the config ADT, but still validate the
      * configs properly during decoding. Circe (at least the version available to us given Scala 2.11 constraint) silently
      * drops decoding errors when there's a default value, and just uses the default instead. This derivation does
      * something different:
      * - Derive the shapeless record type `OR` that corresponds to the case class `A`, but with all values wrapped in `Option`.
      * - Derive the circe decoder for `OR`, which allows values to be unspecified but does not drop validation errors.
      * - Put the optionalized record together with the defaults from the case class constructor, and do another round of
      *   validation where a `None` is replaced by the default (if it exists) or a validation error (if there is no default).
      * - Within the validated structure, go back to the original record type `R` that corresponds to the case class and
      *   use the shapeless LabelledGeneric to translate it to a validated case class instance.
      */
    implicit def deriveConfigDecoder[A, R <: HList, F <: HList, V <: HList, OV <: HList, OR <: HList, D <: HList, ZD <: HList, K <: HList](
      implicit
      gen: LabelledGeneric.Aux[A, R],
      fields: Keys.Aux[R, F],
      values: Values.Aux[R, V],
      optionized: Mapped.Aux[V, Option, OV],
      optionizedRecord: ZipWithKeys.Aux[F, OV, OR],
      decodeR: Lazy[ReprDecoder[OR]],
      defaults: Default.Aux[A, D],
      fieldsToList: ToTraversable.Aux[F, List, Symbol],
      keys: Annotations.Aux[JsonKey, A, K],
      keysToList: ToTraversable.Aux[K, List, Option[JsonKey]],
      zipWithDefaults: Zip.Aux[OR :: D :: HNil, ZD],
      mapper: RightFolder.Aux[ZD, ValidatedNel[DecodingFailure, HNil], optionalOrDefault.type, ValidatedNel[DecodingFailure, V]],
      relabel: ZipWithKeys.Aux[F, V, R]
    ): ValidatedConfigDecoder[A] = new ValidatedConfigDecoder[A] {
      override def apply(c: HCursor): Result[A] = decodeR.value.configuredDecodeAccumulating(c)(circeConfig.transformMemberNames, circeConfig.transformConstructorNames, Map.empty, circeConfig.discriminator).andThen {
        OR => mapper(zipWithDefaults(OR :: defaults() :: HNil), Validated.valid(HNil))
      }.map {
        V => gen.from(relabel(V))
      }.toEither.leftMap {
        errs =>
          val errsList = errs.toList
          val combinedErrs = ("" :: errsList.map(_.message)).mkString("\n- ")
          DecodingFailure(s"Configuration is invalid:$combinedErrs", errsList.flatMap(_.history))
      }
    }
  }

  def deriveConfigDecoder[A](implicit decoder: Lazy[ValidatedConfigDecoder[A]]): Decoder[A] = decoder.value

  implicit val mapStringStringDecoder: Decoder[Map[String, String]] = Decoder[Map[String, Json]].emap {
    jsonMap => jsonMap.toList.map {
      case (key, json) => json.fold(
        Left("No null values allowed in this map"),
        b => Right(key -> b.toString),
        n =>
          n.toLong
            .map(_.toString).orElse(n.toBigDecimal.map(_.toString()))
            .fold(Left("No invalid numeric values allowed in this map"): Either[String, (String, String)])(v => Right(key -> v)),
        s => Right(key -> s),
        _ => Left("No array values allowed in this map"),
        _ => Left("No object values allowed in this map")
      )
    }.sequence.map(_.toMap)
  }

  implicit val patternDecoder: Decoder[Pattern] = Decoder.decodeString.emap {
    regex => Either.catchNonFatal(Pattern.compile(regex)).leftMap(err => s"Invalid regular expression: ${err.getMessage}")
  }

  implicit val patternEncoder: Encoder[Pattern] = Encoder.encodeString.contramap(_.pattern())

  implicit val pathEncoder: Encoder[Path] = Encoder.encodeString.contramap(_.toString())
  implicit val pathDecoder: Decoder[Path] = Decoder.decodeString.emap {
    pathStr => Either.catchNonFatal(new File(pathStr).toPath).leftMap(err => s"Malformed path: ${err.getMessage}")
  }

  implicit val uriEncoder: Encoder[URI] = Encoder.encodeString.contramap(_.toString())
  implicit val uriDecoder: Decoder[URI] = Decoder.decodeString.emap {
    uriStr => Either.catchNonFatal(new URI(uriStr)).leftMap(err => s"Malformed URI: ${err.getMessage}")
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy