mcmp: initial implementation, intention is to use Component instead of Context for all component instantiation

This commit is contained in:
Brian Picciano 2019-06-15 12:36:10 -06:00
parent e9b7f24dba
commit a30edfb5f9
4 changed files with 371 additions and 0 deletions

155
mcmp/component.go Normal file
View File

@ -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:]
}
}

87
mcmp/component_test.go Normal file
View File

@ -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),
)
}
}

84
mcmp/series.go Normal file
View File

@ -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
}

45
mcmp/series_test.go Normal file
View File

@ -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)),
)
}