lang: Add a resource-specific elvis operator
This allows you to omit a resource parameter programmatically, and avoids the need of an `undef` or `nil` in our language, which would contribute to programming errors, crashes, and overall reduced safety.
This commit is contained in:
@@ -90,7 +90,7 @@ There are a very small number of statements in our language. They include:
|
|||||||
- **if**: produces up to one branch of statements based on a conditional
|
- **if**: produces up to one branch of statements based on a conditional
|
||||||
expression
|
expression
|
||||||
|
|
||||||
```
|
```mcl
|
||||||
if <conditional> {
|
if <conditional> {
|
||||||
<statements>
|
<statements>
|
||||||
} else {
|
} else {
|
||||||
@@ -101,7 +101,7 @@ expression
|
|||||||
|
|
||||||
- **resource**: produces a resource
|
- **resource**: produces a resource
|
||||||
|
|
||||||
```
|
```mcl
|
||||||
file "/tmp/hello" {
|
file "/tmp/hello" {
|
||||||
content => "world",
|
content => "world",
|
||||||
mode => "o=rwx",
|
mode => "o=rwx",
|
||||||
@@ -110,7 +110,7 @@ expression
|
|||||||
|
|
||||||
- **edge**: produces an edge
|
- **edge**: produces an edge
|
||||||
|
|
||||||
```
|
```mcl
|
||||||
File["/tmp/hello"] -> Print["alert4"]
|
File["/tmp/hello"] -> Print["alert4"]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -131,14 +131,44 @@ This section needs better documentation.
|
|||||||
|
|
||||||
#### Resource
|
#### Resource
|
||||||
|
|
||||||
This section needs better documentation.
|
Resources express the idempotent workloads that we want to have apply on our
|
||||||
|
system. They correspond to vertices in a [graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph)
|
||||||
|
which represent the order in which their declared state is applied. You will
|
||||||
|
usually want to pass in a number of parameters and associated values to the
|
||||||
|
resource to control how it behaves. For example, setting the `content` parameter
|
||||||
|
of a `file` resource to the string `hello`, will cause the contents of that file
|
||||||
|
to contain the string `hello` after it has run.
|
||||||
|
|
||||||
|
For some parameters, there is a distinction between an unspecified parameter,
|
||||||
|
and a parameter with a `zero` value. For example, for the file resource, you
|
||||||
|
might choose to set the `content` parameter to be the empty string, which would
|
||||||
|
ensure that the file has a length of zero. Alternatively you might wish to not
|
||||||
|
specify the file contents at all, which would leave that property undefined. If
|
||||||
|
you omit listing a property, then it will be undefined. To control this property
|
||||||
|
programmatically, you need to specify an `is-defined` value, as well as the
|
||||||
|
value to use if that boolean is true. You can do this with the resource-specific
|
||||||
|
`elvis` operator.
|
||||||
|
|
||||||
|
```mcl
|
||||||
|
$b = true # change me to false and then try editing the file manually
|
||||||
|
file "/tmp/mgmt-elvis" {
|
||||||
|
content => $b ?: "hello world\n",
|
||||||
|
state => "exists",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example is static, however you can imagine that the `$b` value might be
|
||||||
|
chosen in a programmatic way, even one in which that value varies over time. If
|
||||||
|
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.
|
||||||
|
|
||||||
#### Edge
|
#### Edge
|
||||||
|
|
||||||
Edges express dependencies in the graph of resources which are output. They can
|
Edges express dependencies in the graph of resources which are output. They can
|
||||||
be chained as a pair, or in any greater number. For example, you may write:
|
be chained as a pair, or in any greater number. For example, you may write:
|
||||||
|
|
||||||
```
|
```mcl
|
||||||
Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"]
|
Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
5
examples/lang/elvis0.mcl
Normal file
5
examples/lang/elvis0.mcl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
$b = true # change me to false and then try editing the file manually
|
||||||
|
file "/tmp/mgmt-elvis" {
|
||||||
|
content => $b ?: "hello world\n",
|
||||||
|
state => "exists",
|
||||||
|
}
|
||||||
@@ -39,6 +39,11 @@
|
|||||||
lval.str = yylex.Text()
|
lval.str = yylex.Text()
|
||||||
return ELSE
|
return ELSE
|
||||||
}
|
}
|
||||||
|
/\?:/ {
|
||||||
|
yylex.pos(lval) // our pos
|
||||||
|
lval.str = yylex.Text()
|
||||||
|
return ELVIS
|
||||||
|
}
|
||||||
/=>/ {
|
/=>/ {
|
||||||
yylex.pos(lval) // our pos
|
yylex.pos(lval) // our pos
|
||||||
lval.str = yylex.Text()
|
lval.str = yylex.Text()
|
||||||
|
|||||||
@@ -122,6 +122,20 @@ func TestLexParse0(t *testing.T) {
|
|||||||
//exp: ???, // FIXME: add the expected AST
|
//exp: ???, // FIXME: add the expected AST
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
values = append(values, test{
|
||||||
|
name: "one res with elvis",
|
||||||
|
code: `
|
||||||
|
test "t1" {
|
||||||
|
int16 => true ?: 42, # elvis operator
|
||||||
|
int32 => 42,
|
||||||
|
stringptr => false ?: "", # missing is not ""
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fail: false,
|
||||||
|
//exp: ???, // FIXME: add the expected AST
|
||||||
|
})
|
||||||
|
}
|
||||||
{
|
{
|
||||||
// TODO: skip trailing comma requirement on one-liners
|
// TODO: skip trailing comma requirement on one-liners
|
||||||
values = append(values, test{
|
values = append(values, test{
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func init() {
|
|||||||
%token STRING BOOL INTEGER FLOAT
|
%token STRING BOOL INTEGER FLOAT
|
||||||
%token EQUALS
|
%token EQUALS
|
||||||
%token COMMA COLON SEMICOLON
|
%token COMMA COLON SEMICOLON
|
||||||
%token ROCKET ARROW DOT
|
%token ELVIS ROCKET ARROW DOT
|
||||||
%token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER
|
%token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER
|
||||||
%token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER
|
%token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER
|
||||||
%token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER
|
%token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER
|
||||||
@@ -682,6 +682,11 @@ resource_body:
|
|||||||
posLast(yylex, yyDollar) // our pos
|
posLast(yylex, yyDollar) // our pos
|
||||||
$$.resFields = append($1.resFields, $2.resField)
|
$$.resFields = append($1.resFields, $2.resField)
|
||||||
}
|
}
|
||||||
|
| resource_body conditional_resource_field
|
||||||
|
{
|
||||||
|
posLast(yylex, yyDollar) // our pos
|
||||||
|
$$.resFields = append($1.resFields, $2.resField)
|
||||||
|
}
|
||||||
;
|
;
|
||||||
resource_field:
|
resource_field:
|
||||||
IDENTIFIER ROCKET expr COMMA
|
IDENTIFIER ROCKET expr COMMA
|
||||||
@@ -693,6 +698,18 @@ resource_field:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
conditional_resource_field:
|
||||||
|
// content => $present ?: "hello",
|
||||||
|
IDENTIFIER ROCKET expr ELVIS expr COMMA
|
||||||
|
{
|
||||||
|
posLast(yylex, yyDollar) // our pos
|
||||||
|
$$.resField = &StmtResField{
|
||||||
|
Field: $1.str,
|
||||||
|
Value: $5.expr,
|
||||||
|
Condition: $3.expr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
edge:
|
edge:
|
||||||
// TODO: we could technically prevent single edge_half pieces from being
|
// TODO: we could technically prevent single edge_half pieces from being
|
||||||
// parsed, but it's probably more work than is necessary...
|
// parsed, but it's probably more work than is necessary...
|
||||||
|
|||||||
@@ -120,9 +120,17 @@ func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 := &StmtResField{
|
||||||
Field: x.Field,
|
Field: x.Field,
|
||||||
Value: interpolated,
|
Value: interpolated,
|
||||||
|
Condition: condition,
|
||||||
}
|
}
|
||||||
fields = append(fields, field)
|
fields = append(fields, field)
|
||||||
}
|
}
|
||||||
@@ -143,6 +151,11 @@ func (obj *StmtRes) SetScope(scope *interfaces.Scope) error {
|
|||||||
if err := x.Value.SetScope(scope); err != nil {
|
if err := x.Value.SetScope(scope); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if x.Condition != nil {
|
||||||
|
if err := x.Condition.SetScope(scope); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -173,6 +186,22 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
invariants = append(invariants, invars...)
|
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)
|
typMap, err := resources.LangFieldNameToStructType(obj.Kind)
|
||||||
@@ -230,6 +259,14 @@ func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
graph.AddGraph(g)
|
graph.AddGraph(g)
|
||||||
|
|
||||||
|
if x.Condition != nil {
|
||||||
|
g, err := x.Condition.Graph()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
graph.AddGraph(g)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return graph, nil
|
return graph, nil
|
||||||
@@ -266,6 +303,17 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
|
|||||||
|
|
||||||
// FIXME: we could probably simplify this code...
|
// FIXME: we could probably simplify this code...
|
||||||
for _, x := range obj.Fields {
|
for _, x := range obj.Fields {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typ, err := x.Value.Type()
|
typ, err := x.Value.Type()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errwrap.Wrapf(err, "resource field `%s` did not return a type", x.Field)
|
return nil, errwrap.Wrapf(err, "resource field `%s` did not return a type", x.Field)
|
||||||
@@ -372,6 +420,7 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
|
|||||||
type StmtResField struct {
|
type StmtResField struct {
|
||||||
Field string
|
Field string
|
||||||
Value interfaces.Expr
|
Value interfaces.Expr
|
||||||
|
Condition interfaces.Expr // the value will be used if nil or true
|
||||||
}
|
}
|
||||||
|
|
||||||
// StmtEdge is a representation of a dependency. It also supports send/recv.
|
// StmtEdge is a representation of a dependency. It also supports send/recv.
|
||||||
|
|||||||
Reference in New Issue
Block a user