From a30edfb5f9a3cd7770740af789deabab3aae4341 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 15 Jun 2019 12:36:10 -0600 Subject: [PATCH] mcmp: initial implementation, intention is to use Component instead of Context for all component instantiation --- mcmp/component.go | 155 +++++++++++++++++++++++++++++++++++++++++ mcmp/component_test.go | 87 +++++++++++++++++++++++ mcmp/series.go | 84 ++++++++++++++++++++++ mcmp/series_test.go | 45 ++++++++++++ 4 files changed, 371 insertions(+) create mode 100644 mcmp/component.go create mode 100644 mcmp/component_test.go create mode 100644 mcmp/series.go create mode 100644 mcmp/series_test.go diff --git a/mcmp/component.go b/mcmp/component.go new file mode 100644 index 0000000..26c7b3c --- /dev/null +++ b/mcmp/component.go @@ -0,0 +1,155 @@ +package mcmp + +import ( + "context" + "fmt" + "strings" + + "github.com/mediocregopher/mediocre-go-lib/mctx" +) + +type child struct { + *Component + name string +} + +// Component describes a single component of a program, and holds onto +// key/values for that component for use in generic libraries which instantiate +// those components. +// +// When instantiating a component it's generally necessary to know where in the +// component hierarchy it lies, for purposes of creating configuration +// parameters and so-forth. To support this, Components are able to spawn of +// child Components, each with a blank key/value namespace. Each child is +// differentiated from the other by a name, and a Component is able to use its +// Path (the sequence of names of its ancestors) to differentiate itself from +// any other component in the hierarchy. +// +// A new Component, i.e. the root Component in the hierarchy, can be initialized +// by doing: +// new(Component). +type Component struct { + path []string + parent *Component + children []child + + kv map[interface{}]interface{} +} + +// SetValue sets the given key to the given value on the Component, overwriting +// any previous value for that key. +func (c *Component) SetValue(key, value interface{}) { + if c.kv == nil { + c.kv = make(map[interface{}]interface{}, 1) + } + c.kv[key] = value +} + +// Value returns the value which has been set for the given key. +func (c *Component) Value(key interface{}) interface{} { + return c.kv[key] +} + +// Values returns all key/value pairs which have been set via SetValue. The +// returned map should _not_ be modified. +func (c *Component) Values() map[interface{}]interface{} { + return c.kv +} + +// HasValue returns true if the given key has had a value set on it with +// SetValue. +func (c *Component) HasValue(key interface{}) bool { + _, ok := c.kv[key] + return ok +} + +// Child returns a new child component of the method receiver. The child will +// have the given name, and its Path will be the receiver's path with the name +// appended. The child will not inherit any of the receiver's key/value pairs. +// +// If a child of the given name has already been created this method will panic. +func (c *Component) Child(name string) *Component { + for _, child := range c.children { + if child.name == name { + panic(fmt.Sprintf("child with name %q already exists", name)) + } + } + + childComp := &Component{ + path: append(c.path, name), + parent: c, + } + c.children = append(c.children, child{name: name, Component: childComp}) + return childComp +} + +// Children returns all Components created via the Child method on this +// Component, in the order they were created. +func (c *Component) Children() []*Component { + children := make([]*Component, len(c.children)) + for i := range c.children { + children[i] = c.children[i].Component + } + return children +} + +// Name returns the name this Component was created with (via the Child method), +// or false if this Component was not created via Child (and is therefore the +// root Component). +func (c *Component) Name() (string, bool) { + if len(c.path) == 0 { + return "", false + } + return c.path[len(c.path)-1], true +} + +// Path returns the sequence of names which were passed into Child calls in +// order to create this Component. If the Component was not created via Child +// (and is therefore the root Component) this will return an empty slice. +// +// root := new(Component) +// child := root.Child("child") +// grandChild := child.Child("grandchild") +// fmt.Printf("%#v\n", root.Path()) // "[]string(nil)" +// fmt.Printf("%#v\n", child.Path()) // []string{"child"} +// fmt.Printf("%#v\n", grandChild.Path()) // []string{"child", "grandchild"} +// +func (c *Component) Path() []string { + return c.path +} + +func (c *Component) pathStr() string { + path := c.Path() + for i := range path { + path[i] = strings.ReplaceAll(path[i], "/", `\/`) + } + return "/" + strings.Join(path, "/") +} + +type annotateKey string + +// Annotate annotates the given Context with information about the Component. +func (c *Component) Annotate(ctx context.Context) context.Context { + return mctx.Annotate(ctx, annotateKey("componentPath"), c.pathStr()) +} + +// Annotated is a shortcut for `c.Annotate(context.Background())`. +func (c *Component) Annotated() context.Context { + return c.Annotate(context.Background()) +} + +// BreadthFirstVisit visits this Component and all of its children, and their +// children, etc... in a breadth-first order. If the callback returns false then +// the function returns without visiting any more Components. +func BreadthFirstVisit(c *Component, callback func(*Component) bool) { + queue := []*Component{c} + for len(queue) > 0 { + if !callback(queue[0]) { + return + } + for _, child := range queue[0].Children() { + queue = append(queue, child) + } + queue = queue[1:] + } +} diff --git a/mcmp/component_test.go b/mcmp/component_test.go new file mode 100644 index 0000000..e1ebb77 --- /dev/null +++ b/mcmp/component_test.go @@ -0,0 +1,87 @@ +package mcmp + +import ( + . "testing" + + "github.com/mediocregopher/mediocre-go-lib/mtest/massert" +) + +func TestComponent(t *T) { + assertValue := func(c *Component, key, expectedValue interface{}) massert.Assertion { + val := c.Value(key) + ok := c.HasValue(key) + return massert.All( + massert.Equal(expectedValue, val), + massert.Equal(expectedValue != nil, ok), + ) + } + + assertName := func(c *Component, expectedName string) massert.Assertion { + name, ok := c.Name() + return massert.All( + massert.Equal(expectedName, name), + massert.Equal(expectedName != "", ok), + ) + } + + // test that a Component is initialized correctly + c := new(Component) + massert.Require(t, + assertName(c, ""), + massert.Length(c.Path(), 0), + massert.Length(c.Children(), 0), + assertValue(c, "foo", nil), + assertValue(c, "bar", nil), + ) + + // test that setting values work, and that values aren't inherited + c.SetValue("foo", 1) + child := c.Child("child") + massert.Require(t, + assertName(child, "child"), + massert.Equal([]string{"child"}, child.Path()), + massert.Length(child.Children(), 0), + massert.Equal([]*Component{child}, c.Children()), + assertValue(c, "foo", 1), + assertValue(child, "foo", nil), + ) + + // test that a child setting a value does not affect the parent + child.SetValue("bar", 2) + massert.Require(t, + assertValue(c, "bar", nil), + assertValue(child, "bar", 2), + ) +} +func TestBreadFirstVisit(t *T) { + cmp := new(Component) + cmp1 := cmp.Child("1") + cmp1a := cmp1.Child("a") + cmp1b := cmp1.Child("b") + cmp2 := cmp.Child("2") + + { + got := make([]*Component, 0, 5) + BreadthFirstVisit(cmp, func(cmp *Component) bool { + got = append(got, cmp) + return true + }) + massert.Require(t, + massert.Equal([]*Component{cmp, cmp1, cmp2, cmp1a, cmp1b}, got), + ) + } + + { + got := make([]*Component, 0, 3) + BreadthFirstVisit(cmp, func(cmp *Component) bool { + if len(cmp.Path()) > 1 { + return false + } + got = append(got, cmp) + return true + }) + massert.Require(t, + massert.Equal([]*Component{cmp, cmp1, cmp2}, got), + ) + } +} diff --git a/mcmp/series.go b/mcmp/series.go new file mode 100644 index 0000000..e9789ed --- /dev/null +++ b/mcmp/series.go @@ -0,0 +1,84 @@ +package mcmp + +const ( + seriesEls int = iota + seriesNumValueEls +) + +type seriesKey struct { + userKey interface{} + mod int +} + +// SeriesElement is used to describe a single element in a series, as +// implemented by AddSeriesValue. A SeriesElement can either be a Child which +// was spawned from the Component, or a Value which was added via +// AddSeriesValue. +type SeriesElement struct { + Child *Component + Value interface{} +} + +func seriesKeys(key interface{}) (seriesKey, seriesKey) { + return seriesKey{userKey: key, mod: seriesEls}, + seriesKey{userKey: key, mod: seriesNumValueEls} +} + +func getSeriesElements(c *Component, key interface{}) ([]SeriesElement, int) { + elsKey, numValueElsKey := seriesKeys(key) + lastEls, _ := c.Value(elsKey).([]SeriesElement) + lastNumValueEls, _ := c.Value(numValueElsKey).(int) + + children := c.Children() + lastNumChildrenEls := len(lastEls) - lastNumValueEls + + els := lastEls + for _, child := range children[lastNumChildrenEls:] { + els = append(els, SeriesElement{Child: child}) + } + return els, lastNumValueEls +} + +// AddSeriesValue is a helper which adds a value to a series which is being +// stored under the given key on the given Component. The series of values added +// under any key can be retrieved with GetSeriesValues. +// +// Additionally, AddSeriesValue keeps track of the order of calls to itself and +// children spawned from the Component. By using GetSeriesElements you can +// retrieve the sequence of values and children in the order they were added to +// the Component. +func AddSeriesValue(c *Component, key, value interface{}) { + lastEls, lastNumValueEls := getSeriesElements(c, key) + els := append(lastEls, SeriesElement{Value: value}) + + elsKey, numValueElsKey := seriesKeys(key) + c.SetValue(elsKey, els) + c.SetValue(numValueElsKey, lastNumValueEls+1) +} + +// GetSeriesElements returns the sequence of values that have been added to the +// Component under the given key via AddSeriesValue, interlaced with children +// which have been spawned from the Component, in the same respective order the +// events originally happened. +func GetSeriesElements(c *Component, key interface{}) []SeriesElement { + els, _ := getSeriesElements(c, key) + return els +} + +// GetSeriesValues returns the sequence of values that have been added to the +// Component under the given key via AddSeriesValue, in the same order the +// values were added. +func GetSeriesValues(c *Component, key interface{}) []interface{} { + elsKey, numValueElsKey := seriesKeys(key) + els, _ := c.Value(elsKey).([]SeriesElement) + numValueEls, _ := c.Value(numValueElsKey).(int) + + values := make([]interface{}, 0, numValueEls) + for _, el := range els { + if el.Child != nil { + continue + } + values = append(values, el.Value) + } + return values +} diff --git a/mcmp/series_test.go b/mcmp/series_test.go new file mode 100644 index 0000000..bf62e7b --- /dev/null +++ b/mcmp/series_test.go @@ -0,0 +1,45 @@ +package mcmp + +import ( + . "testing" + + "github.com/mediocregopher/mediocre-go-lib/mtest/massert" +) + +func TestSeries(t *T) { + key := "foo" + + // test empty state + c := new(Component) + massert.Require(t, + massert.Length(GetSeriesElements(c, key), 0), + massert.Length(GetSeriesValues(c, key), 0), + ) + + // test after a single value has been added + AddSeriesValue(c, key, 1) + massert.Require(t, + massert.Equal([]SeriesElement{{Value: 1}}, GetSeriesElements(c, key)), + massert.Equal([]interface{}{1}, GetSeriesValues(c, key)), + ) + + // test after a child has been added + childA := c.Child("a") + massert.Require(t, + massert.Equal( + []SeriesElement{{Value: 1}, {Child: childA}}, + GetSeriesElements(c, key), + ), + massert.Equal([]interface{}{1}, GetSeriesValues(c, key)), + ) + + // test after another value has been added + AddSeriesValue(c, key, 2) + massert.Require(t, + massert.Equal( + []SeriesElement{{Value: 1}, {Child: childA}, {Value: 2}}, + GetSeriesElements(c, key), + ), + massert.Equal([]interface{}{1, 2}, GetSeriesValues(c, key)), + ) +}