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