io.github.arainko.ducktape.internal.PlanConfigurer.scala Maven / Gradle / Ivy
The newest version!
package io.github.arainko.ducktape.internal
import io.github.arainko.ducktape.internal.Configuration.Instruction
import io.github.arainko.ducktape.internal.Path.Segment
import scala.quoted.*
private[ducktape] object PlanConfigurer {
import Plan.*
def run[F <: Fallible](
plan: Plan[Erroneous, F],
configs: List[Configuration.Instruction[F]]
)(using Quotes, Context): Plan.Reconfigured[F] = {
def configureSingle(
plan: Plan[Erroneous, F],
config: Configuration.Instruction[F]
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]): Plan[Erroneous, F] = {
def recurse[F <: Fallible](
current: Plan[Erroneous, F],
segments: List[Path.Segment],
parent: Plan[Erroneous, Fallible] | None.type,
config: Configuration.Instruction[F]
)(using Quotes): Plan[Erroneous, F] = {
def traverseBetweenNotFallible(
parent: BetweenFallibleNonFallible[Erroneous],
plan: Plan[Erroneous, Nothing],
tail: List[Path.Segment]
) =
ConfigInstructionRefiner.run(config) match
case None =>
Plan.Error.from(parent, ErrorMessage.FallibleConfigNotPermitted(config.span, config.side), None)
case nonFallible: Configuration.Instruction[Nothing] =>
parent.copy(plan = recurse(plan, tail, parent, nonFallible))
def handleElement(
segment: Path.Segment.Element,
tail: List[Segment],
current: Plan[Erroneous, F]
): Plan[Erroneous, F] =
current match {
case parent @ BetweenCollections(_, _, _, plan) =>
parent.copy(plan = recurse(plan, tail, parent, config))
case parent @ BetweenOptions(_, _, plan) =>
parent.copy(plan = recurse(plan, tail, parent, config))
case parent @ BetweenNonOptionOption(_, _, plan) =>
parent.copy(plan = recurse(plan, tail, parent, config))
case parent @ BetweenFallibleNonFallible(source, dest, plan) if config.side.isSource =>
traverseBetweenNotFallible(parent, plan, tail)
case parent @ BetweenFallibles(source, dest, mode, plan) if config.side.isSource =>
parent.copy(plan = recurse(plan, tail, parent, config))
case paren: Upcast =>
recurse(paren.alt, segments, parent, config)
case other => invalidPathSegment(config, other, segment)
}
def handleField(segment: Path.Segment.Field, tail: List[Segment], current: Plan[Erroneous, F]): Plan[Erroneous, F] =
current match {
// passthrough BetweenFallibles, the dest is just a normal field in this case
case parent @ BetweenFallibleNonFallible(source, dest, plan) if config.side.isDest =>
traverseBetweenNotFallible(parent, plan, segments)
// passthrough BetweenFallibles, the dest is just a normal field in this case
case parent @ BetweenFallibles(source, dest, mode, plan) if config.side.isDest =>
parent.copy(plan = recurse(plan, segments, parent, config))
case parent @ BetweenProducts(sourceTpe, destTpe, fieldPlans) =>
val fieldPlan =
fieldPlans
.get(segment.name)
.map(fieldPlan => recurse(fieldPlan, tail, parent, config))
.getOrElse(Plan.Error.from(parent, ErrorMessage.InvalidFieldAccessor(segment.name, config.span), None))
parent.copy(fieldPlans = fieldPlans.updated(segment.name, fieldPlan))
case parent @ BetweenTupleProduct(source, dest, plans) if config.side.isDest =>
plans
.get(segment.name)
.map(fieldPlan => parent.copy(plans = plans.updated(segment.name, recurse(fieldPlan, tail, parent, config))))
.getOrElse(Plan.Error.from(parent, ErrorMessage.InvalidFieldAccessor(segment.name, config.span), None))
case parent @ BetweenProductTuple(source, dest, plans) if config.side.isSource =>
val sourceFields = source.fields.keys
// basically, find the index of `fieldName`
plans.zipWithIndex.collectFirst {
case (fieldPlan, index @ sourceFields(segment.name)) =>
parent.copy(plans = plans.updated(index, recurse(fieldPlan, tail, parent, config)))
}.getOrElse(Plan.Error.from(parent, ErrorMessage.InvalidFieldAccessor(segment.name, config.span), None))
case parent @ BetweenProductFunction(sourceTpe, destTpe, argPlans) =>
val argPlan =
argPlans
.get(segment.name)
.map(argPlan => recurse(argPlan, tail, parent, config))
.getOrElse(Plan.Error.from(parent, ErrorMessage.InvalidArgAccessor(segment.name, config.span), None))
parent.copy(argPlans = argPlans.updated(segment.name, argPlan))
case parent @ BetweenTupleFunction(source, dest, argPlans) if config.side.isDest =>
argPlans
.get(segment.name)
.map(argPlan => parent.copy(argPlans = argPlans.updated(segment.name, recurse(argPlan, tail, parent, config))))
.getOrElse(Plan.Error.from(parent, ErrorMessage.InvalidArgAccessor(segment.name, config.span), None))
case paren: Upcast =>
recurse(paren.alt, segments, parent, config)
case other => invalidPathSegment(config, other, segment)
}
def handleTupleElement(
segment: Path.Segment.TupleElement,
tail: List[Segment],
currnet: Plan[Erroneous, F]
): Plan[Erroneous, F] = {
val index = segment.index
Logger.debug(s"Matched tupleElement with index of $index")
current match {
// passthrough BetweenFallibles, the dest is just a tuple elem in this case
case parent @ BetweenFallibleNonFallible(source, dest, plan) if config.side.isDest =>
traverseBetweenNotFallible(parent, plan, segments)
// passthrough BetweenFallibles, the dest is just a tuple elem in this case
case parent @ BetweenFallibles(source, dest, mode, plan) if config.side.isDest =>
parent.copy(plan = recurse(plan, segments, parent, config))
case parent @ BetweenTuples(source, dest, plans) =>
Logger.debug(ds"Matched $parent")
plans
.lift(index)
.map(elemPlan => parent.copy(plans = plans.updated(index, recurse(elemPlan, tail, plan, config))))
.getOrElse(
Plan.Error
.from(plan, ErrorMessage.InvalidTupleAccesor(index, config.span), None)
)
case parent @ BetweenProductTuple(source, dest, plans) if config.side.isDest =>
Logger.debug(ds"Matched $parent")
plans
.lift(index)
.map(elemPlan => parent.copy(plans = plans.updated(index, recurse(elemPlan, tail, parent, config))))
.getOrElse(
Plan.Error
.from(parent, ErrorMessage.InvalidTupleAccesor(index, config.span), None)
)
case parent @ BetweenTupleProduct(source, dest, plans) if config.side.isSource =>
Logger.debug(ds"Matched $parent")
plans.toVector
.lift(index)
.map((name, fieldPlan) => parent.copy(plans = plans.updated(name, recurse(fieldPlan, tail, parent, config))))
.getOrElse(
Plan.Error
.from(parent, ErrorMessage.InvalidTupleAccesor(index, config.span), None)
)
case parent @ BetweenTupleFunction(source, dest, plans) if config.side.isSource =>
Logger.debug(ds"Matched $parent")
plans.toVector
.lift(index)
.map((name, fieldPlan) => parent.copy(argPlans = plans.updated(name, recurse(fieldPlan, tail, parent, config))))
.getOrElse(
Plan.Error
.from(parent, ErrorMessage.InvalidTupleAccesor(index, config.span), None)
)
case paren: Upcast =>
recurse(paren.alt, segments, parent, config)
case other =>
Logger.debug(s"Failing with invalid path segment on node: ${other.getClass.getSimpleName}")
invalidPathSegment(config, other, segment)
}
}
def handleCase(segment: Path.Segment.Case, tail: List[Segment], current: Plan[Erroneous, F]): Plan[Erroneous, F] = {
val tpe = segment.tpe
current match {
// BetweenNonOptionOption keeps the same type as its source so we passthrough it when traversing source nodes
case parent: BetweenNonOptionOption[Erroneous, F] if config.side.isSource =>
parent.copy(plan = recurse(parent.plan, segments, parent, config))
case parent @ BetweenCoproducts(sourceTpe, destTpe, casePlans) =>
def sideTpe(plan: Plan[Erroneous, Fallible]) =
if config.side.isSource then plan.source.tpe.repr else plan.dest.tpe.repr
casePlans.zipWithIndex
.find((plan, _) => tpe.repr =:= sideTpe(plan))
.map((casePlan, idx) => parent.copy(casePlans = casePlans.updated(idx, recurse(casePlan, tail, parent, config))))
.getOrElse(Plan.Error.from(parent, ErrorMessage.InvalidCaseAccessor(tpe, config.span), None))
case paren: Upcast =>
recurse(paren.alt, segments, parent, config)
case other => invalidPathSegment(config, other, segment)
}
}
segments match {
case (segment @ Path.Segment.Field(_, _)) :: tail =>
handleField(segment, tail, current)
case (segment @ Path.Segment.TupleElement(_, _)) :: tail =>
handleTupleElement(segment, tail, current)
case (segment @ Path.Segment.Case(_)) :: tail =>
handleCase(segment, tail, current)
case (segment @ Path.Segment.Element(_)) :: tail =>
handleElement(segment, tail, current)
case Nil =>
configurePlan(config, current, parent)
}
}
// check if a Case config ends with a `.at` segment, otherwise weird things happen
if config.side.isSource && config.path.segments.lastOption.exists(!_.isInstanceOf[Path.Segment.Case]) then
Plan.Error.from(plan, ErrorMessage.SourceConfigDoesntEndWithCaseSegment(config.span), None)
else recurse(plan, config.path.segments.toList, None, config)
}
val (errors, successes, warnings, reconfiguredPlan) =
Accumulator.use[Plan.Error]:
Accumulator.use[(Path, Side)]:
Accumulator.use[ConfigWarning]:
configs.foldLeft(plan)(configureSingle) *: EmptyTuple
Plan.Reconfigured(errors, successes, warnings, reconfiguredPlan)
}
private def configurePlan[F <: Fallible](
config: Configuration.Instruction[F],
current: Plan[Erroneous, F],
parent: Plan[Erroneous, Fallible] | None.type
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning], Context): Plan[Erroneous, F] = {
Logger.debug(ds"Configuring plan $current with $config")
config match {
case cfg: (Configuration.Instruction.Static[F] | Configuration.Instruction.Dynamic) =>
staticOrDynamic(cfg, current, parent)
case instruction: Configuration.Instruction.Bulk =>
bulk(current, instruction)
case cfg @ Configuration.Instruction.Regional(path, side, modifier, span) =>
regional(current, cfg, parent)
case cfg @ Configuration.Instruction.Failed(path, side, message, span) =>
Accumulator.append {
Plan.Error.from(current, ErrorMessage.ConfigurationFailed(cfg), None)
}
}
}
private def invalidPathSegment(
config: Configuration.Instruction[Fallible],
plan: Plan[Erroneous, Fallible],
segment: Path.Segment
): Plan.Error =
plan match {
case suppressed: Plan.Error =>
Plan.Error.from(plan, ErrorMessage.InvalidPathSegment(segment, config.side, config.span), Some(suppressed))
case other =>
Plan.Error.from(plan, ErrorMessage.InvalidPathSegment(segment, config.side, config.span), None)
}
private def staticOrDynamic[F <: Fallible](
instruction: Configuration.Instruction.Static[F] | Configuration.Instruction.Dynamic,
current: Plan[Erroneous, F],
parent: Plan[Erroneous, Fallible] | None.type
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning], Context) = {
instruction match {
case static: Configuration.Instruction.Static[F] =>
current.configureIfValid(static, static.config)
case dynamic: Configuration.Instruction.Dynamic =>
dynamic.config(parent) match {
case Right(config) => current.configureIfValid(dynamic, config)
case Left(errorMessage) =>
val failed = Configuration.Instruction.Failed.from(dynamic, errorMessage)
Accumulator.append {
Plan.Error.from(current, ErrorMessage.ConfigurationFailed(failed), None)
}
}
}
}
private def regional[F <: Fallible](
plan: Plan[Erroneous, F],
modifier: Configuration.Instruction.Regional,
parent: Plan[Erroneous, Fallible] | None.type
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning], Context): Plan[Erroneous, F] =
plan match {
case plan: Upcast => plan
case plan: UserDefined[F] => plan
case plan: Derived[F] => plan
case plan: Configured[F] => plan
case plan: BetweenProductFunction[Erroneous, F] =>
plan.copy(argPlans = plan.argPlans.transform((_, argPlan) => regional(argPlan, modifier, plan)))
case plan: BetweenTupleFunction[Erroneous, F] =>
plan.copy(argPlans = plan.argPlans.transform((_, argPlan) => regional(argPlan, modifier, plan)))
case plan: BetweenUnwrappedWrapped => plan
case plan: BetweenWrappedUnwrapped => plan
case plan: BetweenSingletons => plan
case plan: BetweenProducts[Erroneous, F] =>
plan.copy(fieldPlans = plan.fieldPlans.transform((_, fieldPlan) => regional(fieldPlan, modifier, plan)))
case plan: BetweenProductTuple[Erroneous, F] =>
plan.copy(plans = plan.plans.map(fieldPlan => regional(fieldPlan, modifier, plan)))
case plan: BetweenTupleProduct[Erroneous, F] =>
plan.copy(plans = plan.plans.transform((_, fieldPlan) => regional(fieldPlan, modifier, plan)))
case plan: BetweenTuples[Erroneous, F] =>
plan.copy(plans = plan.plans.map(fieldPlan => regional(fieldPlan, modifier, plan)))
case plan: BetweenCoproducts[Erroneous, F] =>
plan.copy(casePlans = plan.casePlans.map(regional(_, modifier, plan)))
case plan: BetweenOptions[Erroneous, F] =>
plan.copy(plan = regional(plan.plan, modifier, plan))
case plan: BetweenNonOptionOption[Erroneous, F] =>
plan.copy(plan = regional(plan.plan, modifier, plan))
case plan: BetweenCollections[Erroneous, F] =>
plan.copy(plan = regional(plan.plan, modifier, plan))
case plan: BetweenFallibleNonFallible[Erroneous] =>
plan.copy(plan = regional(plan.plan, modifier, plan))
case plan @ BetweenFallibles(_, _, _, elemPlan) =>
plan.copy(plan = regional(elemPlan, modifier, plan))
case plan: Error =>
// TODO: Detect when a regional config doesn't do anything and emit an error
modifier.modifier(parent, plan) match {
case config: Configuration[F] => plan.configureIfValid(modifier, config)
case other: plan.type => other
}
}
// TODO: Support tuple-to-tuple, product-to-tuple?
private def bulk[F <: Fallible](
current: Plan[Erroneous, F],
instruction: Configuration.Instruction.Bulk
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning], Context): Plan[Erroneous, F] = {
enum IsAnythingModified {
case Yes, No
}
var isAnythingModified = IsAnythingModified.No
def updatePlan(
parent: Plan.BetweenProducts[Erroneous, F] | Plan.BetweenProductFunction[Erroneous, F] |
Plan.BetweenTupleProduct[Erroneous, F]
)(
name: String,
plan: Plan[Erroneous, F]
)(using Quotes) = {
instruction.modifier(parent, name, plan) match {
case config: Configuration[Nothing] =>
isAnythingModified = IsAnythingModified.Yes
plan.configureIfValid(instruction, config)
case p: plan.type => p
}
}
PartialFunction
.condOpt(current) {
case func: Plan.BetweenProductFunction[Erroneous, F] =>
val updatedArgPlans = func.argPlans.transform(updatePlan(func))
func.copy(argPlans = updatedArgPlans)
case prod: Plan.BetweenProducts[Erroneous, F] =>
val updatedFieldPlans = prod.fieldPlans.transform(updatePlan(prod))
prod.copy(fieldPlans = updatedFieldPlans)
case prodTuple: Plan.BetweenTupleProduct[Erroneous, F] =>
val updatedFieldPlans = prodTuple.plans.transform(updatePlan(prodTuple))
prodTuple.copy(plans = updatedFieldPlans)
}
.toRight(
"This config only works when applied to name-wise based product transformations (product-to-product, tuple-to-product, product-via-function)"
)
.filterOrElse(_ => isAnythingModified == IsAnythingModified.Yes, "Config option is not doing anything")
.fold(
errorMessage => {
val failed = Configuration.Instruction.Failed.from(instruction, errorMessage)
Accumulator.append {
Plan.Error.from(current, ErrorMessage.ConfigurationFailed(failed), None)
}
},
identity
)
}
extension [F <: Fallible](currentPlan: Plan[Erroneous, F]) {
private def configureIfValid(
instruction: Configuration.Instruction[F],
config: Configuration[F]
)(using
quotes: Quotes,
errors: Accumulator[Plan.Error],
successes: Accumulator[(Path, Side)],
warnings: Accumulator[ConfigWarning],
context: Context
) = {
def isReplaceableBy(update: Configuration[F])(using Quotes) =
update.tpe.repr <:< currentPlan.destPath.currentTpe.repr
if isReplaceableBy(config) then
val (path, _) =
Accumulator.append {
if instruction.side == Side.Dest then currentPlan.destPath -> instruction.side
else currentPlan.sourcePath -> instruction.side
}
Accumulator.appendAll {
ConfiguredCollector
.run(currentPlan, Nil)
.map(plan => ConfigWarning(plan.span, instruction.span, path))
}
Plan.Configured.from(currentPlan, config, instruction)
else
Accumulator.append {
Plan.Error.from(
currentPlan,
ErrorMessage.InvalidConfiguration(
config.tpe,
currentPlan.destPath.currentTpe,
instruction.side,
instruction.span
),
None
)
}
}
}
private object ConfiguredCollector extends PlanTraverser[List[Plan.Configured[Fallible]]] {
protected def foldOver(
plan: Plan[Erroneous, Fallible],
accumulator: List[Plan.Configured[Fallible]]
): List[Plan.Configured[Fallible]] =
plan match {
case configured: Plan.Configured[Fallible] => configured :: accumulator
case other => accumulator
}
}
}