From 08a809089e579b2dd196acae0a4c9ccc8b46676a Mon Sep 17 00:00:00 2001 From: Claire Gaestel <213631+nyobe@users.noreply.github.com> Date: Fri, 14 Feb 2025 22:31:07 -0800 Subject: [PATCH 1/2] WIP --- ast/expr.go | 24 + eval/eval.go | 12 + eval/eval_test.go | 3 + eval/expr.go | 24 + eval/testdata/eval/late-binding/env.yaml | 3 + .../testdata/eval/late-binding/example/a.yaml | 4 + .../testdata/eval/late-binding/example/b.yaml | 4 + .../eval/late-binding/example/defn.yaml | 3 + eval/testdata/eval/late-binding/expected.json | 1202 +++++++++++++++++ 9 files changed, 1279 insertions(+) create mode 100644 eval/testdata/eval/late-binding/env.yaml create mode 100644 eval/testdata/eval/late-binding/example/a.yaml create mode 100644 eval/testdata/eval/late-binding/example/b.yaml create mode 100644 eval/testdata/eval/late-binding/example/defn.yaml create mode 100644 eval/testdata/eval/late-binding/expected.json diff --git a/ast/expr.go b/ast/expr.go index 536c0da6..773046fc 100644 --- a/ast/expr.go +++ b/ast/expr.go @@ -718,6 +718,18 @@ func Validate(schemaExpr, valueExpr Expr) *ValidateExpr { } } +type TemplateExpr struct { + builtinNode + + //TemplateDef Expr +} + +type EvalExpr struct { + builtinNode + + //TemplateValue Expr +} + func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool) { var diags syntax.Diagnostics if node.Len() != 1 { @@ -760,6 +772,18 @@ func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool) parse = parseToJSON case "fn::toString": parse = parseToString + case "fn::template": + parse = func(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) { + return &TemplateExpr{ + builtinNode: builtin(node, name, args), + }, nil + } + case "fn::eval": + parse = func(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) { + return &EvalExpr{ + builtinNode: builtin(node, name, args), + }, nil + } default: if strings.HasPrefix(kvp.Key.Value(), "fn::open::") { parse = parseShortOpen diff --git a/eval/eval.go b/eval/eval.go index 7328cecc..e90cfa62 100644 --- a/eval/eval.go +++ b/eval/eval.go @@ -397,6 +397,12 @@ func declare[Expr exprNode](e *evalContext, path string, x Expr, base *value) *e case *ast.ToStringExpr: repr := &toStringExpr{node: x, value: declare(e, "", x.Value, nil)} return newExpr(path, repr, schema.String().Schema(), base) + case *ast.TemplateExpr: + repr := &templateExpr{node: x, template: declare(e, "", x.Args(), nil)} + return newExpr(path, repr, schema.Never().Schema(), base) + case *ast.EvalExpr: + repr := &evalExpr{node: x, value: declare(e, "", x.Args(), base)} + return newExpr(path, repr, schema.Always().Schema(), base) case *ast.ArrayExpr: elements := make([]*expr, len(x.Elements)) for i, x := range x.Elements { @@ -648,6 +654,12 @@ func (e *evalContext) evaluateExpr(x *expr, accept *schema.Schema) *value { val = e.evaluateBuiltinToJSON(x, repr) case *toStringExpr: val = e.evaluateBuiltinToString(x, repr) + case *templateExpr: + val = &value{def: x, schema: x.schema, repr: repr, unknown: true} // templates defer evaluation + case *evalExpr: + defn := e.evaluateExpr(repr.value, schema.Always()) // deref template expr + expr := declare(e, "", defn.repr.(*templateExpr).node.Args(), nil) // clone its ast node into a new expr + val = e.evaluateExpr(expr, schema.Always()) // evaluate it case *arrayExpr: val = e.evaluateArray(x, repr, accept) case *objectExpr: diff --git a/eval/eval_test.go b/eval/eval_test.go index 13dd682f..97813b45 100644 --- a/eval/eval_test.go +++ b/eval/eval_test.go @@ -364,6 +364,9 @@ func TestEval(t *testing.T) { path := filepath.Join("testdata", "eval") entries, err := os.ReadDir(path) + entries = slices.DeleteFunc(entries, func(entry os.DirEntry) bool { + return entry.Name() != "late-binding" + }) require.NoError(t, err) for _, e := range entries { if e.Name() == "bench" { diff --git a/eval/expr.go b/eval/expr.go index 79ae7488..045c6e36 100644 --- a/eval/expr.go +++ b/eval/expr.go @@ -344,6 +344,10 @@ func (x *expr) export(environment string) esc.Expr { for k, v := range repr.properties { ex.Object[k] = v.export(environment) } + case *templateExpr: + // not evaluated + case *evalExpr: + return repr.value.export(environment) default: panic(fmt.Sprintf("fatal: invalid expr type %T", repr)) } @@ -599,3 +603,23 @@ type validateExpr struct { func (x *validateExpr) syntax() ast.Expr { return x.node } + +type templateExpr struct { + node *ast.TemplateExpr + + template *expr +} + +func (x *templateExpr) syntax() ast.Expr { + return x.node +} + +type evalExpr struct { + node *ast.EvalExpr + + value *expr +} + +func (x *evalExpr) syntax() ast.Expr { + return x.node +} diff --git a/eval/testdata/eval/late-binding/env.yaml b/eval/testdata/eval/late-binding/env.yaml new file mode 100644 index 00000000..6653f67d --- /dev/null +++ b/eval/testdata/eval/late-binding/env.yaml @@ -0,0 +1,3 @@ +values: + a: ${environments.example.a} + b: ${environments.example.b} \ No newline at end of file diff --git a/eval/testdata/eval/late-binding/example/a.yaml b/eval/testdata/eval/late-binding/example/a.yaml new file mode 100644 index 00000000..382c7ec7 --- /dev/null +++ b/eval/testdata/eval/late-binding/example/a.yaml @@ -0,0 +1,4 @@ +values: + name: "foo" + example: + fn::eval: ${environments.example.defn.hello} diff --git a/eval/testdata/eval/late-binding/example/b.yaml b/eval/testdata/eval/late-binding/example/b.yaml new file mode 100644 index 00000000..db4021ab --- /dev/null +++ b/eval/testdata/eval/late-binding/example/b.yaml @@ -0,0 +1,4 @@ +values: + name: "bar" + example: + fn::eval: ${environments.example.defn.hello} diff --git a/eval/testdata/eval/late-binding/example/defn.yaml b/eval/testdata/eval/late-binding/example/defn.yaml new file mode 100644 index 00000000..18cac554 --- /dev/null +++ b/eval/testdata/eval/late-binding/example/defn.yaml @@ -0,0 +1,3 @@ +values: + hello: + fn::template: Hello ${name}! diff --git a/eval/testdata/eval/late-binding/expected.json b/eval/testdata/eval/late-binding/expected.json new file mode 100644 index 00000000..f29180af --- /dev/null +++ b/eval/testdata/eval/late-binding/expected.json @@ -0,0 +1,1202 @@ +{ + "check": { + "exprs": { + "a": { + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + }, + "schema": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "foo" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + }, + "symbol": [ + { + "key": "environments", + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 8, + "byte": 15 + }, + "end": { + "line": 2, + "column": 20, + "byte": 27 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + } + }, + { + "key": "example", + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 20, + "byte": 27 + }, + "end": { + "line": 2, + "column": 28, + "byte": 35 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + } + }, + { + "key": "a", + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 28, + "byte": 35 + }, + "end": { + "line": 2, + "column": 30, + "byte": 37 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + ] + }, + "b": { + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + }, + "schema": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "bar" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + }, + "symbol": [ + { + "key": "environments", + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 8, + "byte": 2 + }, + "end": { + "line": 3, + "column": 20, + "byte": 14 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + } + }, + { + "key": "example", + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 20, + "byte": 14 + }, + "end": { + "line": 3, + "column": 28, + "byte": 22 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + } + }, + { + "key": "b", + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 28, + "byte": 22 + }, + "end": { + "line": 3, + "column": 30, + "byte": 24 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + ] + } + }, + "properties": { + "a": { + "value": { + "example": { + "value": "Hello foo!", + "trace": { + "def": { + "environment": "example/defn", + "begin": { + "line": 3, + "column": 19, + "byte": 35 + }, + "end": { + "line": 3, + "column": 33, + "byte": 49 + } + } + } + }, + "name": { + "value": "foo", + "trace": { + "def": { + "environment": "example/a", + "begin": { + "line": 2, + "column": 9, + "byte": 16 + }, + "end": { + "line": 2, + "column": 12, + "byte": 19 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + } + } + }, + "b": { + "value": { + "example": { + "value": "Hello bar!", + "trace": { + "def": { + "environment": "example/defn", + "begin": { + "line": 3, + "column": 19, + "byte": 35 + }, + "end": { + "line": 3, + "column": 33, + "byte": 49 + } + } + } + }, + "name": { + "value": "bar", + "trace": { + "def": { + "environment": "example/b", + "begin": { + "line": 2, + "column": 9, + "byte": 16 + }, + "end": { + "line": 2, + "column": 12, + "byte": 19 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + } + } + } + }, + "schema": { + "properties": { + "a": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "foo" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + }, + "b": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "bar" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + } + }, + "type": "object", + "required": [ + "a", + "b" + ] + }, + "executionContext": { + "properties": { + "currentEnvironment": { + "value": { + "name": { + "value": "late-binding", + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + }, + "pulumi": { + "value": { + "user": { + "value": { + "id": { + "value": "USER_123", + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + }, + "rootEnvironment": { + "value": { + "name": { + "value": "late-binding", + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "schema": { + "properties": { + "currentEnvironment": { + "properties": { + "name": { + "type": "string", + "const": "late-binding" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "pulumi": { + "properties": { + "user": { + "properties": { + "id": { + "type": "string", + "const": "USER_123" + } + }, + "type": "object", + "required": [ + "id" + ] + } + }, + "type": "object", + "required": [ + "user" + ] + }, + "rootEnvironment": { + "properties": { + "name": { + "type": "string", + "const": "late-binding" + } + }, + "type": "object", + "required": [ + "name" + ] + } + }, + "type": "object", + "required": [ + "currentEnvironment", + "pulumi", + "rootEnvironment" + ] + } + } + }, + "checkJson": { + "a": { + "example": "Hello foo!", + "name": "foo" + }, + "b": { + "example": "Hello bar!", + "name": "bar" + } + }, + "eval": { + "exprs": { + "a": { + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + }, + "schema": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "foo" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + }, + "symbol": [ + { + "key": "environments", + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 8, + "byte": 15 + }, + "end": { + "line": 2, + "column": 20, + "byte": 27 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + } + }, + { + "key": "example", + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 20, + "byte": 27 + }, + "end": { + "line": 2, + "column": 28, + "byte": 35 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + } + }, + { + "key": "a", + "range": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 28, + "byte": 35 + }, + "end": { + "line": 2, + "column": 30, + "byte": 37 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + ] + }, + "b": { + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + }, + "schema": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "bar" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + }, + "symbol": [ + { + "key": "environments", + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 8, + "byte": 2 + }, + "end": { + "line": 3, + "column": 20, + "byte": 14 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + } + }, + { + "key": "example", + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 20, + "byte": 14 + }, + "end": { + "line": 3, + "column": 28, + "byte": 22 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + } + }, + { + "key": "b", + "range": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 28, + "byte": 22 + }, + "end": { + "line": 3, + "column": 30, + "byte": 24 + } + }, + "value": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + ] + } + }, + "properties": { + "a": { + "value": { + "example": { + "value": "Hello foo!", + "trace": { + "def": { + "environment": "example/defn", + "begin": { + "line": 3, + "column": 19, + "byte": 35 + }, + "end": { + "line": 3, + "column": 33, + "byte": 49 + } + } + } + }, + "name": { + "value": "foo", + "trace": { + "def": { + "environment": "example/a", + "begin": { + "line": 2, + "column": 9, + "byte": 16 + }, + "end": { + "line": 2, + "column": 12, + "byte": 19 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 2, + "column": 6, + "byte": 13 + }, + "end": { + "line": 2, + "column": 31, + "byte": 38 + } + } + } + }, + "b": { + "value": { + "example": { + "value": "Hello bar!", + "trace": { + "def": { + "environment": "example/defn", + "begin": { + "line": 3, + "column": 19, + "byte": 35 + }, + "end": { + "line": 3, + "column": 33, + "byte": 49 + } + } + } + }, + "name": { + "value": "bar", + "trace": { + "def": { + "environment": "example/b", + "begin": { + "line": 2, + "column": 9, + "byte": 16 + }, + "end": { + "line": 2, + "column": 12, + "byte": 19 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 3, + "column": 6, + "byte": 0 + }, + "end": { + "line": 3, + "column": 31, + "byte": 0 + } + } + } + } + }, + "schema": { + "properties": { + "a": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "foo" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + }, + "b": { + "properties": { + "example": { + "type": "string" + }, + "name": { + "type": "string", + "const": "bar" + } + }, + "type": "object", + "required": [ + "example", + "name" + ] + } + }, + "type": "object", + "required": [ + "a", + "b" + ] + }, + "executionContext": { + "properties": { + "currentEnvironment": { + "value": { + "name": { + "value": "late-binding", + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + }, + "pulumi": { + "value": { + "user": { + "value": { + "id": { + "value": "USER_123", + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + }, + "rootEnvironment": { + "value": { + "name": { + "value": "late-binding", + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "trace": { + "def": { + "environment": "late-binding", + "begin": { + "line": 0, + "column": 0, + "byte": 0 + }, + "end": { + "line": 0, + "column": 0, + "byte": 0 + } + } + } + } + }, + "schema": { + "properties": { + "currentEnvironment": { + "properties": { + "name": { + "type": "string", + "const": "late-binding" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "pulumi": { + "properties": { + "user": { + "properties": { + "id": { + "type": "string", + "const": "USER_123" + } + }, + "type": "object", + "required": [ + "id" + ] + } + }, + "type": "object", + "required": [ + "user" + ] + }, + "rootEnvironment": { + "properties": { + "name": { + "type": "string", + "const": "late-binding" + } + }, + "type": "object", + "required": [ + "name" + ] + } + }, + "type": "object", + "required": [ + "currentEnvironment", + "pulumi", + "rootEnvironment" + ] + } + } + }, + "evalJsonRedacted": { + "a": { + "example": "Hello foo!", + "name": "foo" + }, + "b": { + "example": "Hello bar!", + "name": "bar" + } + }, + "evalJSONRevealed": { + "a": { + "example": "Hello foo!", + "name": "foo" + }, + "b": { + "example": "Hello bar!", + "name": "bar" + } + } +} From 74552708a2581212442eb2969b6ce9da9356020f Mon Sep 17 00:00:00 2001 From: Claire Gaestel <213631+nyobe@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:01:28 -0800 Subject: [PATCH 2/2] WIP --- ast/expr.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ast/expr.go b/ast/expr.go index 773046fc..2ab6770b 100644 --- a/ast/expr.go +++ b/ast/expr.go @@ -728,6 +728,8 @@ type EvalExpr struct { builtinNode //TemplateValue Expr + // either a symbol or a template expr? + // or just allow anything but behave as pass-thru for non-templates? } func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool) {