Skip to content

Commit

Permalink
groups: supports intra-group explicit dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
ryane committed Oct 21, 2016
1 parent ae2086e commit 1fccc09
Show file tree
Hide file tree
Showing 10 changed files with 474 additions and 70 deletions.
79 changes: 78 additions & 1 deletion graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ func (g *Graph) Connect(from, to string) {
g.inner.Connect(dag.BasicEdge(from, to))
}

// SafeConnect connects two vertices together by ID but only if valid
func (g *Graph) SafeConnect(from, to string) error {
g.innerLock.Lock()
defer g.innerLock.Unlock()

g.inner.Connect(dag.BasicEdge(from, to))

if err := g.Validate(); err != nil {
g.inner.RemoveEdge(dag.BasicEdge(from, to))
return err
}
return nil
}

// Disconnect two vertices by IDs
func (g *Graph) Disconnect(from, to string) {
g.innerLock.Lock()
Expand All @@ -177,6 +191,20 @@ func (g *Graph) Disconnect(from, to string) {
g.inner.RemoveEdge(dag.BasicEdge(from, to))
}

// SafeDisconnect disconnects two vertices by IDs but only if valid
func (g *Graph) SafeDisconnect(from, to string) error {
g.innerLock.Lock()
defer g.innerLock.Unlock()

g.inner.RemoveEdge(dag.BasicEdge(from, to))

if err := g.Validate(); err != nil {
g.inner.Connect(dag.BasicEdge(from, to))
return err
}
return nil
}

// UpEdges returns inward-facing edges of the specified vertex
func (g *Graph) UpEdges(id string) (out []dag.Edge) {
g.innerLock.RLock()
Expand Down Expand Up @@ -205,6 +233,36 @@ func (g *Graph) DownEdges(id string) (out []dag.Edge) {
return out
}

// DownEdgesInGroup returns the outward-facing edges of the specified vertex in
// the specified group
func (g *Graph) DownEdgesInGroup(id, group string) (out []string) {
var ingroup []string
for _, edge := range g.DownEdges(id) {
edgeID := edge.Target().(string)
if edgeNode, ok := g.Get(edgeID); ok {
if edgeNode.Group == group {
ingroup = append(ingroup, edgeID)
}
}
}
return ingroup
}

// UpEdgesInGroup returns the outward-facing edges of the specified vertex in
// the specified group
func (g *Graph) UpEdgesInGroup(id, group string) (out []string) {
var ingroup []string
for _, edge := range g.UpEdges(id) {
edgeID := edge.Source().(string)
if edgeNode, ok := g.Get(edgeID); ok {
if edgeNode.Group != "" && edgeNode.Group == group {
ingroup = append(ingroup, edgeID)
}
}
}
return ingroup
}

// Descendents gets a list of all descendents (not just children, everything)
// This only works if you're using the hierarchical ID functions from this
// module.
Expand Down Expand Up @@ -551,7 +609,7 @@ func (g *Graph) Validate() error {
return nil
}

// Vertices will det a list of the IDs for every vertex in the graph, cast to a
// Vertices will get a list of the IDs for every vertex in the graph, cast to a
// string.
func (g *Graph) Vertices() []string {
graphVertices := g.inner.Vertices()
Expand All @@ -562,6 +620,25 @@ func (g *Graph) Vertices() []string {
return vertices
}

// GroupNodes will return all nodes in the graph in the specified group
func (g *Graph) GroupNodes(group string) []*node.Node {
var nodes = []*node.Node{}
if group == "" {
return nodes
}

graphVertices := g.inner.Vertices()
for v := range graphVertices {
id := graphVertices[v].(string)
if meta, ok := g.Get(id); ok {
if meta.Group == group {
nodes = append(nodes, meta)
}
}
}
return nodes
}

// Contains returns true if the id exists in the map
func (g *Graph) Contains(id string) bool {
_, found := g.Get(id)
Expand Down
114 changes: 114 additions & 0 deletions graph/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ func TestDownEdges(t *testing.T) {
assert.Equal(t, 0, len(g.DownEdges("two")))
}

// TestDownEdgesInGroup tests that DownEdgesInGroup returns only edges with
// values in a group
func TestDownEdgesInGroup(t *testing.T) {
t.Parallel()

group := "group"
g := graph.New()
g.Add(newGroupNode("one", group, 1))
g.Add(newGroupNode("two", group, 2))
g.Add(node.New("three", 3))
g.Connect("one", "two")
g.Connect("one", "three")

assert.Equal(t, []string{"two"}, g.DownEdgesInGroup("one", group))
assert.Equal(t, 0, len(g.DownEdges("two")))
}

func TestUpEdges(t *testing.T) {
// UpEdges should return string IDs for the upward edges of a given node
t.Parallel()
Expand All @@ -116,6 +133,66 @@ func TestUpEdges(t *testing.T) {
assert.Equal(t, 0, len(g.UpEdges("one")))
}

// TestDownEdgesInGroup tests that UpEdgesInGroup returns only edges with
// values in a group
func TestUpEdgesInGroup(t *testing.T) {
t.Parallel()

group := "test"
g := graph.New()
g.Add(newGroupNode("one", group, 1))
g.Add(newGroupNode("two", group, 2))
g.Add(node.New("three", 2)) // no group
g.Connect("one", "two")
g.Connect("three", "two")

assert.Equal(t, []string{"one"}, g.UpEdgesInGroup("two", group))
}

// TestGroupNodes tests that GroupNodes only returns nodes in a specific group
func TestGroupNodes(t *testing.T) {
t.Parallel()

group := "test"
newNode := func(id string, val int) *node.Node {
n := node.New(id, val)
n.Group = group
return n
}

g := graph.New()
g.Add(newNode("one", 1))
g.Add(newNode("two", 2))
g.Add(node.New("three", 2)) // no group
g.Connect("one", "two")
g.Connect("three", "two")

assert.Equal(t, []string{"one"}, g.UpEdgesInGroup("two", group))
}

// TestSafeConnect tests that calling SafeConnect on a an invalid graph will
// return an error
func TestSafeConnect(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
g := graph.New()
g.Add(node.New("a", nil))
g.Add(node.New("b", nil))
err := g.SafeConnect("a", "b")

assert.NoError(t, err)
})

t.Run("invalid", func(t *testing.T) {
g := invalidGraph()
g.Add(node.New("a", nil))
g.Add(node.New("b", nil))
err := g.SafeConnect("a", "b")

assert.Error(t, err)
})
}

func TestDisconnect(t *testing.T) {
t.Parallel()

Expand All @@ -128,6 +205,37 @@ func TestDisconnect(t *testing.T) {
assert.NotContains(t, g.DownEdges("one"), "two")
}

// TestSafeDisconnect tests that calling SafeDisconnect on a an invalid graph
// will return an error
func TestSafeDisconnect(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
g := graph.New()
g.Add(node.New("one", 1))
g.Add(node.New("two", 2))
g.Add(node.New("three", 2))
g.Connect("one", "two")
g.Connect("one", "three")
g.Connect("two", "three")

err := g.SafeDisconnect("two", "three")
assert.NoError(t, err)
})

t.Run("invalid", func(t *testing.T) {
g := invalidGraph()
g.Add(node.New("one", 1))
g.Add(node.New("two", 2))
g.Add(node.New("three", 2))
g.Connect("one", "two")
g.Connect("one", "three")
g.Connect("two", "three")
err := g.SafeDisconnect("two", "three")

assert.Error(t, err)
})
}

func TestDescendents(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -547,3 +655,9 @@ func invalidGraph() *graph.Graph {
g.Connect("Bad", "Nodes")
return g
}

func newGroupNode(id, group string, val interface{}) *node.Node {
n := node.New(id, val)
n.Group = group
return n
}
20 changes: 18 additions & 2 deletions graph/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,28 @@

package node

// Groupable returns a group
type Groupable interface {
Group() string
}

// Node tracks the metadata associated with a node in the graph
type Node struct {
ID string `json:"id"`
ID string `json:"id"`
Group string `json:"group"`

value interface{}
}

// New creates a new node
func New(id string, value interface{}) *Node {
return &Node{
n := &Node{
ID: id,
value: value,
}
n.setGroup()

return n
}

// Value gets the inner value of this node
Expand All @@ -39,6 +48,13 @@ func (n *Node) WithValue(value interface{}) *Node {
copied := new(Node)
*copied = *n
copied.value = value
copied.setGroup()

return copied
}

func (n *Node) setGroup() {
if groupable, ok := n.value.(Groupable); ok {
n.Group = groupable.Group()
}
}
24 changes: 24 additions & 0 deletions graph/node/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,27 @@ func TestWithValue(t *testing.T) {
})
})
}

// TestWithGroupable tests that group is set when the value is Groupable
func TestWithGroupable(t *testing.T) {
t.Parallel()

t.Run("New", func(t *testing.T) {
n := node.New("test", &aGroupable{group: "somegroup"})
assert.Equal(t, "somegroup", n.Group)
})

t.Run("WithValue", func(t *testing.T) {
fst := node.New("test", 1)
assert.Equal(t, "", fst.Group)

snd := fst.WithValue(&aGroupable{group: "somegroup"})
assert.Equal(t, "somegroup", snd.Group)
})
}

type aGroupable struct {
group string
}

func (a *aGroupable) Group() string { return a.group }
Loading

0 comments on commit 1fccc09

Please sign in to comment.