diff --git a/api.go b/api.go index b4d68fb..85575a0 100644 --- a/api.go +++ b/api.go @@ -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 } diff --git a/example_provider_test.go b/example_provider_test.go index 9f71590..bc0e85f 100644 --- a/example_provider_test.go +++ b/example_provider_test.go @@ -139,34 +139,3 @@ func ExampleCurry() { // // 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: - // -} diff --git a/example_reorder_test.go b/example_reorder_test.go new file mode 100644 index 0000000..128bd91 --- /dev/null +++ b/example_reorder_test.go @@ -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: + // +} + +// 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: + // +} diff --git a/nject.go b/nject.go index ae04d76..2c9a87a 100644 --- a/nject.go +++ b/nject.go @@ -35,6 +35,9 @@ type provider struct { singleton bool cluster int32 parallel bool + replaceByName string + insertBeforeName string + insertAfterName string // added by characterize memoized bool @@ -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, } } @@ -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 diff --git a/replace.go b/replace.go new file mode 100644 index 0000000..6b5bb02 --- /dev/null +++ b/replace.go @@ -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 +} diff --git a/replace_test.go b/replace_test.go new file mode 100644 index 0000000..cd9853b --- /dev/null +++ b/replace_test.go @@ -0,0 +1,318 @@ +package nject_test + +import ( + "strconv" + "testing" + + "github.com/muir/nject" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReplace(t *testing.T) { + t.Parallel() + type action struct { + at int + target int + op func(target string, fn interface{}) nject.Provider + } + cases := []struct { + name string + n int + actions []action + want string + error string + addDup int + }{ + { + name: "no replacments", + n: 10, + want: "> 1 2 3 4 4 5 6 6 6 7 8 8 9 9 9 10 10 10 10 10", + }, + { + name: "one replace back, middle", + n: 10, + want: "> 1 2 7 4 4 5 6 6 6 8 8 9 9 9 10 10 10 10 10", + actions: []action{ + { + at: 7, + target: 3, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "one replace forward, middle", + n: 10, + want: "> 1 2 4 4 5 6 6 6 3 8 8 9 9 9 10 10 10 10 10", + actions: []action{ + { + at: 3, + target: 7, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace one with one forward, at end", + n: 11, + want: "> 1 2 4 4 5 6 6 6 7 8 8 9 9 9 10 10 10 10 10 3", + actions: []action{ + { + at: 3, + target: 11, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with one back, middle", + n: 10, + want: "> 1 2 3 7 5 6 6 6 8 8 9 9 9 10 10 10 10 10", + actions: []action{ + { + at: 7, + target: 4, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with one forward, middle", + n: 10, + want: "> 1 3 4 4 5 6 6 6 7 8 8 2 10 10 10 10 10", + actions: []action{ + { + at: 2, + target: 9, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with one forward, at end", + n: 10, + want: "> 1 3 4 4 5 6 6 6 7 8 8 9 9 9 2", + actions: []action{ + { + at: 2, + target: 10, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with mult, backwards", + n: 10, + want: "> 1 2 3 9 9 9 5 6 6 6 7 8 8 10 10 10 10 10", + actions: []action{ + { + at: 9, + target: 4, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with mult, forwards, gapped", + n: 10, + want: "> 1 2 3 4 4 5 7 8 8 6 6 6 10 10 10 10 10", + actions: []action{ + { + at: 6, + target: 9, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with mult, forwards, adjacent", + n: 10, + want: "> 1 2 3 4 4 5 6 6 6 7 8 8 10 10 10 10 10", + actions: []action{ + { + at: 8, + target: 9, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with mult, backwards, adjacent", + n: 10, + want: "> 1 2 3 4 4 5 6 6 6 7 9 9 9 10 10 10 10 10", + actions: []action{ + { + at: 9, + target: 8, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "replace mult with mult, backwards, gapped", + n: 10, + want: "> 1 2 3 9 9 9 5 6 6 6 7 8 8 10 10 10 10 10", + actions: []action{ + { + at: 9, + target: 4, + op: nject.ReplaceNamed, + }, + }, + }, + { + name: "multiple moves, before, gapped", + n: 10, + want: "> 3 1 6 6 6 2 4 4 5 7 8 8 9 9 9 10 10 10 10 10", + actions: []action{ + {at: 3, target: 1, op: nject.InsertBeforeNamed}, + {at: 6, target: 2, op: nject.InsertBeforeNamed}, + }, + }, + { + name: "multiple moves, after, gapped A1", + n: 10, + want: "> 2 3 1 4 4 5 6 6 6 7 8 8 9 9 9 10 10 10 10 10", + actions: []action{ + {at: 1, target: 3, op: nject.InsertAfterNamed}, + }, + }, + { + name: "multiple moves, after, gapped A2", + n: 10, + want: "> 2 3 1 5 6 6 6 7 4 4 8 8 9 9 9 10 10 10 10 10", + actions: []action{ + {at: 1, target: 3, op: nject.InsertAfterNamed}, + {at: 4, target: 7, op: nject.InsertAfterNamed}, + }, + }, + { + name: "multiple moves, after, gapped A3", + n: 10, + want: "> 2 3 1 6 6 6 7 4 4 8 8 9 9 9 5 10 10 10 10 10", + actions: []action{ + {at: 1, target: 3, op: nject.InsertAfterNamed}, + {at: 4, target: 7, op: nject.InsertAfterNamed}, + {at: 5, target: 9, op: nject.InsertAfterNamed}, + }, + }, + { + name: "mixed up moves", + n: 12, + want: "> 3 1 4 4 5 6 6 6 9 9 9 8 8 2 7 10 10 10 10 10 11 12 12 12", + actions: []action{ + {at: 2, target: 9, op: nject.InsertAfterNamed}, + {at: 3, target: 1, op: nject.InsertBeforeNamed}, + {at: 6, target: 7, op: nject.InsertAfterNamed}, + {at: 7, target: 2, op: nject.InsertAfterNamed}, + {at: 9, target: 8, op: nject.InsertBeforeNamed}, + {at: 10, target: 7, op: nject.InsertAfterNamed}, // no-op + {at: 11, target: 12, op: nject.InsertBeforeNamed}, // no-op + }, + }, + { + name: "moved and replaced together", + n: 10, + want: "> 1 2 3 8 8 5 9 9 9 10 10 10 10 10 4 4", + actions: []action{ + {at: 4, target: 7, op: nject.ReplaceNamed}, + {at: 8, target: 3, op: nject.InsertAfterNamed}, + {at: 9, target: 6, op: nject.ReplaceNamed}, + {at: 10, target: 4, op: nject.InsertBeforeNamed}, + }, + }, + { + name: "replace target doesn't exist", + n: 10, + error: "not in chain", + actions: []action{{at: 4, target: 22, op: nject.ReplaceNamed}}, + }, + { + name: "multiple actions", + n: 10, + error: "can have only one of", + actions: []action{ + {at: 4, target: 5, op: nject.ReplaceNamed}, + {at: 4, target: 7, op: nject.InsertBeforeNamed}, + }, + }, + { + name: "add dup, not referenced", + n: 10, + addDup: 6, + want: "> 1 2 3 4 4 6 6 6 7 8 8 9 9 9 10 10 10 10 10 6", + actions: []action{ + {at: 4, target: 5, op: nject.ReplaceNamed}, + }, + }, + { + name: "add dup, referenced", + n: 10, + addDup: 5, + error: "duplicated in chain", + actions: []action{ + {at: 4, target: 5, op: nject.ReplaceNamed}, + }, + }, + } + + mkInjector := func(prefix string, i int) nject.Provider { + return nject.Provide(prefix+strconv.Itoa(i), func(s string) string { + return s + " " + strconv.Itoa(i) + }) + } + mkInjectors := func(count int, i int) *nject.Collection { + injectors := make([]interface{}, 0, count) + n := strconv.Itoa(count) + "@" + strconv.Itoa(i) + for j := 1; j <= count; j++ { + injectors = append(injectors, mkInjector(n, i)) + } + return nject.Sequence(n, injectors...) + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + injectors := []interface{}{ + func() string { return ">" }, + } + for i := 1; i <= tc.n; i++ { + var injector interface{} + for _, m := range []int{7, 5, 3, 2} { + if i > m && i%m == 0 { + injector = mkInjectors(m, i) + break + } + } + if injector == nil { + injector = mkInjector(strconv.Itoa(i), i) + } + injector = nject.Provide("X"+strconv.Itoa(i), injector) + for _, action := range tc.actions { + if action.at == i { + injector = action.op("X"+strconv.Itoa(action.target), injector) + } + } + injectors = append(injectors, injector) + } + if tc.addDup != 0 { + injectors = append(injectors, nject.Provide("X"+strconv.Itoa(tc.addDup), mkInjector(strconv.Itoa(tc.addDup), tc.addDup))) + } + + var ran bool + err := nject.Run(t.Name(), + nject.Sequence("test", injectors...), + func(s string) { + ran = true + assert.Equal(t, tc.want, s) + }, + ) + if tc.error != "" { + assert.Contains(t, err.Error(), tc.error, "error") + } else { + require.NoError(t, err, "run") + assert.True(t, ran) + } + }) + } +}