mediocre-go-lib/mctx/ctx.go

249 lines
7.7 KiB
Go

// Package mctx extends the builtin context package to organize Contexts into a
// hierarchy. Each node in the hierarchy is given a name and is aware of all of
// its ancestors.
//
// This package also provides extra functionality which allows contexts
// to be more useful when used in the hierarchy.
//
// All functions and methods in this package are thread-safe unless otherwise
// noted.
package mctx
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
)
////////////////////////////////////////////////////////////////////////////////
type ancestryKey int // 0 -> children, 1 -> parent, 2 -> path
const (
ancestryKeyChildren ancestryKey = iota
ancestryKeyChildrenMap
ancestryKeyParent
ancestryKeyPath
)
// Child returns the Context of the given name which was added to parent via
// WithChild, or nil if no Context of that name was ever added.
func Child(parent context.Context, name string) context.Context {
childrenMap, _ := parent.Value(ancestryKeyChildrenMap).(map[string]int)
if len(childrenMap) == 0 {
return nil
}
i, ok := childrenMap[name]
if !ok {
return nil
}
return parent.Value(ancestryKeyChildren).([]context.Context)[i]
}
// Children returns all children of this Context which have been kept by
// WithChild, mapped by their name. If this Context wasn't produced by WithChild
// then this returns nil.
func Children(parent context.Context) []context.Context {
children, _ := parent.Value(ancestryKeyChildren).([]context.Context)
return children
}
func childrenCP(parent context.Context) ([]context.Context, map[string]int) {
children := Children(parent)
// plus 1 because this is most commonly used in WithChild, which will append
// to it. At any rate it doesn't hurt anything.
outChildren := make([]context.Context, len(children), len(children)+1)
copy(outChildren, children)
childrenMap, _ := parent.Value(ancestryKeyChildrenMap).(map[string]int)
outChildrenMap := make(map[string]int, len(childrenMap)+1)
for name, i := range childrenMap {
outChildrenMap[name] = i
}
return outChildren, outChildrenMap
}
// parentOf returns the Context from which this one was generated via NewChild.
// Returns nil if this Context was not generated via NewChild.
//
// This is kept private because the behavior is a bit confusing. This will
// return the Context which was passed into NewChild, but users would probably
// expect it to return the one from WithChild if they were to call this.
func parentOf(ctx context.Context) context.Context {
parent, _ := ctx.Value(ancestryKeyParent).(context.Context)
return parent
}
// Path returns the sequence of names which were used to produce this Context
// via the NewChild function. If this Context wasn't produced by NewChild then
// this returns nil.
func Path(ctx context.Context) []string {
path, _ := ctx.Value(ancestryKeyPath).([]string)
return path
}
func pathCP(ctx context.Context) []string {
path := Path(ctx)
// plus 1 because this is most commonly used in NewChild, which will append
// to it. At any rate it doesn't hurt anything.
outPath := make([]string, len(path), len(path)+1)
copy(outPath, path)
return outPath
}
func pathHash(path []string) string {
pathHash := sha256.New()
for _, pathEl := range path {
fmt.Fprintf(pathHash, "%q.", pathEl)
}
return hex.EncodeToString(pathHash.Sum(nil))
}
// Name returns the name this Context was generated with via NewChild, or false
// if this Context was not generated via NewChild.
func Name(ctx context.Context) (string, bool) {
path := Path(ctx)
if len(path) == 0 {
return "", false
}
return path[len(path)-1], true
}
// NewChild creates a new Context based off of the parent one, and returns a new
// instance of the passed in parent and the new child. The child will have a
// path which is the parent's path with the given name appended. The parent will
// have the new child as part of its set of children (see Children function).
//
// If the parent already has a child of the given name this function panics.
func NewChild(parent context.Context, name string) context.Context {
if Child(parent, name) != nil {
panic(fmt.Sprintf("child with name %q already exists on parent", name))
}
childPath := append(pathCP(parent), name)
child := withoutLocalValues(parent)
child = context.WithValue(child, ancestryKeyChildren, nil) // unset children
child = context.WithValue(child, ancestryKeyChildrenMap, nil) // unset children
child = context.WithValue(child, ancestryKeyParent, parent)
child = context.WithValue(child, ancestryKeyPath, childPath)
return child
}
func isChild(parent, child context.Context) bool {
parentPath, childPath := Path(parent), Path(child)
if len(parentPath) != len(childPath)-1 {
return false
}
for i := range parentPath {
if parentPath[i] != childPath[i] {
return false
}
}
return true
}
// WithChild returns a modified parent which holds a reference to child in its
// Children set. If the child's name is already taken in the parent then this
// function panics.
func WithChild(parent, child context.Context) context.Context {
if !isChild(parent, child) {
panic(fmt.Sprintf("child cannot be kept by Context which is not its parent"))
}
name, _ := Name(child)
children, childrenMap := childrenCP(parent)
if _, ok := childrenMap[name]; ok {
panic(fmt.Sprintf("child with name %q already exists on parent", name))
}
children = append(children, child)
childrenMap[name] = len(children) - 1
parent = context.WithValue(parent, ancestryKeyChildren, children)
parent = context.WithValue(parent, ancestryKeyChildrenMap, childrenMap)
return parent
}
// BreadthFirstVisit visits this Context and all of its children, and their
// children, in a breadth-first order. If the callback returns false then the
// function returns without visiting any more Contexts.
func BreadthFirstVisit(ctx context.Context, callback func(context.Context) bool) {
queue := []context.Context{ctx}
for len(queue) > 0 {
if !callback(queue[0]) {
return
}
for _, child := range Children(queue[0]) {
queue = append(queue, child)
}
queue = queue[1:]
}
}
////////////////////////////////////////////////////////////////////////////////
// local value stuff
type localValsKey int
type localVal struct {
prev *localVal
key, val interface{}
}
// WithLocalValue is like WithValue, but the stored value will not be present
// on any children created via WithChild. Local values must be retrieved with
// the LocalValue function in this package. Local values share a different
// namespace than the normal WithValue/Value values (i.e. they do not overlap).
func WithLocalValue(ctx context.Context, key, val interface{}) context.Context {
prev, _ := ctx.Value(localValsKey(0)).(*localVal)
return context.WithValue(ctx, localValsKey(0), &localVal{
prev: prev,
key: key, val: val,
})
}
func withoutLocalValues(ctx context.Context) context.Context {
return context.WithValue(ctx, localValsKey(0), nil)
}
// LocalValue returns the value for the given key which was set by a call to
// WithLocalValue, or nil if no value was set for the given key.
func LocalValue(ctx context.Context, key interface{}) interface{} {
lv, _ := ctx.Value(localValsKey(0)).(*localVal)
for {
if lv == nil {
return nil
} else if lv.key == key {
return lv.val
}
lv = lv.prev
}
}
func localValuesIter(ctx context.Context, callback func(key, val interface{})) {
m := map[interface{}]struct{}{}
lv, _ := ctx.Value(localValsKey(0)).(*localVal)
for {
if lv == nil {
return
} else if _, ok := m[lv.key]; !ok {
callback(lv.key, lv.val)
m[lv.key] = struct{}{}
}
lv = lv.prev
}
}
// LocalValues returns all key/value pairs which have been set on the Context
// via WithLocalValue.
func LocalValues(ctx context.Context) map[interface{}]interface{} {
m := map[interface{}]interface{}{}
localValuesIter(ctx, func(key, val interface{}) {
m[key] = val
})
return m
}