mcmp: initial implementation, intention is to use Component instead of Context for all component instantiation
This commit is contained in:
parent
e9b7f24dba
commit
a30edfb5f9
155
mcmp/component.go
Normal file
155
mcmp/component.go
Normal 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
87
mcmp/component_test.go
Normal 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
84
mcmp/series.go
Normal 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
45
mcmp/series_test.go
Normal 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)),
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user