Skip to content

Commit

Permalink
#1968: locator-private bindings (#2162)
Browse files Browse the repository at this point in the history
* #1968: private bindings PoC

* #1968: private bindings PoC

* #1968: locator-private bindings PoC

* #1968: public roots mode

* #1968: public roots mode for bootstrap injector

* docs

* bootstrap planning mode customization

* docs

* locator pretty-print

* locator pretty-print

* better formatting

* better formatting
  • Loading branch information
pshirshov authored Aug 13, 2024
1 parent 30ca54a commit f8d21be
Show file tree
Hide file tree
Showing 53 changed files with 1,136 additions and 1,037 deletions.
1,236 changes: 527 additions & 709 deletions build.sbt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@ trait AbstractLocator extends Locator {
}

override final def lookupRef[T: Tag](key: DIKey): Option[GenericTypedRef[T]] = {
recursiveLookup(key, this)
recursiveLookup(key, this, this)
}

private final def recursiveLookup[T: Tag](key: DIKey, locator: Locator): Option[GenericTypedRef[T]] = {
locator
.lookupLocal[T](key)
.orElse(locator.parent.flatMap(p => recursiveLookup[T](key, p)))
private final def recursiveLookup[T: Tag](key: DIKey, locator: Locator, origin: Locator): Option[GenericTypedRef[T]] = {
locator.lookupLocal[T](key) match {
case Some(_) if (locator ne origin) && locator.isPrivate(key) =>
None
case a @ Some(_) => a
case None =>
locator.parent.flatMap(p => recursiveLookup[T](key, p, origin))
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import izumi.distage.AbstractLocator
import izumi.distage.model.Locator.LocatorMeta
import izumi.distage.model.definition.Identifier
import izumi.distage.model.plan.Plan
import izumi.distage.model.plan.repr.LocatorFormatter
import izumi.distage.model.providers.Functoid
import izumi.distage.model.provisioning.OpStatus
import izumi.distage.model.provisioning.PlanInterpreter.Finalizer
import izumi.distage.model.references.IdentifiedRef
import izumi.distage.model.reflection.{DIKey, GenericTypedRef}
import izumi.functional.Renderable
import izumi.functional.lifecycle.Lifecycle
import izumi.functional.quasi.QuasiPrimitives
import izumi.reflect.{Tag, TagK}
Expand Down Expand Up @@ -41,6 +43,8 @@ trait Locator {
def lookupRefOrThrow[T: Tag](key: DIKey): GenericTypedRef[T]
def lookupRef[T: Tag](key: DIKey): Option[GenericTypedRef[T]]

def isPrivate(key: DIKey): Boolean

/** The plan that produced this object graph */
def plan: Plan
def parent: Option[Locator]
Expand Down Expand Up @@ -106,6 +110,22 @@ trait Locator {
}
args.map(fn.unsafeApply(_).asInstanceOf[T])
}

final def depth: Int = {
var d = -1
var loc: Option[Locator] = Some(this)
while (loc.nonEmpty) {
d = d + 1
loc = loc.get.parent
}
d
}

def render()(implicit ev: Renderable[Locator]): String = ev.render(this)

override def toString: String = {
this.render()
}
}

object Locator {
Expand All @@ -114,15 +134,17 @@ object Locator {
resource.use(_.run(function))
}

@inline implicit final def defaultFormatter: Renderable[Locator] = LocatorFormatter

val empty: AbstractLocator = new AbstractLocator {
override protected def lookupLocalUnsafe(key: DIKey): Option[Any] = None
override def instances: immutable.Seq[IdentifiedRef] = Nil
override def plan: Plan = Plan.empty
override def parent: Option[Locator] = None
override def finalizers[F[_]: TagK]: Seq[Finalizer[F]] = Nil
override def index: Map[DIKey, Any] = Map.empty

override def meta: LocatorMeta = LocatorMeta.empty
override def isPrivate(key: DIKey): Boolean = false
}

/** @param timings How long it took to instantiate each component */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package izumi.distage.model

import izumi.distage.model.definition.errors.DIError
import izumi.distage.model.definition.{Activation, ModuleBase}
import izumi.distage.model.definition.{Activation, LocatorPrivacy, ModuleBase}
import izumi.distage.model.plan.*
import izumi.fundamentals.collections.nonempty.NEList

/** Transforms [[izumi.distage.model.definition.ModuleDef]] into [[izumi.distage.model.plan.Plan]] */
trait Planner {
def plan(input: PlannerInput): Either[NEList[DIError], Plan]

@inline final def plan(bindings: ModuleBase, activation: Activation, roots: Roots): Either[NEList[DIError], Plan] = {
plan(PlannerInput(bindings, activation, roots))
}

@inline final def plan(bindings: ModuleBase, roots: Roots): Either[NEList[DIError], Plan] = {
plan(bindings, Activation.empty, roots)
@inline final def plan(
bindings: ModuleBase,
roots: Roots,
activation: Activation = Activation.empty,
locatorPrivacy: LocatorPrivacy = LocatorPrivacy.PublicByDefault,
): Either[NEList[DIError], Plan] = {
plan(PlannerInput(bindings, roots, activation, locatorPrivacy))
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package izumi.distage.model

import izumi.distage.model.definition.{Activation, ModuleBase}
import izumi.distage.model.definition.{Activation, LocatorPrivacy, ModuleBase}
import izumi.distage.model.plan.Roots
import izumi.distage.model.reflection.DIKey
import izumi.fundamentals.collections.nonempty.NESet
Expand All @@ -21,48 +21,64 @@ import izumi.reflect.Tag
* On [[izumi.distage.model.plan.Roots.Everything]] garbage collection will not be performed – that would be equivalent to
* designating _all_ DIKeys as roots.
*/
final case class PlannerInput(
bindings: ModuleBase,
activation: Activation,
roots: Roots,
)
final case class PlannerInput(bindings: ModuleBase, roots: Roots, activation: Activation, locatorPrivacy: LocatorPrivacy)

object PlannerInput {
/**
* Instantiate `roots` and the dependencies of `roots`, discarding bindings that are unrelated.
private val DefaultPrivacy = LocatorPrivacy.PublicByDefault

implicit class PlannerInputSyntax(private val input: PlannerInput) extends AnyVal {
def withLocatorPrivacy(privacy: LocatorPrivacy): PlannerInput = input.copy(locatorPrivacy = privacy)
def privateByDefault: PlannerInput = withLocatorPrivacy(LocatorPrivacy.PrivateByDefault)
def publicByDefault: PlannerInput = withLocatorPrivacy(LocatorPrivacy.PublicByDefault)

def withActivation(activation: Activation): PlannerInput = input.copy(activation = activation)
def emptyActivation: PlannerInput = withActivation(Activation.empty)
}

def apply(bindings: ModuleBase, roots: Roots, activation: Activation = Activation.empty, locatorPrivacy: LocatorPrivacy = DefaultPrivacy): PlannerInput =
new PlannerInput(bindings, roots, activation, locatorPrivacy)

/** Instantiate `T` and the dependencies of `T`, discarding bindings that are unrelated.
*
* Effectively, this selects and creates a *sub-graph* of the largest possible object graph that can be described by `bindings`
*/
def apply(bindings: ModuleBase, activation: Activation, roots: NESet[? <: DIKey]): PlannerInput = PlannerInput(bindings, activation, Roots(roots))
def target[T: Tag](bindings: ModuleBase): PlannerInput =
PlannerInput(bindings, Roots.target[T], Activation.empty, DefaultPrivacy)

/**
* Instantiate `roots` and the dependencies of `roots`, discarding bindings that are unrelated.
/** Instantiate `T @Id(name)` and the dependencies of `T @Id(name)`, discarding bindings that are unrelated.
*
* Effectively, this selects and creates a *sub-graph* of the largest possible object graph that can be described by `bindings`
*
* @see [[izumi.distage.model.definition.Id @Id annotation]]
*/
def apply(bindings: ModuleBase, activation: Activation, roots: Set[? <: DIKey])(implicit d: DummyImplicit): PlannerInput =
PlannerInput(bindings, activation, Roots(roots))
def target[T: Tag](name: String)(bindings: ModuleBase): PlannerInput =
PlannerInput(bindings, Roots.target[T](name), Activation.empty, DefaultPrivacy)

/** Instantiate `root`, `roots` and their dependencies, discarding bindings that are unrelated.
/** Disable all dependency pruning. Every binding in `bindings` will be instantiated, without selection of the root components. There's almost always a better way to model things though. */
def everything(bindings: ModuleBase, activation: Activation = Activation.empty, locatorPrivacy: LocatorPrivacy = DefaultPrivacy): PlannerInput =
PlannerInput(bindings, Roots.Everything, activation, locatorPrivacy)

/**
* Instantiate `roots` and the dependencies of `roots`, discarding bindings that are unrelated.
*
* Effectively, this selects and creates a *sub-graph* of the largest possible object graph that can be described by `bindings`
*/
def apply(bindings: ModuleBase, activation: Activation, root: DIKey, roots: DIKey*): PlannerInput = PlannerInput(bindings, activation, Roots(root, roots*))
def apply(bindings: ModuleBase, roots: NESet[? <: DIKey], activation: Activation): PlannerInput =
PlannerInput(bindings, Roots(roots), activation, DefaultPrivacy)

/** Instantiate `T` and the dependencies of `T`, discarding bindings that are unrelated.
/**
* Instantiate `roots` and the dependencies of `roots`, discarding bindings that are unrelated.
*
* Effectively, this selects and creates a *sub-graph* of the largest possible object graph that can be described by `bindings`
*/
def target[T: Tag](bindings: ModuleBase, activation: Activation): PlannerInput = PlannerInput(bindings, activation, DIKey.get[T])
def apply(bindings: ModuleBase, roots: Set[? <: DIKey], activation: Activation)(implicit d: DummyImplicit): PlannerInput =
PlannerInput(bindings, Roots(roots), activation, DefaultPrivacy)

/** Instantiate `T @Id(name)` and the dependencies of `T @Id(name)`, discarding bindings that are unrelated.
/** Instantiate `root`, `roots` and their dependencies, discarding bindings that are unrelated.
*
* Effectively, this selects and creates a *sub-graph* of the largest possible object graph that can be described by `bindings`
*
* @see [[izumi.distage.model.definition.Id @Id annotation]]
*/
def target[T: Tag](name: String)(bindings: ModuleBase, activation: Activation): PlannerInput = PlannerInput(bindings, activation, DIKey.get[T].named(name))
def apply(bindings: ModuleBase, activation: Activation, root: DIKey, roots: DIKey*): PlannerInput =
PlannerInput(bindings, Roots(root, roots*), activation, DefaultPrivacy)

/** Disable all dependency pruning. Every binding in `bindings` will be instantiated, without selection of the root components. There's almost always a better way to model things though. */
def everything(bindings: ModuleBase, activation: Activation = Activation.empty): PlannerInput = PlannerInput(bindings, activation, Roots.Everything)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ object BindingTag {
implicit def apply(tag: AxisChoice): BindingTag = AxisTag(tag)

final case class AxisTag(choice: AxisChoice) extends BindingTag

final case object Confined extends BindingTag
final case object Exposed extends BindingTag
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ trait LocatorDef extends AbstractLocator with AbstractBindingDefDSL[LocatorDef.B
override private[definition] final def _bindDSLAfterFrom[T](ref: SingletonRef): LocatorDef.BindDSLUnnamedAfterFrom[T] = new LocatorDef.BindDSLUnnamedAfterFrom(ref)
override private[definition] final def _setDSL[T](ref: SetRef): LocatorDef.SetDSL[T] = new LocatorDef.SetDSL[T](ref)

protected def initialState: mutable.ArrayBuffer[BindingRef] = mutable.ArrayBuffer.empty
// protected def initialState: mutable.ArrayBuffer[BindingRef] = mutable.ArrayBuffer.empty

override protected def lookupLocalUnsafe(key: DIKey): Option[Any] = {
frozenMap.get(key)
Expand All @@ -44,6 +44,8 @@ trait LocatorDef extends AbstractLocator with AbstractBindingDefDSL[LocatorDef.B
override def instances: immutable.Seq[IdentifiedRef] = frozenInstances
override def index: Map[DIKey, Any] = frozenMap

override def isPrivate(key: DIKey): Boolean = confined.contains(key)

/** The plan that produced this object graph */
override def plan: Plan = {
val ops = frozenInstances.map {
Expand All @@ -55,13 +57,17 @@ trait LocatorDef extends AbstractLocator with AbstractBindingDefDSL[LocatorDef.B

val s = IncidenceMatrix(ops.map(op => (op._1.target, Set.empty[DIKey])).toMap)
val nodes = ops.map(op => (op._1.target, op._1))
Plan(DG(s, s.transposed, GraphMeta(nodes.toMap)), PlannerInput(Module.make(ops.map(_._2).toSet), Activation.empty, Roots.Everything))
Plan(
DG(s, s.transposed, GraphMeta(nodes.toMap)),
PlannerInput(Module.make(ops.map(_._2).toSet), Roots.Everything, Activation.empty, LocatorPrivacy.PublicByDefault),
)
}

override def parent: Option[Locator] = None

private final lazy val (frozenMap, frozenInstances): (Map[DIKey, Any], immutable.Seq[IdentifiedRef]) = {
private final lazy val (frozenMap, frozenInstances, confined): (Map[DIKey, Any], immutable.Seq[IdentifiedRef], Set[DIKey]) = {
val map = new mutable.LinkedHashMap[DIKey, Any]
val confined = frozenState.filter(_.tags.contains(BindingTag.Confined)).map(_.key).toSet

frozenState.foreach {
case SingletonBinding(key, InstanceImpl(_, instance), _, _, false) =>
Expand All @@ -79,7 +85,7 @@ trait LocatorDef extends AbstractLocator with AbstractBindingDefDSL[LocatorDef.B
)
}

map.toMap -> map.iterator.map { case (k, v) => IdentifiedRef(k, v) }.toList
(map.toMap, map.iterator.map { case (k, v) => IdentifiedRef(k, v) }.toList, confined)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package izumi.distage.model.definition

sealed trait LocatorPrivacy

object LocatorPrivacy {
/** All the bindings are public, unless explicitly marked as "confined"
*/
case object PublicByDefault extends LocatorPrivacy
/** All the bindings are private, unless explicitly marked as "exposed"
*/
case object PrivateByDefault extends LocatorPrivacy

/** Only planning roots are public
*/
case object PublicRoots extends LocatorPrivacy

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import izumi.distage.model.definition.*
import izumi.distage.model.definition.Binding.{EmptySetBinding, ImplBinding, SetElementBinding, SingletonBinding}
import izumi.distage.model.definition.dsl.AbstractBindingDefDSL.*
import izumi.distage.model.definition.dsl.AbstractBindingDefDSL.SetElementInstruction.ElementAddTags
import izumi.distage.model.definition.dsl.AbstractBindingDefDSL.SetInstruction.{AddTagsAll, SetIdAll}
import izumi.distage.model.definition.dsl.AbstractBindingDefDSL.SetInstruction.{AddTagOntoSet, SetIdAll}
import izumi.distage.model.definition.dsl.AbstractBindingDefDSL.SingletonInstruction.*
import izumi.distage.model.exceptions.dsl.InvalidFunctoidModifier
import izumi.distage.model.providers.Functoid
import izumi.distage.model.reflection.{DIKey, MultiSetImplId}
import izumi.distage.model.reflection.SetKeyMeta
import izumi.distage.model.reflection.{DIKey, MultiSetImplId, SetKeyMeta}
import izumi.fundamentals.platform.language.{CodePositionMaterializer, SourceFilePosition}
import izumi.reflect.Tag

import scala.collection.mutable

trait AbstractBindingDefDSL[BindDSL[_], BindDSLAfterFrom[_], SetDSL[_]] extends AbstractBindingDefDSLMacro[BindDSL] { self =>
private final val mutableState: mutable.ArrayBuffer[BindingRef] = _initialState

protected def _initialState: mutable.ArrayBuffer[BindingRef] = mutable.ArrayBuffer.empty

private[definition] def _bindDSL[T](ref: SingletonRef): BindDSL[T]
Expand Down Expand Up @@ -280,7 +280,7 @@ object AbstractBindingDefDSL {

}

final class ModifyTaggingDSL[T](private val mutableState: SingletonRef) extends AnyVal with AddDependencyDSL[T, ModifyTaggingDSL[T]] {
final class ModifyTaggingDSL[T](private val mutableState: SingletonRef) extends AnyVal with AddDependencyDSL[T, ModifyTaggingDSL[T]] with Tagging[ModifyTaggingDSL[T]] {

def tagged(tags: BindingTag*): ModifyTaggingDSL[T] = {
new ModifyTaggingDSL(mutableState.append(AddTags(tags.toSet)))
Expand Down Expand Up @@ -339,7 +339,7 @@ object AbstractBindingDefDSL {
override protected def toSame: SubcontextRef => SubcontextNamedDSL[T] = new SubcontextNamedDSL[T](_)
}

sealed abstract class SubcontextDSLBase[T, Self] {
sealed abstract class SubcontextDSLBase[T, Self] extends Tagging[Self] {
protected def mutableState: SubcontextRef
protected def toSame: SubcontextRef => Self

Expand Down Expand Up @@ -447,7 +447,7 @@ object AbstractBindingDefDSL {
val emptySetBinding = setOps.foldLeft(initial: EmptySetBinding[DIKey.BasicKey]) {
(b, instr) =>
instr match {
case AddTagsAll(tags) => b.addTags(tags)
case AddTagOntoSet(tags) => b.addTags(tags)
case SetIdAll(id) => b.withTarget(DIKey.TypeKey(b.key.tpe).named(id))
}
}
Expand Down Expand Up @@ -563,7 +563,7 @@ object AbstractBindingDefDSL {

sealed trait SetInstruction
object SetInstruction {
final case class AddTagsAll(tags: Set[BindingTag]) extends SetInstruction
final case class AddTagOntoSet(tags: Set[BindingTag]) extends SetInstruction
final case class SetIdAll(id: Identifier) extends SetInstruction
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ object IncludesDSL {
def interpret(frozenOuterTags: Set[BindingTag]): Iterator[Binding] = {

def excludeTagsOnOverlap(allTags: Set[BindingTag], excludeOnOverlap: Set[BindingTag]): Set[BindingTag] = {
val overlaps = allTags.groupBy { case BindingTag.AxisTag(choice) => choice.axis; case _ => null }
val overlaps = allTags
.collect { case a: BindingTag.AxisTag => a: BindingTag }
.groupBy { case BindingTag.AxisTag(choice) => choice.axis }

val tagsToRemove = overlaps.iterator.flatMap {
case (null, _) => Nil
case (_, overlappingTags) if overlappingTags.size > 1 => overlappingTags.intersect(excludeOnOverlap)
case _ => Nil
case _ => List.empty
}
allTags -- tagsToRemove
}
Expand Down
Loading

0 comments on commit f8d21be

Please sign in to comment.