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

pipez.internal.Definitions.scala Maven / Gradle / Ivy

The newest version!
package pipez.internal

import pipez.{ PipeDerivation, PipeDerivationConfig }

import scala.annotation.nowarn
import scala.collection.Factory

/** Contains definitions that could be shared between Scala 2 and Scala 3 implementation of the derivation logic, which
  * aren't strictly related to code parsing and generation.
  *
  * @tparam Pipe
  *   the type class that we are deriving for
  * @tparam In
  *   input type of the type class we are deriving
  * @tparam Out
  *   output type of the type class we are deriving
  */
@nowarn("msg=The outer reference in this type test cannot be checked at run time.")
private[internal] trait Definitions[Pipe[_, _], In, Out] { self =>

  /** Platform-specific type representation (c.universe.Type in 2, scala.quoted.Type[A] in 3) */
  type Type[A]

  /** Platform-specific expression representation (c.universe.Expr[A] in 2, quotes.Expr[A] in 3 */
  type Expr[A]

  /** Summons Type independently of the platform */
  def typeOf[A](implicit tpe: Type[A]): Type[A] = tpe

  /** Provides `Type` instance for `Pipe[I, O]` */
  implicit def PipeOf[I: Type, O: Type]: Type[Pipe[I, O]]

  /** Prints type value */
  def previewType[A: Type]: String

  /** Provides `Type` instance for `In` */
  implicit val In: Type[In]

  /** Provides `Type` instance for `Out` */
  implicit val Out: Type[Out]

  /** Value of `PipeDerivation[Pipe]`, which was passed to macro as (most likely) implicit */
  val pipeDerivation: Expr[PipeDerivation.Aux[Pipe, Definitions.Context, Definitions.Result]]

  /** Like previewCode(pipeDerivation) but allowing hiding some shenanigans we do */
  def previewPipeDerivation: String

  /** Translates `Path` as seen in macro to runtime value we can pass to `updateContext` */
  def pathCode(path: Path): Expr[pipez.Path]

  /** Type representing how we got the specific value from the `in: In` argument */
  sealed trait Path extends Product with Serializable
  object Path {

    case object Root extends Path { override def toString = "_" }
    final case class Field(from: Path, name: String) extends Path { override def toString = s"$from.$name" }
    final case class Subtype(from: Path, name: String) extends Path { override def toString = s"($from : $name)" }
  }

  /** Possible configuration options that we can work with inside a macro. Parsed from `pipez.PipeDerivationConfig` */
  sealed trait ConfigEntry extends Product with Serializable
  object ConfigEntry {

    case object EnableDiagnostics extends ConfigEntry

    final case class AddField[OutField](
      outputField:  Path,
      outFieldType: Type[OutField],
      pipe:         Expr[Pipe[In, OutField]]
    ) extends ConfigEntry {
      override def toString: String = s"AddField($outputField : ${previewType(outFieldType)}, ${previewCode(pipe)})"
    }

    final case class RenameField[InField, OutField](
      inputField:     Path,
      inputFieldType: Type[InField],
      outputField:    Path,
      outFieldType:   Type[OutField]
    ) extends ConfigEntry {
      override def toString: String =
        s"AddField($inputField : ${previewType(inputFieldType)}, $outputField : ${previewType(outFieldType)})"
    }

    final case class PlugInField[InField, OutField](
      inputField:     Path,
      inputFieldType: Type[InField],
      outputField:    Path,
      outFieldType:   Type[OutField],
      pipe:           Expr[Pipe[InField, OutField]]
    ) extends ConfigEntry {
      override def toString: String =
        s"PlugInField($inputField : ${previewType(inputFieldType)}, $outputField : ${previewType(outFieldType)}, ${previewCode(pipe)})"
    }

    case object FieldCaseInsensitive extends ConfigEntry

    final case class AddFallbackValue[FallbackValue](
      fallbackValueType: Type[FallbackValue],
      fallbackValue:     Expr[FallbackValue]
    ) extends ConfigEntry

    case object EnableFallbackToDefaults extends ConfigEntry

    final case class RemoveSubtype[InSubtype <: In](
      inputSubtype:     Path,
      inputSubtypeType: Type[InSubtype],
      pipe:             Expr[Pipe[InSubtype, Out]]
    ) extends ConfigEntry {
      override def toString: String =
        s"RemoveSubtype($inputSubtype : ${previewType(inputSubtypeType)}, ${previewCode(pipe)})"
    }

    final case class RenameSubtype[InSubtype <: In, OutSubtype <: Out](
      inputSubtype:      Path,
      inputSubtypeType:  Type[InSubtype],
      outputSubtype:     Path,
      outputSubtypeType: Type[OutSubtype]
    ) extends ConfigEntry {
      override def toString: String =
        s"RenameSubtype($inputSubtype : ${previewType(inputSubtypeType)}, $outputSubtype : ${previewType(outputSubtypeType)})"
    }

    final case class PlugInSubtype[InSubtype <: In, OutSubtype <: Out](
      inputSubtype:      Path,
      inputSubtypeType:  Type[InSubtype],
      outputSubtype:     Path,
      outputSubtypeType: Type[OutSubtype],
      pipe:              Expr[Pipe[InSubtype, OutSubtype]]
    ) extends ConfigEntry {
      override def toString: String =
        s"PlugInSubtype($inputSubtype : ${previewType(inputSubtypeType)}, $outputSubtype : ${previewType(outputSubtypeType)}, ${previewCode(pipe)})"
    }

    case object EnumCaseInsensitive extends ConfigEntry

    case object EnableRecursiveDerivation extends ConfigEntry
  }

  /** Collection of config options obtained after parsing `pipez.PipeDerivationConfig` */
  final class Settings(entries: List[ConfigEntry]) {

    import ConfigEntry.*

    lazy val isDiagnosticsEnabled: Boolean = entries.contains(EnableDiagnostics)

    lazy val isFieldCaseInsensitive: Boolean = entries.contains(FieldCaseInsensitive)

    lazy val isEnumCaseInsensitive: Boolean = entries.contains(EnumCaseInsensitive)

    lazy val providedFallbackValues: List[ConfigEntry.AddFallbackValue[?]] = entries.collect {
      case config @ ConfigEntry.AddFallbackValue(_, _) => config
    }

    lazy val isFallbackToDefaultEnabled: Boolean = entries.contains(EnableFallbackToDefaults)

    lazy val isRecursiveDerivationEnabled: Boolean = entries.contains(EnableRecursiveDerivation)

    final def resolve[A](default: A)(overrideWhen: PartialFunction[ConfigEntry, A]): A = entries.foldLeft(default) {
      (a, entry) => overrideWhen.applyOrElse[ConfigEntry, A](entry, _ => a)
    }

    final def stripSpecificsToCurrentDerivation: Settings = new Settings(
      entries.collect {
        case ConfigEntry.EnableDiagnostics        => ConfigEntry.EnableDiagnostics
        case ConfigEntry.FieldCaseInsensitive     => ConfigEntry.FieldCaseInsensitive
        case ConfigEntry.EnableFallbackToDefaults => ConfigEntry.EnableFallbackToDefaults
        case ConfigEntry.EnumCaseInsensitive      => ConfigEntry.EnumCaseInsensitive
      }
    )

    override def toString: String = s"Settings(${entries.mkString(", ")})"
  }

  /** Possible errors that can happen during derivation */
  sealed trait DerivationError extends Product with Serializable
  object DerivationError {

    case object MissingPublicConstructor extends DerivationError

    final case class MissingPublicSource(
      outFieldName: String
    ) extends DerivationError

    final case class MissingMatchingSubType(
      subtypeName: String
    ) extends DerivationError

    final case class MissingMatchingValue(
      valueName: String
    ) extends DerivationError

    final case class RequiredImplicitNotFound[I, O](
      inFieldType:  Type[I],
      outFieldType: Type[O]
    ) extends DerivationError

    final case class RecursiveDerivationFailed[I, O](
      inType:  Type[I],
      outType: Type[O],
      errors:  List[DerivationError]
    ) extends DerivationError

    final case class NotSupportedFieldConversion[I, O](
      inField:      String,
      inFieldType:  Type[I],
      outField:     String,
      outFieldType: Type[O]
    ) extends DerivationError

    final case class NotSupportedEnumConversion(
      isInSumType:  Boolean,
      isOutSumType: Boolean
    ) extends DerivationError

    final case class InvalidConfiguration(msg: String) extends DerivationError

    final case class InvalidInput(msg: String) extends DerivationError

    case object NotYetSupported extends DerivationError

    final case class NotYetImplemented(msg: String) extends DerivationError
  }

  final class LazyMessage(msg: => String) {
    final def value: String = msg
  }

  /** Helper which allows: use of for-comprehension, parallel error composition, logging */
  sealed trait DerivationResult[+A] extends Product with Serializable {

    import DerivationResult.*

    final def flatMap[B](f: A => DerivationResult[B]): DerivationResult[B] = this match {
      case Success(value, diagnostic) =>
        f(value) match {
          case Success(value, diagnostic2)  => Success(value, diagnostic ++ diagnostic2)
          case Failure(errors, diagnostic2) => Failure(errors, diagnostic ++ diagnostic2)
        }
      case Failure(errors, diagnostic) => Failure(errors, diagnostic)
    }
    final def map[B](f: A => B): DerivationResult[B] = flatMap(f andThen pure)

    final def map2[B, C](other: DerivationResult[B])(f: (A, B) => C): DerivationResult[C] = (this, other) match {
      case (Success(a, d1), Success(b, d2))   => Success(f(a, b), d1 ++ d2)
      case (Failure(e1, d1), Failure(e2, d2)) => Failure(e1 ++ e2, d1 ++ d2)
      case (Failure(e, d1), Success(_, d2))   => Failure(e, d1 ++ d2)
      case (Success(_, d1), Failure(e, d2))   => Failure(e, d1 ++ d2)
    }

    final def zip[B](other: DerivationResult[B]): DerivationResult[(A, B)] = map2(other)(_ -> _)

    def orElse[A1 >: A](result: => DerivationResult[A1]): DerivationResult[A1] = this match {
      case success @ Success(_, _) => success
      case Failure(errors1, diagnostic1) =>
        result match {
          case Success(value, diagnostic2)   => Success(value, diagnostic2)
          case Failure(errors2, diagnostic2) => Failure(errors1 ++ errors2, diagnostic1 ++ diagnostic2)
        }
    }

    final def fold[B](success: A => B)(failure: List[DerivationError] => B): B = this match {
      case Success(value, _)  => success(value)
      case Failure(errors, _) => failure(errors)
    }

    def diagnostic: Diagnostic
    final def log(message: => String): DerivationResult[A] = this match {
      case Success(value, diagnostic)  => Success(value, diagnostic :+ new LazyMessage(message))
      case Failure(errors, diagnostic) => Failure(errors, diagnostic :+ new LazyMessage(message))
    }
    final def logResult(success: A => String)(failure: List[DerivationError] => String): DerivationResult[A] =
      this match {
        case Success(value, diagnostic)  => Success(value, diagnostic :+ new LazyMessage(success(value)))
        case Failure(errors, diagnostic) => Failure(errors, diagnostic :+ new LazyMessage(failure(errors)))
      }
    final def logSuccess(success: A => String): DerivationResult[A] = this match {
      case DerivationResult.Success(value, diagnostics) => Success(value, diagnostic :+ new LazyMessage(success(value)))
      case _                                            => this
    }
    final def logFailure(failure: List[DerivationError] => String): DerivationResult[A] = this match {
      case Failure(errors, diagnostic) => Failure(errors, diagnostic :+ new LazyMessage(failure(errors)))
      case _                           => this
    }
  }
  object DerivationResult {

    final private case class Success[+A](
      value:      A,
      diagnostic: Diagnostic
    ) extends DerivationResult[A]

    final private case class Failure(
      errors:     List[DerivationError],
      diagnostic: Diagnostic
    ) extends DerivationResult[Nothing]

    type Diagnostic = Vector[LazyMessage]

    def pure[A](value: A):                           DerivationResult[A]       = Success(value, Vector.empty)
    def fail(error: DerivationError):                DerivationResult[Nothing] = Failure(List(error), Vector.empty)
    def failMultiple(errors: List[DerivationError]): DerivationResult[Nothing] = Failure(errors, Vector.empty)

    def unsafe[A](thunk: => A)(error: Throwable => DerivationError): DerivationResult[A] =
      try
        pure(thunk)
      catch {
        case e: Throwable => fail(error(e))
      }

    def sequence[A, Coll[A0] <: Seq[A0]](
      seq: Coll[DerivationResult[A]]
    )(implicit
      factory: Factory[A, Coll[A]]
    ): DerivationResult[Coll[A]] =
      seq.foldLeft(pure(factory.newBuilder))((builder, next) => builder.map2(next)(_.addOne(_))).map(_.result())

    def fromOption[A](opt: Option[A])(err: => DerivationError): DerivationResult[A] =
      opt.fold[DerivationResult[A]](fail(err))(pure)
    def fromEither[E, A](either: Either[E, A])(err: E => DerivationError): DerivationResult[A] =
      either.fold(e => fail(err(e)), pure)
  }

  /** Allows displaying the generated code in platform-independent way */
  def previewCode[A](code: Expr[A]): String

  /** Allows summoning the type class in platform-independent way */
  def summonPipe[Input: Type, Output: Type]: DerivationResult[Expr[Pipe[Input, Output]]]

  /** Allows recursively deriving the type class in platform-independent way */
  def derivePipe[Input: Type, Output: Type](settings: Settings): DerivationResult[Expr[Pipe[Input, Output]]]

  /** Attempts to summon the type class and on failure fallbacks to deriving it */
  final def summonOrDerive[Input: Type, Output: Type](
    settings:              Settings,
    alwaysAllowDerivation: Boolean
  ): DerivationResult[Expr[Pipe[Input, Output]]] =
    summonPipe[Input, Output].orElse {
      if (alwaysAllowDerivation || settings.isRecursiveDerivationEnabled) derivePipe[Input, Output](settings)
      else
        DerivationResult.fail(
          DerivationError.RecursiveDerivationFailed(
            typeOf[Input],
            typeOf[Output],
            List(
              DerivationError.InvalidInput("Recursive derivation was not enabled")
            )
          )
        )
    }

  /** If we pass Single Abstract Method as argument, after expansion inference sometimes fails, compiler might need a
    * hint
    */
  def singleAbstractMethodExpansion[SAM: Type](code: Expr[SAM]): Expr[SAM]

  /** Turns the code defining `PipeDerivationConfig[Pipe, In, Out]` into `Settings`.
    *
    * Requires that config is created as one chain while passing the parameter.
    */
  def readConfig(code: Expr[PipeDerivationConfig[Pipe, In, Out]]): DerivationResult[Settings]

  /** Reads configs if passed, or fallback to defaults (empty `Settings`) otherwise */
  final def readSettingsIfGiven(code: Option[Expr[PipeDerivationConfig[Pipe, In, Out]]]): DerivationResult[Settings] =
    code
      .fold(DerivationResult.pure(new Settings(Nil)))(readConfig)
      .log(if (code.isDefined) "Derivation started with configuration" else "Derivation started without configuration")
      .log(s"Pipeline from ${previewType[In]} to ${previewType[Out]}")
      .log(s"PipeDerivation used: $previewPipeDerivation")
      .logSuccess(config => s"Configuration used: $config")
}
object Definitions {

  /** Can be used instead of pipeDerivation.Context to avoid path-dependent types */
  type Context

  /** Can be used instead of pipeDerivation.Result[O] to avoid path-dependent types */
  type Result[+O]
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy