Skip to content

Commit

Permalink
feat: Add ReplaceNamed(), InsertBeforeNamed(), and InsertAfterNamed() (
Browse files Browse the repository at this point in the history
…#63)

* Add ReplaceNamed(), InsertBeforeNamed(), and InsertAfterNamed()

* coverage identified some lines that were not needed

* clarify comment

* add failure tests

* review feedback
  • Loading branch information
muir authored Apr 1, 2023
1 parent 22f5e15 commit b7fb35f
Show file tree
Hide file tree
Showing 6 changed files with 651 additions and 31 deletions.
2 changes: 2 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
// implements the Provider interface and can be used anywhere a Provider is
// required.
type Collection struct {
// The above comment is wrong but helps understanding as-is.
// A collection holds a list of *provider not Provider. That list is already flattened.
name string
contents []*provider
}
Expand Down
31 changes: 0 additions & 31 deletions example_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,34 +139,3 @@ func ExampleCurry() {
// <nil>
// foo-10-33
}

// This demonstrates how it to have a default that gets overridden by
// by later inputs.
func ExampleReorder() {
type string2 string
seq1 := nject.Sequence("example",
nject.Shun(func() string {
fmt.Println("fallback default included")
return "fallback default"
}),
func(s string) string2 {
return "<" + string2(s) + ">"
},
)
seq2 := nject.Sequence("later inputs",
// for this to work, it must be reordered to be in front
// of the string->string2 provider
nject.Reorder(func() string {
return "override value"
}),
)
fmt.Println(nject.Run("combination",
seq1,
seq2,
func(s string2) {
fmt.Println(s)
},
))
// Output: <override value>
// <nil>
}
67 changes: 67 additions & 0 deletions example_reorder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package nject_test

import (
"fmt"

"github.com/muir/nject"
)

// This demonstrates how it to have a default that gets overridden by
// by later inputs using Reorder
func ExampleReorder() {
type string2 string
seq1 := nject.Sequence("example",
nject.Shun(func() string {
fmt.Println("fallback default included")
return "fallback default"
}),
func(s string) string2 {
return "<" + string2(s) + ">"
},
)
seq2 := nject.Sequence("later inputs",
// for this to work, it must be reordered to be in front
// of the string->string2 provider
nject.Reorder(func() string {
return "override value"
}),
)
fmt.Println(nject.Run("combination",
seq1,
seq2,
func(s string2) {
fmt.Println(s)
},
))
// Output: <override value>
// <nil>
}

// This demonstrates how it to have a default that gets overridden by
// by later inputs using ReplaceNamed
func ExampleReplaceNamed() {
type string2 string
seq1 := nject.Sequence("example",
nject.Provide("default-string", func() string {
fmt.Println("fallback default included")
return "fallback default"
}),
func(s string) string2 {
return "<" + string2(s) + ">"
},
)
seq2 := nject.Sequence("later inputs",
nject.ReplaceNamed("default-string", func() string {
return "override value"
}),
)
fmt.Println(nject.Run("combination",
seq1,
seq2,
func(s string2) {
fmt.Println(s)
},
))
// Output: <override value>
// <nil>
}
11 changes: 11 additions & 0 deletions nject.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type provider struct {
singleton bool
cluster int32
parallel bool
replaceByName string
insertBeforeName string
insertAfterName string

// added by characterize
memoized bool
Expand Down Expand Up @@ -102,6 +105,9 @@ func (fm *provider) copy() *provider {
flows: fm.flows,
isSynthetic: fm.isSynthetic,
mapKeyCheck: fm.mapKeyCheck,
replaceByName: fm.replaceByName,
insertBeforeName: fm.insertBeforeName,
insertAfterName: fm.insertAfterName,
}
}

Expand Down Expand Up @@ -185,6 +191,11 @@ func (c Collection) characterizeAndFlatten(nonStaticTypes map[typeCode]bool) ([]
afterInit := make([]*provider, 0, len(c.contents))
afterInvoke := make([]*provider, 0, len(c.contents))

err := c.handleReplaceByName()
if err != nil {
return nil, nil, err
}

c.reorderNonFinal()

// Handle mutations
Expand Down
253 changes: 253 additions & 0 deletions replace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package nject

import (
"fmt"
)

// ReplaceNamed will edit the set of injectors, replacing target injector,
// identified by the name it was given with Provide(), with the
// injector provided here.
// This replacement happens very early in the
// injection chain processing, before Reorder or injector selection.
// If target does not exist, the injection chain is deemed invalid.
func ReplaceNamed(target string, fn interface{}) Provider {
return newThing(fn).modify(func(fm *provider) {
fm.replaceByName = target
})
}

// InsertAfterNamed will edit the set of injectors, inserting the
// provided injector after the target injector, which is identified
// by the name it was given with Provide(). That injector can be a
// Collection. This re-arrangement happens very early in the injection
// chain processing, before Reorder or injector selection.
// If target does not exist, the injection chain is deemed invalid.
func InsertAfterNamed(target string, fn interface{}) Provider {
return newThing(fn).modify(func(fm *provider) {
fm.insertAfterName = target
})
}

// InsertBeforeNamed will edit the set of injectors, inserting the
// provided injector before the target injector, which is identified
// by the name it was given with Provide(). That injector can be a
// Collection. This re-arrangement happens very early in the injection
// chain processing, before Reorder or injector selection.
// If target does not exist, the injection chain is deemed invalid.
func InsertBeforeNamed(target string, fn interface{}) Provider {
return newThing(fn).modify(func(fm *provider) {
fm.insertBeforeName = target
})
}

// What makes handleReplaceByName complicated is that names can be duplicated
// and the replace directives can be duplicated.
//
// When you add a name, with Provide(), you can add it to a collection
// thus naming multiple providers with the same name.
//
// Likewise, when you tag a provider with InsertAfterName, you can
// be tagging a colleciton, not an individual.
func (c *Collection) handleReplaceByName() (err error) {
defer func() {
if debugEnabled() {
debugln("replacment directives --------------------------------------")
for _, fm := range c.contents {
var tag string
if fm.replaceByName != "" {
tag = "replace:" + fm.replaceByName
}
if fm.insertBeforeName != "" {
tag = "insertBefore:" + fm.insertBeforeName
}
if fm.insertAfterName != "" {
tag = "insertAfter:" + fm.insertAfterName
}
if tag != "" {
debugln("\t", tag, fm)
}
}
}
}()

var hasReplacements bool
for _, fm := range c.contents {
if fm.replaceByName != "" || fm.insertAfterName != "" || fm.insertBeforeName != "" {
hasReplacements = true
break
}
}
if !hasReplacements {
return nil
}

type node struct {
i int // for debugging
fm *provider
prev *node
next *node
processed bool
}

// step 1, convert to a linked list with a fake head & tail
head := &node{}
prior := head
for i, fm := range c.contents {
var replacers int
if fm.replaceByName != "" {
replacers++
}
if fm.insertBeforeName != "" {
replacers++
}
if fm.insertAfterName != "" {
replacers++
}
if replacers > 1 {
return fmt.Errorf("a provider, %s, can have only one of the ReplaceName, InsertAfterName, InsertBeforeName annotations", fm)
}
n := &node{
i: i,
fm: fm,
prev: prior,
}
prior.next = n
prior = n
}
tail := &node{}
prior.next = tail

// step 2, build the name index
type firstLast struct {
first *node
last *node
duplicated bool
}
names := make(map[string]*firstLast)
var lastName string
var lastFirstLast *firstLast
for n := head.next; n != tail; n = n.next {
switch {
case n.fm.origin == "":
// nothing to do
lastName = ""
case n.fm.origin == lastName:
lastFirstLast.last = n
default:
lastFirstLast = &firstLast{
first: n,
last: n,
}
lastName = n.fm.origin
if current, ok := names[lastName]; ok {
current.duplicated = true
} else {
names[lastName] = lastFirstLast
}
}
}

getTarget := func(name string, op string) (*firstLast, error) {
target, ok := names[name]
if !ok {
return nil, fmt.Errorf("cannot %s '%s', not in chain", op, name)
}
if target.duplicated {
return nil, fmt.Errorf("cannot %s '%s', duplicated in chain", op, name)
}
return target, nil
}

// step 3, do replacements
var infiniteLoopCounter int
for n := head.next; n != nil && n != tail; n = n.next {
if infiniteLoopCounter > 10000 {
return fmt.Errorf("internal error #92, infinite loop doing replacements")
}
// predicate must be true for target
snip := func(target *node, predicate func(*node) bool) (start *node, end *node) {
start = target
for end = target; end.next != tail && predicate(end.next); end = end.next {
end.processed = true
}
end.processed = true
start.prev.next = end.next
end.next.prev = start.prev
return
}
insertBefore := func(target *node, start *node, end *node) {
prev := target.prev
start.prev = prev
prev.next = start
target.prev = end
end.next = target
}
if n.processed {
// processed could already be true if a node moved
// forward in the list
continue
}
switch {
case n.fm.replaceByName != "":
name := n.fm.replaceByName
firstLast, err := getTarget(name, "replace")
if err != nil {
return err
}
delete(names, name)
firstSnip, lastSnip := snip(firstLast.first, func(n *node) bool { return n.fm.origin == name })
firstMove, lastMove := snip(n, func(n *node) bool { return n.fm.replaceByName == name })
if lastSnip.next == firstMove {
// adjacent blocks, snip before move, hack a reconnect
lastSnip.next = lastMove.next
}

if firstSnip == lastSnip {
if firstMove == lastMove {
debugln("ReplaceNamed replacing", firstSnip.i, firstSnip.fm, "with", firstMove.i, firstMove.fm)
} else {
debugln("ReplaceNamed replacing", firstSnip.i, firstSnip.fm, "with sequence from", firstMove.i, firstMove.fm, "to", lastMove.i, lastMove.fm)
}
} else {
if firstMove == lastMove {
debugln("ReplaceNamed replacing sequence from", firstSnip.i, firstSnip.fm, "through", lastSnip.i, lastSnip.fm, "with", firstMove.i, firstMove.fm)
} else {
debugln("ReplaceNamed replacing sequence from", firstSnip.i, firstSnip.fm, "through", lastSnip.i, lastSnip.fm, "with sequence from", firstMove.i, firstMove.fm, "to", lastMove.i, lastMove.fm)
}
}
afterLastMove := lastMove.next
insertBefore(lastSnip.next, firstMove, lastMove)
n = afterLastMove.prev
case n.fm.insertBeforeName != "":
name := n.fm.insertBeforeName
firstLast, err := getTarget(name, "insert before")
if err != nil {
return err
}
firstMove, lastMove := snip(n, func(n *node) bool { return n.fm.insertBeforeName == name })
afterLastMove := lastMove.next
insertBefore(firstLast.first, firstMove, lastMove)
n = afterLastMove.prev
case n.fm.insertAfterName != "":
name := n.fm.insertAfterName
firstLast, err := getTarget(name, "insert after")
if err != nil {
return err
}
firstMove, lastMove := snip(n, func(n *node) bool { return n.fm.insertAfterName == name })
afterLastMove := lastMove.next
insertBefore(firstLast.last.next, firstMove, lastMove)
n = afterLastMove.prev
default:
// nothing
}
}

// step 4, convert back to list
contents := make([]*provider, 0, len(c.contents))
for n := head.next; n != tail; n = n.next {
contents = append(contents, n.fm)
}
c.contents = contents
return nil
}
Loading

0 comments on commit b7fb35f

Please sign in to comment.