diff --git a/docs/language-guide.md b/docs/language-guide.md index d46710ae..4ebdbae5 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -262,6 +262,24 @@ Please note that at the moment, you must specify a full metaparams struct, since partial struct types are currently not supported in the language. Patches are welcome if you'd like to add this tricky feature! +##### Resource naming + +Each resource must have a unique name of type `str` that is used to uniquely +identify that resource, and can be used in the functioning of the resource at +that resources discretion. For example, the `file` resource uses the unique name +value to specify the path. + +Alternatively, the name value may be a list of strings `[]str` to build a list +of resources, each with a name from that list. When this is done, each resource +will use the same set of parameters. The list of internal edges specified in the +same resource block is created intelligently to have the appropriate edge for +each separate resource. + +Using this construct is a veiled form of looping (iteration). This technique is +one of many ways you can perform iterative tasks that you might have +traditionally used a `for` loop for instead. This is preferred, because flow +control is error-prone and can make for less readable code. + ##### Internal edges Resources may also declare edges internally. The edges may point to or from diff --git a/lang/interpret_test/TestAstFunc1/empty-res-list-0.graph b/lang/interpret_test/TestAstFunc1/empty-res-list-0.graph new file mode 100644 index 00000000..d368c5f9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/empty-res-list-0.graph @@ -0,0 +1,12 @@ +Edge: list(str(hey)) -> var(names) # names +Edge: str(hello) -> list(str(hello), str(world)) # 0 +Edge: str(hey) -> list(str(hey)) # 0 +Edge: str(world) -> list(str(hello), str(world)) # 1 +Vertex: list() +Vertex: list(str(hello), str(world)) +Vertex: list(str(hey)) +Vertex: str(hello) +Vertex: str(hey) +Vertex: str(name) +Vertex: str(world) +Vertex: var(names) diff --git a/lang/interpret_test/TestAstFunc1/empty-res-list-0/main.mcl b/lang/interpret_test/TestAstFunc1/empty-res-list-0/main.mcl new file mode 100644 index 00000000..469c57ef --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/empty-res-list-0/main.mcl @@ -0,0 +1,13 @@ +# this is an empty list of test resources, iow test resources +# this must pass type unification +test [] {} + +# single resource +test "name" {} + +# single resource, defined by list variable +$names = ["hey",] +test $names {} + +# multiples resources, defined by list +test ["hello", "world",] {} diff --git a/lang/structs.go b/lang/structs.go index 8d76c0f2..3e705a9a 100644 --- a/lang/structs.go +++ b/lang/structs.go @@ -150,10 +150,12 @@ func (obj *StmtBind) Output() (*interfaces.Output, error) { return interfaces.EmptyOutput(), nil } -// 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{}. +// StmtRes is a representation of a resource and possibly some edges. The `Name` +// value can be a single string or a list of strings. The former will produce a +// single resource, the latter produces a list of resources. Using this list +// mechanism is a safe alternative to traditional flow control like `for` loops. +// TODO: Consider expanding Name to have this return a list of Res's in the +// Output function if it is a map[name]struct{}, or even a map[[]name]struct{}. type StmtRes struct { data *interfaces.Data @@ -250,11 +252,24 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) { } invariants = append(invariants, invars...) - // name must be a string - invar := &unification.EqualsInvariant{ + // name must be a string or a list + ors := []interfaces.Invariant{} + + invarStr := &unification.EqualsInvariant{ Expr: obj.Name, Type: types.TypeStr, } + ors = append(ors, invarStr) + + invarListStr := &unification.EqualsInvariant{ + Expr: obj.Name, + Type: types.NewType("[]str"), + } + ors = append(ors, invarListStr) + + invar := &unification.ExclusiveInvariant{ + Invariants: ors, // one and only one of these should be true + } invariants = append(invariants, invar) // collect all the invariants of each field and edge @@ -311,27 +326,48 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) { if err != nil { return nil, err } - // TODO: test for []str instead, and loop - name := nameValue.Str() // must not panic - res, err := obj.resource(name) - if err != nil { - return nil, errwrap.Wrapf(err, "error building resource") + names := []string{} // list of names to build + switch { + case types.TypeStr.Cmp(nameValue.Type()) == nil: + name := nameValue.Str() // must not panic + names = append(names, name) + + case types.NewType("[]str").Cmp(nameValue.Type()) == nil: + for _, x := range nameValue.List() { // must not panic + name := x.Str() // must not panic + names = append(names, name) + } + + default: + // programming error + return nil, fmt.Errorf("unhandled resource name type: %+v", nameValue.Type()) } - edges, err := obj.edges(name) - if err != nil { - return nil, errwrap.Wrapf(err, "error building edges") - } + resources := []engine.Res{} + edges := []*interfaces.Edge{} + for _, name := range names { + res, err := obj.resource(name) + if err != nil { + return nil, errwrap.Wrapf(err, "error building resource") + } - metaparams, err := obj.metaparams() - if err != nil { - return nil, errwrap.Wrapf(err, "error building meta params") + edgeList, err := obj.edges(name) + if err != nil { + return nil, errwrap.Wrapf(err, "error building edges") + } + edges = append(edges, edgeList...) + + metaparams, err := obj.metaparams() + if err != nil { + return nil, errwrap.Wrapf(err, "error building meta params") + } + res.SetMetaParams(metaparams) + resources = append(resources, res) } - res.SetMetaParams(metaparams) return &interfaces.Output{ - Resources: []engine.Res{res}, + Resources: resources, Edges: edges, }, nil }