Skip to content

Commit

Permalink
feat(util/component): add Declared.SkipFutureChanges
Browse files Browse the repository at this point in the history
  • Loading branch information
SOF3 committed Dec 16, 2024
1 parent 16d7816 commit 726728c
Showing 1 changed file with 88 additions and 31 deletions.
119 changes: 88 additions & 31 deletions util/component/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ type Component interface {
// Describes this component for dependency resolution.
manifest() manifest

canBeMergedInto() bool
// Updates the other component upon name duplication.
mergeInto(other Component) []*depRequest

Expand Down Expand Up @@ -356,21 +357,53 @@ type componentImpl[Args any, Options any, Deps any, State any] struct {
// Merge arguments of this instantiation into the previous instantiation.
//
// Optionally requests extra dependencies.
onMerge func(*Args, *Deps, *DepRequests)
mergeIntoFn func(Component) []*depRequest

// Records whether SkipFutureMerges() was called.
skipFutureMerges bool

phase *atomic.Pointer[string]
}

type setOnMerge[Args any, Deps any] interface {
type declaredComp[Args any, Deps any] interface {
setOnMerge(onMerge func(*Args, *Deps, *DepRequests))
setSkipFutureMerges()
}

//
//nolint:unused // Implements unexported interface setOnMerge, false positive from unused lint
//nolint:unused // Implements unexported interface declaredComp, false positive from unused lint
func (impl *componentImpl[Args, Options, Deps, State]) setOnMerge(
onMerge func(*Args, *Deps, *DepRequests),
) {
impl.onMerge = onMerge
impl.mergeIntoFn = strictMergeIntoFn[Args, Options, Deps, State](impl.name, onMerge)
}

//nolint:unused // Implements unexported interface declaredComp, false positive from unused lint
func (impl *componentImpl[Args, Options, Deps, State]) setSkipFutureMerges() {
impl.skipFutureMerges = true
}

func strictMergeIntoFn[Args any, Options any, Deps any, State any](
compName string,
onMerge func(*Args, *Deps, *DepRequests),
) func(Component) []*depRequest {
return func(other Component) []*depRequest {
if !other.canBeMergedInto() {
return []*depRequest{}
}

if other, isValidType := other.(*componentImpl[Args, Options, Deps, State]); isValidType {
reqs := DepRequests{requests: nil}
onMerge(&other.Args, &other.Deps, &reqs)
return reqs.requests
} else {
panic(fmt.Sprintf(
"cannot merge %q [%v, %v, %v, %v] into incompatible Component type %T",
compName,
util.Type[Args](), util.Type[Options](), util.Type[Deps](), util.Type[State](),
other,
))
}
}
}

const phaseStarted = "Started"
Expand All @@ -388,28 +421,29 @@ func isPhaseReady(phase string) bool {
//
// Refer to package documentation for the description of the arguments.
func Declare[Args any, Options any, Deps any, State any, Api any](
name func(args Args) string,
nameFn func(args Args) string,
newOptions func(args Args, fs *flag.FlagSet) Options,
newDeps func(args Args, requests *DepRequests) Deps,
init func(ctx context.Context, args Args, options Options, deps Deps) (*State, error),
lifecycle Lifecycle[Args, Options, Deps, State],
api func(d *Data[Args, Options, Deps, State]) Api,
) DeclaredCtor[Args, Deps, Api] {
return func(args Args) Declared[Api] {
name := nameFn(args)
impl := &componentImpl[Args, Options, Deps, State]{
Data: Data[Args, Options, Deps, State]{
Args: args,
Options: util.Zero[Options](),
Deps: util.Zero[Deps](),
State: nil,
},
name: name(args),
optionsFn: newOptions,
depsFn: newDeps,
init: init,
lifecycle: lifecycle,
onMerge: nil,
phase: nil,
name: name,
optionsFn: newOptions,
depsFn: newDeps,
init: init,
lifecycle: lifecycle,
mergeIntoFn: strictMergeIntoFn[Args, Options, Deps, State](name, func(*Args, *Deps, *DepRequests) {}),
phase: nil,
}

if start := impl.lifecycle.Start; start != nil {
Expand Down Expand Up @@ -442,12 +476,26 @@ func Declare[Args any, Options any, Deps any, State any, Api any](

type DeclaredCtor[Args any, Deps any, Api any] func(Args) Declared[Api]

// Avoid merging with future dependency requests with the same name, silently dropping them instead.
//
// This is equivalent to [Declared.SkipFutureMerges], only differing by call site
// (before vs after passing args).
func (ctor DeclaredCtor[Args, Deps, Api]) SkipFutureMerges() DeclaredCtor[Args, Deps, Api] {
return func(args Args) Declared[Api] {
decl := ctor(args)
decl.SkipFutureMerges()
return decl
}
}

// If another component with the same name already exists and was not created from `ApiOnly`,
// merge them together by calling `onMerge` on the states of the preceding instance.
func (ctor DeclaredCtor[Args, Deps, Api]) WithMergeFn(
onMerge func(*Args, *Deps, *DepRequests),
) DeclaredCtor[Args, Deps, Api] {
return func(args Args) Declared[Api] {
decl := ctor(args).(*declaredImpl[Args, Deps, Api])
impl := decl.comp.(setOnMerge[Args, Deps])
impl := decl.comp.(declaredComp[Args, Deps])
impl.setOnMerge(onMerge)

return decl
Expand All @@ -461,6 +509,17 @@ type Declared[Api any] interface {
GetNew() (Component, func() Api)

set(comp Component, typedApi func() Api)

// Do not merge with future dependency requests with the same name, silently dropping them instead.
// Transitive dependencies from the skipped requests will not be processed.
//
// Custom main files may register components with `SkipFutureMerges` at the beginning
// to override the "default" implementation registered afterwards.
// The custom main file must be aware of the actual implementation getting skipped
// and ensure that the overriding implementation (the receiver of this method) replaces it fully.
//
// Always returns the receiver.
SkipFutureMerges() Declared[Api]
}

// A component declaration.
Expand All @@ -483,6 +542,11 @@ func (decl *declaredImpl[Args, Deps, Api]) set(comp Component, typedApi func() A
decl.api = typedApi
}

func (decl *declaredImpl[Args, Deps, Api]) SkipFutureMerges() Declared[Api] {
decl.comp.(declaredComp[Args, Deps]).setSkipFutureMerges()
return decl
}

//nolint:unused // Used from asRawDep, false positive from unused lint
type rawDep[Api any] struct {
api *func() Api
Expand Down Expand Up @@ -534,24 +598,12 @@ func (impl *componentImpl[Args, Options, Deps, State]) manifest() manifest {
}
}

func (impl *componentImpl[Args, Options, Deps, State]) mergeInto(other Component) []*depRequest {
switch other := other.(type) {
case *componentImpl[Args, Options, Deps, State]:
deps := DepRequests{requests: nil}

if impl.onMerge != nil {
impl.onMerge(&other.Args, &other.Deps, &deps)
}

return deps.requests

case emptyComponent:
// do not merge into empty components since the implementation is exclusively determined by the test case
return []*depRequest{}
func (impl *componentImpl[Args, Options, Deps, State]) canBeMergedInto() bool {
return !impl.skipFutureMerges
}

default:
panic(fmt.Sprintf("cannot merge %q (%T) into incompatible Component type %T", impl.name, impl, other))
}
func (impl *componentImpl[Args, Options, Deps, State]) mergeInto(other Component) []*depRequest {
return impl.mergeIntoFn(other)
}

func (impl *componentImpl[Args, Options, Deps, State]) AddFlags(fs *flag.FlagSet) {
Expand Down Expand Up @@ -628,6 +680,11 @@ func (comp emptyComponent) manifest() manifest {
}
}

func (comp emptyComponent) canBeMergedInto() bool {
// do not merge into empty components since the implementation is exclusively determined by the test case
return false
}

func (comp emptyComponent) mergeInto(other Component) []*depRequest {
panic(fmt.Sprintf("component %q is already registered as %T", comp.name, other))
}
Expand Down

0 comments on commit 726728c

Please sign in to comment.