lang: Add internal, resource specific edges
This adds the ability to specify internal, resource specific edges, with and without notifications. We use the special words: "Notify", "Before", "Listen", and "Depend". They must have the first character capitalized. They also support the "elvis" operator.
This commit is contained in:
@@ -163,6 +163,45 @@ it evaluates to `true`, then the parameter will be used. If no `elvis` operator
|
||||
is specified, then the parameter value will also be used. If the parameter is
|
||||
not specified, then it will obviously not be used.
|
||||
|
||||
Resources may also declare edges internally. The edges may point to or from
|
||||
another resource, and may optionally include a notification. The four properties
|
||||
are: `Before`, `Depend`, `Notify` and `Listen`. The first two represent normal
|
||||
edge dependencies, and the second two are normal edge dependencies that also
|
||||
send notifications. You may have multiples of these per resource, including
|
||||
multiple `Depend` lines if necessary. Each of these properties also supports the
|
||||
conditional inclusion `elvis` operator as well.
|
||||
|
||||
For example, you may write is:
|
||||
|
||||
```mcl
|
||||
$b = true # for example purposes
|
||||
if $b {
|
||||
pkg "drbd" {
|
||||
state => "installed",
|
||||
|
||||
# multiple properties may be used in the same resource
|
||||
Before => File["/etc/drbd.conf"],
|
||||
Before => Svc["drbd"],
|
||||
}
|
||||
}
|
||||
file "/etc/drbd.conf" {
|
||||
content => "some config",
|
||||
|
||||
Depend => $b ?: Pkg["drbd"],
|
||||
Notify => Svc["drbd"],
|
||||
}
|
||||
svc "drbd" {
|
||||
state => "running",
|
||||
}
|
||||
```
|
||||
|
||||
There are two unique properties about these edges that is different from what
|
||||
you might expect from other automation software:
|
||||
|
||||
1. The ability to specify multiples of these properties allows you to avoid
|
||||
having to manage arrays and conditional trees of these different dependencies.
|
||||
2. The keywords all have the same length, which means your code lines up nicely.
|
||||
|
||||
#### Edge
|
||||
|
||||
Edges express dependencies in the graph of resources which are output. They can
|
||||
|
||||
18
examples/lang/edges1.mcl
Normal file
18
examples/lang/edges1.mcl
Normal file
@@ -0,0 +1,18 @@
|
||||
$b = true
|
||||
if $b {
|
||||
exec "exec0" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
}
|
||||
exec "exec1" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
|
||||
Depend => $b ?: Exec["exec0"],
|
||||
Before => Exec["exec2"],
|
||||
}
|
||||
exec "exec2" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
@@ -59,8 +59,8 @@ func TestInterpolate0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: &ExprStr{
|
||||
V: "foo",
|
||||
@@ -103,8 +103,8 @@ func TestInterpolate0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: fieldName,
|
||||
},
|
||||
@@ -190,8 +190,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: &ExprStr{
|
||||
V: "foo",
|
||||
@@ -208,8 +208,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: &ExprStr{
|
||||
V: "foo",
|
||||
@@ -234,8 +234,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t${blah}",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: &ExprStr{
|
||||
V: "foo",
|
||||
@@ -264,8 +264,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: resName,
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: &ExprStr{
|
||||
V: "foo",
|
||||
@@ -290,8 +290,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t${42}", // incorrect type
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: &ExprStr{
|
||||
V: "foo",
|
||||
@@ -321,8 +321,8 @@ func TestInterpolateBasicStmt(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: resName,
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: &ExprStr{
|
||||
V: "foo",
|
||||
|
||||
@@ -320,8 +320,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -361,8 +361,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "float32",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -413,8 +413,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -465,8 +465,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -517,8 +517,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -569,8 +569,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "boolptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -621,8 +621,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "boolptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -673,8 +673,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "boolptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -722,8 +722,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "boolptr",
|
||||
Value: &ExprCall{
|
||||
Name: operatorFuncName,
|
||||
@@ -774,8 +774,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprInt{
|
||||
V: 42,
|
||||
@@ -788,8 +788,8 @@ func TestLexParse0(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "t2",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprInt{
|
||||
V: 13,
|
||||
|
||||
@@ -63,8 +63,9 @@ func init() {
|
||||
structFields []*ExprStructField
|
||||
structField *ExprStructField
|
||||
|
||||
resFields []*StmtResField
|
||||
resContents []StmtResContents // interface
|
||||
resField *StmtResField
|
||||
resEdge *StmtResEdge
|
||||
|
||||
edgeHalfList []*StmtEdgeHalf
|
||||
edgeHalf *StmtEdgeHalf
|
||||
@@ -667,7 +668,7 @@ resource:
|
||||
$$.stmt = &StmtRes{
|
||||
Kind: $1.str,
|
||||
Name: $2.expr,
|
||||
Fields: $4.resFields,
|
||||
Contents: $4.resContents,
|
||||
}
|
||||
}
|
||||
;
|
||||
@@ -675,17 +676,27 @@ resource_body:
|
||||
/* end of list */
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.resFields = []*StmtResField{}
|
||||
$$.resContents = []StmtResContents{}
|
||||
}
|
||||
| resource_body resource_field
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.resFields = append($1.resFields, $2.resField)
|
||||
$$.resContents = append($1.resContents, $2.resField)
|
||||
}
|
||||
| resource_body conditional_resource_field
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.resFields = append($1.resFields, $2.resField)
|
||||
$$.resContents = append($1.resContents, $2.resField)
|
||||
}
|
||||
| resource_body resource_edge
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.resContents = append($1.resContents, $2.resEdge)
|
||||
}
|
||||
| resource_body conditional_resource_edge
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.resContents = append($1.resContents, $2.resEdge)
|
||||
}
|
||||
;
|
||||
resource_field:
|
||||
@@ -710,6 +721,29 @@ conditional_resource_field:
|
||||
}
|
||||
}
|
||||
;
|
||||
resource_edge:
|
||||
// Before => Test["t1"],
|
||||
CAPITALIZED_IDENTIFIER ROCKET edge_half COMMA
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.resEdge = &StmtResEdge{
|
||||
Property: $1.str,
|
||||
EdgeHalf: $3.edgeHalf,
|
||||
}
|
||||
}
|
||||
;
|
||||
conditional_resource_edge:
|
||||
// Before => $present ?: Test["t1"],
|
||||
CAPITALIZED_IDENTIFIER ROCKET expr ELVIS edge_half COMMA
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.resEdge = &StmtResEdge{
|
||||
Property: $1.str,
|
||||
EdgeHalf: $5.edgeHalf,
|
||||
Condition: $3.expr,
|
||||
}
|
||||
}
|
||||
;
|
||||
edge:
|
||||
// TODO: we could technically prevent single edge_half pieces from being
|
||||
// parsed, but it's probably more work than is necessary...
|
||||
|
||||
593
lang/structs.go
593
lang/structs.go
@@ -35,6 +35,24 @@ import (
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// EdgeNotify declares an edge a -> b, such that a notification occurs.
|
||||
// This is most similar to "notify" in Puppet.
|
||||
EdgeNotify = "notify"
|
||||
|
||||
// EdgeBefore declares an edge a -> b, such that no notification occurs.
|
||||
// This is most similar to "before" in Puppet.
|
||||
EdgeBefore = "before"
|
||||
|
||||
// EdgeListen declares an edge a <- b, such that a notification occurs.
|
||||
// This is most similar to "subscribe" in Puppet.
|
||||
EdgeListen = "listen"
|
||||
|
||||
// EdgeDepend declares an edge a <- b, such that no notification occurs.
|
||||
// This is most similar to "require" in Puppet.
|
||||
EdgeDepend = "depend"
|
||||
)
|
||||
|
||||
// StmtBind is a representation of an assignment, which binds a variable to an
|
||||
// expression.
|
||||
type StmtBind struct {
|
||||
@@ -96,48 +114,38 @@ func (obj *StmtBind) Output() (*interfaces.Output, error) {
|
||||
return (&interfaces.Output{}).Empty(), nil
|
||||
}
|
||||
|
||||
// StmtRes is a representation of a resource.
|
||||
// StmtRes is a representation of a resource and possibly some edges.
|
||||
// TODO: consider expanding Name (if it's a list) to have this return a list of
|
||||
// Res's in the Output function. Alternatively, it could be a map[name]struct{},
|
||||
// or even a map[[]name]struct{}.
|
||||
type StmtRes struct {
|
||||
Kind string // kind of resource, eg: pkg, file, svc, etc...
|
||||
Name interfaces.Expr // unique name for the res of this kind
|
||||
Fields []*StmtResField // list of fields in parsed order
|
||||
Contents []StmtResContents // list of fields/edges in parsed order
|
||||
}
|
||||
|
||||
// Interpolate returns a new node (or itself) once it has been expanded. This
|
||||
// generally increases the size of the AST when it is used. It calls Interpolate
|
||||
// on any child elements and builds the new node with those new node contents.
|
||||
// TODO: could we expand Name (if it's a list) to have this return a list of
|
||||
// Res's ? We'd have to return a StmtProg containing those in its place...
|
||||
func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) {
|
||||
name, err := obj.Name.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := []*StmtResField{}
|
||||
for _, x := range obj.Fields {
|
||||
interpolated, err := x.Value.Interpolate()
|
||||
contents := []StmtResContents{}
|
||||
for _, x := range obj.Contents { // make sure we preserve ordering...
|
||||
interpolated, err := x.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var condition interfaces.Expr
|
||||
if x.Condition != nil {
|
||||
condition, err = x.Condition.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
field := &StmtResField{
|
||||
Field: x.Field,
|
||||
Value: interpolated,
|
||||
Condition: condition,
|
||||
}
|
||||
fields = append(fields, field)
|
||||
contents = append(contents, interpolated)
|
||||
}
|
||||
|
||||
return &StmtRes{
|
||||
Kind: obj.Kind,
|
||||
Name: name,
|
||||
Fields: fields,
|
||||
Contents: contents,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -147,15 +155,10 @@ func (obj *StmtRes) SetScope(scope *interfaces.Scope) error {
|
||||
if err := obj.Name.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, x := range obj.Fields {
|
||||
if err := x.Value.SetScope(scope); err != nil {
|
||||
for _, x := range obj.Contents {
|
||||
if err := x.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
if x.Condition != nil {
|
||||
if err := x.Condition.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -179,54 +182,13 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) {
|
||||
}
|
||||
invariants = append(invariants, invar)
|
||||
|
||||
// collect all the invariants of each field
|
||||
for _, x := range obj.Fields {
|
||||
invars, err := x.Value.Unify()
|
||||
// collect all the invariants of each field and edge
|
||||
for _, x := range obj.Contents {
|
||||
invars, err := x.Unify(obj.Kind) // pass in the resource kind
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, invars...)
|
||||
|
||||
// conditional expression might have some children invariants to share
|
||||
if x.Condition != nil {
|
||||
condition, err := x.Condition.Unify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, condition...)
|
||||
|
||||
// the condition must ultimately be a boolean
|
||||
conditionInvar := &unification.EqualsInvariant{
|
||||
Expr: x.Condition,
|
||||
Type: types.TypeBool,
|
||||
}
|
||||
invariants = append(invariants, conditionInvar)
|
||||
}
|
||||
}
|
||||
|
||||
typMap, err := resources.LangFieldNameToStructType(obj.Kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, x := range obj.Fields {
|
||||
field := strings.TrimSpace(x.Field)
|
||||
if len(field) != len(x.Field) {
|
||||
return nil, fmt.Errorf("field was wrapped in whitespace")
|
||||
}
|
||||
if len(strings.Fields(field)) != 1 {
|
||||
return nil, fmt.Errorf("field was empty or contained spaces")
|
||||
}
|
||||
|
||||
typ, exists := typMap[x.Field]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("could not determine type for `%s` field of `%s`", x.Field, obj.Kind)
|
||||
}
|
||||
invar := &unification.EqualsInvariant{
|
||||
Expr: x.Value,
|
||||
Type: typ,
|
||||
}
|
||||
invariants = append(invariants, invar)
|
||||
}
|
||||
|
||||
return invariants, nil
|
||||
@@ -253,20 +215,12 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
|
||||
for _, x := range obj.Fields {
|
||||
g, err := x.Value.Graph()
|
||||
for _, x := range obj.Contents {
|
||||
g, err := x.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
|
||||
if x.Condition != nil {
|
||||
g, err := x.Condition.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
}
|
||||
}
|
||||
|
||||
return graph, nil
|
||||
@@ -277,12 +231,13 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
|
||||
// analogous function for expressions is Value. Those Value functions might get
|
||||
// called by this Output function if they are needed to produce the output. In
|
||||
// the case of this resource statement, this is definitely the case.
|
||||
// XXX: Add MetaParams...
|
||||
// XXX: Add MetaParams as a simple meta field with a struct of the right type...
|
||||
func (obj *StmtRes) Output() (*interfaces.Output, error) {
|
||||
nameValue, err := obj.Name.Value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: test for []str instead, and loop
|
||||
name := nameValue.Str() // must not panic
|
||||
|
||||
res, err := resources.NewNamedResource(obj.Kind, name)
|
||||
@@ -302,7 +257,12 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
|
||||
ts := reflect.TypeOf(res).Elem() // pointer to struct, then struct
|
||||
|
||||
// FIXME: we could probably simplify this code...
|
||||
for _, x := range obj.Fields {
|
||||
for _, line := range obj.Contents {
|
||||
x, ok := line.(*StmtResField)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if x.Condition != nil {
|
||||
b, err := x.Condition.Value()
|
||||
if err != nil {
|
||||
@@ -408,13 +368,141 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
|
||||
value.Elem().Set(valof)
|
||||
}
|
||||
f.Set(value) // set it !
|
||||
|
||||
}
|
||||
|
||||
edges, err := obj.edges()
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error building edges")
|
||||
}
|
||||
|
||||
return &interfaces.Output{
|
||||
Resources: []resources.Res{res},
|
||||
Edges: edges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// edges is a helper function to generate the edges that come from the resource.
|
||||
func (obj *StmtRes) edges() ([]*interfaces.Edge, error) {
|
||||
edges := []*interfaces.Edge{}
|
||||
|
||||
// to and from self, map of kind, name, notify
|
||||
var to = make(map[string]map[string]bool) // to this from self
|
||||
var from = make(map[string]map[string]bool) // from this to self
|
||||
|
||||
for _, line := range obj.Contents {
|
||||
x, ok := line.(*StmtResEdge)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if x.Condition != nil {
|
||||
b, err := x.Condition.Value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !b.Bool() { // if value exists, and is false, skip it
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
v, err := x.EdgeHalf.Name.Value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := v.Str() // must not panic
|
||||
kind := x.EdgeHalf.Kind
|
||||
var notify bool
|
||||
|
||||
switch p := x.Property; p {
|
||||
// a -> b
|
||||
// a notify b
|
||||
// a before b
|
||||
case EdgeNotify:
|
||||
notify = true
|
||||
fallthrough
|
||||
case EdgeBefore:
|
||||
if m, exists := to[kind]; !exists {
|
||||
to[kind] = make(map[string]bool)
|
||||
} else if n, exists := m[name]; exists {
|
||||
notify = notify || n // collate
|
||||
}
|
||||
to[kind][name] = notify // to this from self
|
||||
|
||||
// b -> a
|
||||
// b listen a
|
||||
// b depend a
|
||||
case EdgeListen:
|
||||
notify = true
|
||||
fallthrough
|
||||
case EdgeDepend:
|
||||
if m, exists := from[kind]; !exists {
|
||||
from[kind] = make(map[string]bool)
|
||||
} else if n, exists := m[name]; exists {
|
||||
notify = notify || n // collate
|
||||
}
|
||||
from[kind][name] = notify // from this to self
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown property: %s", p)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we could detect simple loops here (if `from` and `to` have the
|
||||
// same entry) but we can leave this to the proper dag checker later on
|
||||
|
||||
v, err := obj.Name.Value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
self := v.Str() // must not panic
|
||||
|
||||
for kind, x := range to { // to this from self
|
||||
for name, notify := range x {
|
||||
edge := &interfaces.Edge{
|
||||
Kind1: obj.Kind,
|
||||
Name1: self,
|
||||
//Send: "",
|
||||
|
||||
Kind2: kind,
|
||||
Name2: name,
|
||||
//Recv: "",
|
||||
|
||||
Notify: notify,
|
||||
}
|
||||
edges = append(edges, edge)
|
||||
}
|
||||
}
|
||||
for kind, x := range from { // from this to self
|
||||
for name, notify := range x {
|
||||
edge := &interfaces.Edge{
|
||||
Kind1: kind,
|
||||
Name1: name,
|
||||
//Send: "",
|
||||
|
||||
Kind2: obj.Kind,
|
||||
Name2: self,
|
||||
//Recv: "",
|
||||
|
||||
Notify: notify,
|
||||
}
|
||||
edges = append(edges, edge)
|
||||
}
|
||||
}
|
||||
|
||||
return edges, nil
|
||||
}
|
||||
|
||||
// StmtResContents is the interface that is met by the resource contents. Look
|
||||
// closely for while it is similar to the Stmt interface, it is quite different.
|
||||
type StmtResContents interface {
|
||||
Interpolate() (StmtResContents, error) // different!
|
||||
SetScope(*interfaces.Scope) error
|
||||
Unify(kind string) ([]interfaces.Invariant, error) // different!
|
||||
Graph() (*pgraph.Graph, error)
|
||||
}
|
||||
|
||||
// StmtResField represents a single field in the parsed resource representation.
|
||||
// This does not satisfy the Stmt interface.
|
||||
type StmtResField struct {
|
||||
@@ -423,6 +511,243 @@ type StmtResField struct {
|
||||
Condition interfaces.Expr // the value will be used if nil or true
|
||||
}
|
||||
|
||||
// Interpolate returns a new node (or itself) once it has been expanded. This
|
||||
// generally increases the size of the AST when it is used. It calls Interpolate
|
||||
// on any child elements and builds the new node with those new node contents.
|
||||
// This interpolate is different It is different from the interpolate found in
|
||||
// the Expr and Stmt interfaces because it returns a different type as output.
|
||||
func (obj *StmtResField) Interpolate() (StmtResContents, error) {
|
||||
interpolated, err := obj.Value.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var condition interfaces.Expr
|
||||
if obj.Condition != nil {
|
||||
condition, err = obj.Condition.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &StmtResField{
|
||||
Field: obj.Field,
|
||||
Value: interpolated,
|
||||
Condition: condition,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetScope stores the scope for later use in this resource and it's children,
|
||||
// which it propagates this downwards to.
|
||||
func (obj *StmtResField) SetScope(scope *interfaces.Scope) error {
|
||||
if err := obj.Value.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
if obj.Condition != nil {
|
||||
if err := obj.Condition.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unify returns the list of invariants that this node produces. It recursively
|
||||
// calls Unify on any children elements that exist in the AST, and returns the
|
||||
// collection to the caller. It is different from the Unify found in the Expr
|
||||
// and Stmt interfaces because it adds an input parameter.
|
||||
func (obj *StmtResField) Unify(kind string) ([]interfaces.Invariant, error) {
|
||||
var invariants []interfaces.Invariant
|
||||
|
||||
invars, err := obj.Value.Unify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, invars...)
|
||||
|
||||
// conditional expression might have some children invariants to share
|
||||
if obj.Condition != nil {
|
||||
condition, err := obj.Condition.Unify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, condition...)
|
||||
|
||||
// the condition must ultimately be a boolean
|
||||
conditionInvar := &unification.EqualsInvariant{
|
||||
Expr: obj.Condition,
|
||||
Type: types.TypeBool,
|
||||
}
|
||||
invariants = append(invariants, conditionInvar)
|
||||
}
|
||||
|
||||
// TODO: unfortunately this gets called separately for each field... if
|
||||
// we could cache this, it might be worth looking into for performance!
|
||||
typMap, err := resources.LangFieldNameToStructType(kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(obj.Field)
|
||||
if len(field) != len(obj.Field) {
|
||||
return nil, fmt.Errorf("field was wrapped in whitespace")
|
||||
}
|
||||
if len(strings.Fields(field)) != 1 {
|
||||
return nil, fmt.Errorf("field was empty or contained spaces")
|
||||
}
|
||||
|
||||
typ, exists := typMap[obj.Field]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("could not determine type for `%s` field of `%s`", obj.Field, kind)
|
||||
}
|
||||
invar := &unification.EqualsInvariant{
|
||||
Expr: obj.Value,
|
||||
Type: typ,
|
||||
}
|
||||
invariants = append(invariants, invar)
|
||||
|
||||
return invariants, nil
|
||||
}
|
||||
|
||||
// Graph returns the reactive function graph which is expressed by this node. It
|
||||
// includes any vertices produced by this node, and the appropriate edges to any
|
||||
// vertices that are produced by its children. Nodes which fulfill the Expr
|
||||
// interface directly produce vertices (and possible children) where as nodes
|
||||
// that fulfill the Stmt interface do not produces vertices, where as their
|
||||
// children might. It is interesting to note that nothing directly adds an edge
|
||||
// to the resources created, but rather, once all the values (expressions) with
|
||||
// no outgoing edges have produced at least a single value, then the resources
|
||||
// know they're able to be built.
|
||||
func (obj *StmtResField) Graph() (*pgraph.Graph, error) {
|
||||
graph, err := pgraph.NewGraph("resfield")
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "could not create graph")
|
||||
}
|
||||
|
||||
g, err := obj.Value.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
|
||||
if obj.Condition != nil {
|
||||
g, err := obj.Condition.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
}
|
||||
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// StmtResEdge represents a single edge property in the parsed resource
|
||||
// representation. This does not satisfy the Stmt interface.
|
||||
type StmtResEdge struct {
|
||||
Property string // TODO: iota constant instead?
|
||||
EdgeHalf *StmtEdgeHalf
|
||||
Condition interfaces.Expr // the value will be used if nil or true
|
||||
}
|
||||
|
||||
// Interpolate returns a new node (or itself) once it has been expanded. This
|
||||
// generally increases the size of the AST when it is used. It calls Interpolate
|
||||
// on any child elements and builds the new node with those new node contents.
|
||||
// This interpolate is different It is different from the interpolate found in
|
||||
// the Expr and Stmt interfaces because it returns a different type as output.
|
||||
func (obj *StmtResEdge) Interpolate() (StmtResContents, error) {
|
||||
interpolated, err := obj.EdgeHalf.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var condition interfaces.Expr
|
||||
if obj.Condition != nil {
|
||||
condition, err = obj.Condition.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &StmtResEdge{
|
||||
Property: obj.Property,
|
||||
EdgeHalf: interpolated,
|
||||
Condition: condition,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetScope stores the scope for later use in this resource and it's children,
|
||||
// which it propagates this downwards to.
|
||||
func (obj *StmtResEdge) SetScope(scope *interfaces.Scope) error {
|
||||
if err := obj.EdgeHalf.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
if obj.Condition != nil {
|
||||
if err := obj.Condition.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unify returns the list of invariants that this node produces. It recursively
|
||||
// calls Unify on any children elements that exist in the AST, and returns the
|
||||
// collection to the caller. It is different from the Unify found in the Expr
|
||||
// and Stmt interfaces because it adds an input parameter.
|
||||
func (obj *StmtResEdge) Unify(kind string) ([]interfaces.Invariant, error) {
|
||||
var invariants []interfaces.Invariant
|
||||
|
||||
invars, err := obj.EdgeHalf.Unify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, invars...)
|
||||
|
||||
// conditional expression might have some children invariants to share
|
||||
if obj.Condition != nil {
|
||||
condition, err := obj.Condition.Unify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, condition...)
|
||||
|
||||
// the condition must ultimately be a boolean
|
||||
conditionInvar := &unification.EqualsInvariant{
|
||||
Expr: obj.Condition,
|
||||
Type: types.TypeBool,
|
||||
}
|
||||
invariants = append(invariants, conditionInvar)
|
||||
}
|
||||
|
||||
return invariants, nil
|
||||
}
|
||||
|
||||
// Graph returns the reactive function graph which is expressed by this node. It
|
||||
// includes any vertices produced by this node, and the appropriate edges to any
|
||||
// vertices that are produced by its children. Nodes which fulfill the Expr
|
||||
// interface directly produce vertices (and possible children) where as nodes
|
||||
// that fulfill the Stmt interface do not produces vertices, where as their
|
||||
// children might. It is interesting to note that nothing directly adds an edge
|
||||
// to the resources created, but rather, once all the values (expressions) with
|
||||
// no outgoing edges have produced at least a single value, then the resources
|
||||
// know they're able to be built.
|
||||
func (obj *StmtResEdge) Graph() (*pgraph.Graph, error) {
|
||||
graph, err := pgraph.NewGraph("resedge")
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "could not create graph")
|
||||
}
|
||||
|
||||
g, err := obj.EdgeHalf.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
|
||||
if obj.Condition != nil {
|
||||
g, err := obj.Condition.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
}
|
||||
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// StmtEdge is a representation of a dependency. It also supports send/recv.
|
||||
// Edges represents that the first resource (Kind/Name) listed in the
|
||||
// EdgeHalfList should happen in the resource graph *before* the next resource
|
||||
@@ -448,16 +773,10 @@ type StmtEdge struct {
|
||||
func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) {
|
||||
edgeHalfList := []*StmtEdgeHalf{}
|
||||
for _, x := range obj.EdgeHalfList {
|
||||
name, err := x.Name.Interpolate()
|
||||
edgeHalf, err := x.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edgeHalf := &StmtEdgeHalf{
|
||||
Kind: x.Kind,
|
||||
Name: name,
|
||||
SendRecv: x.SendRecv,
|
||||
}
|
||||
edgeHalfList = append(edgeHalfList, edgeHalf)
|
||||
}
|
||||
|
||||
@@ -471,7 +790,7 @@ func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) {
|
||||
// which it propagates this downwards to.
|
||||
func (obj *StmtEdge) SetScope(scope *interfaces.Scope) error {
|
||||
for _, x := range obj.EdgeHalfList {
|
||||
if err := x.Name.SetScope(scope); err != nil {
|
||||
if err := x.SetScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -499,26 +818,15 @@ func (obj *StmtEdge) Unify() ([]interfaces.Invariant, error) {
|
||||
}
|
||||
|
||||
for _, x := range obj.EdgeHalfList {
|
||||
if x.Kind == "" {
|
||||
return nil, fmt.Errorf("missing resource kind in edge")
|
||||
}
|
||||
|
||||
if x.SendRecv != "" && len(obj.EdgeHalfList) != 2 {
|
||||
return nil, fmt.Errorf("send/recv edges must come in pairs")
|
||||
}
|
||||
|
||||
invars, err := x.Name.Unify()
|
||||
invars, err := x.Unify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, invars...)
|
||||
|
||||
// name must be a string
|
||||
invar := &unification.EqualsInvariant{
|
||||
Expr: x.Name,
|
||||
Type: types.TypeStr,
|
||||
}
|
||||
invariants = append(invariants, invar)
|
||||
}
|
||||
|
||||
return invariants, nil
|
||||
@@ -540,7 +848,7 @@ func (obj *StmtEdge) Graph() (*pgraph.Graph, error) {
|
||||
}
|
||||
|
||||
for _, x := range obj.EdgeHalfList {
|
||||
g, err := x.Name.Graph()
|
||||
g, err := x.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -599,6 +907,81 @@ type StmtEdgeHalf struct {
|
||||
SendRecv string // name of field to send/recv from, empty to ignore
|
||||
}
|
||||
|
||||
// Interpolate returns a new node (or itself) once it has been expanded. This
|
||||
// generally increases the size of the AST when it is used. It calls Interpolate
|
||||
// on any child elements and builds the new node with those new node contents.
|
||||
// This interpolate is different It is different from the interpolate found in
|
||||
// the Expr and Stmt interfaces because it returns a different type as output.
|
||||
func (obj *StmtEdgeHalf) Interpolate() (*StmtEdgeHalf, error) {
|
||||
name, err := obj.Name.Interpolate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edgeHalf := &StmtEdgeHalf{
|
||||
Kind: obj.Kind,
|
||||
Name: name,
|
||||
SendRecv: obj.SendRecv,
|
||||
}
|
||||
return edgeHalf, nil
|
||||
}
|
||||
|
||||
// SetScope stores the scope for later use in this resource and it's children,
|
||||
// which it propagates this downwards to.
|
||||
func (obj *StmtEdgeHalf) SetScope(scope *interfaces.Scope) error {
|
||||
return obj.Name.SetScope(scope)
|
||||
}
|
||||
|
||||
// Unify returns the list of invariants that this node produces. It recursively
|
||||
// calls Unify on any children elements that exist in the AST, and returns the
|
||||
// collection to the caller.
|
||||
func (obj *StmtEdgeHalf) Unify() ([]interfaces.Invariant, error) {
|
||||
var invariants []interfaces.Invariant
|
||||
|
||||
if obj.Kind == "" {
|
||||
return nil, fmt.Errorf("missing resource kind in edge")
|
||||
}
|
||||
|
||||
invars, err := obj.Name.Unify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invariants = append(invariants, invars...)
|
||||
|
||||
// name must be a string
|
||||
invar := &unification.EqualsInvariant{
|
||||
Expr: obj.Name,
|
||||
Type: types.TypeStr,
|
||||
}
|
||||
invariants = append(invariants, invar)
|
||||
|
||||
return invariants, nil
|
||||
}
|
||||
|
||||
// Graph returns the reactive function graph which is expressed by this node. It
|
||||
// includes any vertices produced by this node, and the appropriate edges to any
|
||||
// vertices that are produced by its children. Nodes which fulfill the Expr
|
||||
// interface directly produce vertices (and possible children) where as nodes
|
||||
// that fulfill the Stmt interface do not produces vertices, where as their
|
||||
// children might. It is interesting to note that nothing directly adds an edge
|
||||
// to the resources created, but rather, once all the values (expressions) with
|
||||
// no outgoing edges have produced at least a single value, then the resources
|
||||
// know they're able to be built.
|
||||
func (obj *StmtEdgeHalf) Graph() (*pgraph.Graph, error) {
|
||||
graph, err := pgraph.NewGraph("edgehalf")
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "could not create graph")
|
||||
}
|
||||
|
||||
g, err := obj.Name.Graph()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.AddGraph(g)
|
||||
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// StmtIf represents an if condition that contains between one and two branches
|
||||
// of statements to be executed based on the evaluation of the boolean condition
|
||||
// over time. In particular, this is different from an ExprIf which returns a
|
||||
|
||||
@@ -51,8 +51,8 @@ func TestUnification1(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{V: "t1"},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "str",
|
||||
Value: expr,
|
||||
},
|
||||
@@ -85,8 +85,8 @@ func TestUnification1(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{V: "test"},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "slicestring",
|
||||
Value: expr,
|
||||
},
|
||||
@@ -125,8 +125,8 @@ func TestUnification1(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{V: "test"},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "mapintfloat",
|
||||
Value: expr,
|
||||
},
|
||||
@@ -167,8 +167,8 @@ func TestUnification1(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{V: "test"},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "mixedstruct",
|
||||
Value: expr,
|
||||
},
|
||||
@@ -215,8 +215,8 @@ func TestUnification1(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "n1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: expr, // func
|
||||
},
|
||||
@@ -270,8 +270,8 @@ func TestUnification1(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "n1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "int64ptr",
|
||||
Value: expr,
|
||||
},
|
||||
@@ -326,8 +326,8 @@ func TestUnification1(t *testing.T) {
|
||||
Name: &ExprStr{
|
||||
V: "n1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "float32",
|
||||
Value: expr,
|
||||
},
|
||||
@@ -458,8 +458,8 @@ func TestUnification1(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{V: "t1"},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: expr,
|
||||
},
|
||||
@@ -493,8 +493,8 @@ func TestUnification1(t *testing.T) {
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{V: "t1"},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Contents: []StmtResContents{
|
||||
&StmtResField{
|
||||
Field: "stringptr",
|
||||
Value: expr,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user