Skip to content

Commit

Permalink
Support for typename import aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
TristonianJones committed Aug 2, 2024
1 parent 3545aac commit d62ef29
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 17 deletions.
34 changes: 27 additions & 7 deletions policy/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,36 @@ func CompileRule(env *cel.Env, p *Policy, opts ...CompilerOption) (*CompiledRule
src: p.Source(),
maxNestedExpressions: defaultMaxNestedExpressions,
}
var err error
errs := common.NewErrors(c.src)
iss := cel.NewIssuesWithSourceInfo(errs, c.info)
for _, o := range opts {
if err := o(c); err != nil {
if err = o(c); err != nil {
iss.ReportErrorAtID(p.Name().ID, "error configuring compiler option: %s", err)
return nil, iss
}
}
rule, ruleIss := c.compileRule(p.Rule(), c.env, iss)
iss = iss.Append(ruleIss)
return rule, iss
c.env, err = c.env.Extend(cel.EagerlyValidateDeclarations(true))
if err != nil {
iss.ReportErrorAtID(p.Name().ID, "error configuring environment: %s", err)
return nil, iss
}

importCount := len(p.Imports())
if importCount > 0 {
importNames := make([]string, importCount)
for i, imp := range p.Imports() {
typeName := imp.Name().Value
importNames[i] = typeName
}
env, err := c.env.Extend(cel.Abbrevs(importNames...))
if err != nil {
iss.ReportErrorAtID(p.Imports()[0].Name().ID, "error configuring imports: %s", err)
} else {
c.env = env
}
}
return c.compileRule(p.Rule(), c.env, iss)
}

type compiler struct {
Expand All @@ -234,7 +253,6 @@ type compiler struct {
}

func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*CompiledRule, *cel.Issues) {
var err error
compiledVars := make([]*CompiledVariable, len(r.Variables()))
for i, v := range r.Variables() {
exprSrc := c.relSource(v.Expression())
Expand All @@ -254,9 +272,11 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*Com
// Introduce the variable into the environment. By extending the environment, the variables
// are effectively scoped such that they must be declared before use.
varDecl := decls.NewVariable(fmt.Sprintf("%s.%s", variablePrefix, varName), varType)
ruleEnv, err = ruleEnv.Extend(cel.Variable(varDecl.Name(), varDecl.Type()))
varEnv, err := ruleEnv.Extend(cel.Variable(varDecl.Name(), varDecl.Type()))
if err != nil {
iss.ReportErrorAtID(v.Expression().ID, "invalid variable declaration")
iss.ReportErrorAtID(v.exprID, "invalid variable declaration: %s", err.Error())
} else {
ruleEnv = varEnv
}
compiledVar := &CompiledVariable{
exprID: v.name.ID,
Expand Down
28 changes: 19 additions & 9 deletions policy/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,13 @@ var (
},
{
name: "pb",
expr: `(spec.single_int32 > 10)
? optional.of("invalid spec, got single_int32=%d, wanted <= 10".format([spec.single_int32]))
: optional.none()`,
expr: `
(spec.single_int32 > google.expr.proto3.test.TestAllTypes{single_int64: 10}.single_int64)
? optional.of("invalid spec, got single_int32=%d, wanted <= 10".format([spec.single_int32]))
: ((spec.standalone_enum == google.expr.proto3.test.TestAllTypes.NestedEnum.BAR ||
google.expr.proto3.test.ImportedGlobalEnum.IMPORT_BAR in spec.imported_enums)
? optional.of("invalid spec, neither nested nor imported enums may refer to BAR or IMPORT_BAR")
: optional.none())`,
envOpts: []cel.EnvOption{
cel.Types(&proto3pb.TestAllTypes{}),
},
Expand Down Expand Up @@ -168,22 +172,28 @@ var (
}{
{
name: "errors",
err: `ERROR: testdata/errors/policy.yaml:19:19: undeclared reference to 'spec' (in container '')
err: `ERROR: testdata/errors/policy.yaml:17:12: error configuring imports: invalid qualified name: bad import, wanted name of the form 'qualified.name'
| - name: "bad import"
| ...........^
ERROR: testdata/errors/policy.yaml:21:19: undeclared reference to 'spec' (in container '')
| expression: spec.labels
| ..................^
ERROR: testdata/errors/policy.yaml:21:50: Syntax error: mismatched input 'resource' expecting ')'
ERROR: testdata/errors/policy.yaml:22:7: invalid variable declaration: overlapping identifier for name 'variables.want'
| - name: want
| ......^
ERROR: testdata/errors/policy.yaml:25:50: Syntax error: mismatched input 'resource' expecting ')'
| expression: variables.want.filter(l, !(lin resource.labels))
| .................................................^
ERROR: testdata/errors/policy.yaml:21:66: Syntax error: extraneous input ')' expecting <EOF>
ERROR: testdata/errors/policy.yaml:25:66: Syntax error: extraneous input ')' expecting <EOF>
| expression: variables.want.filter(l, !(lin resource.labels))
| .................................................................^
ERROR: testdata/errors/policy.yaml:23:27: Syntax error: mismatched input '2' expecting {'}', ','}
ERROR: testdata/errors/policy.yaml:27:27: Syntax error: mismatched input '2' expecting {'}', ','}
| expression: "{1:305 2:569}"
| ..........................^
ERROR: testdata/errors/policy.yaml:31:75: Syntax error: extraneous input ']' expecting ')'
ERROR: testdata/errors/policy.yaml:35:75: Syntax error: extraneous input ']' expecting ')'
| "missing one or more required labels: %s".format(variables.missing])
| ..........................................................................^
ERROR: testdata/errors/policy.yaml:34:67: undeclared reference to 'format' (in container '')
ERROR: testdata/errors/policy.yaml:38:67: undeclared reference to 'format' (in container '')
| "invalid values provided on one or more labels: %s".format([variables.invalid])
| ..................................................................^
ERROR: testdata/errors/policy.yaml:38:16: incompatible output types: bool not assignable to string
Expand Down
61 changes: 61 additions & 0 deletions policy/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ func NewPolicy(src *Source, info *ast.SourceInfo) *Policy {
source: src,
info: info,
semantic: firstMatch,
imports: []*Import{},
}
}

// Policy declares a name, rule, and evaluation semantic for a given expression graph.
type Policy struct {
name ValueString
imports []*Import
rule *Rule
semantic semanticType
info *ast.SourceInfo
Expand All @@ -63,6 +65,10 @@ func (p *Policy) SourceInfo() *ast.SourceInfo {
return p.info
}

func (p *Policy) Imports() []*Import {
return p.imports
}

// Name returns the name of the policy.
func (p *Policy) Name() ValueString {
return p.name
Expand All @@ -88,6 +94,10 @@ func (p *Policy) MetadataKeys() []string {
return keys
}

func (p *Policy) AddImport(name ValueString) {
p.imports = append(p.imports, &Import{name: name})
}

// SetName configures the policy name.
func (p *Policy) SetName(name ValueString) {
p.name = name
Expand Down Expand Up @@ -119,6 +129,22 @@ func (p *Policy) GetExplanationOutputPolicy() *Policy {
return &ep
}

func NewImport() *Import {
return &Import{}
}

type Import struct {
name ValueString
}

func (i *Import) Name() ValueString {
return i.name
}

func (i *Import) SetName(name ValueString) {
i.name = name
}

// NewRule creates a Rule instance.
func NewRule(exprID int64) *Rule {
return &Rule{
Expand Down Expand Up @@ -582,6 +608,8 @@ func (p *parserImpl) ParsePolicy(ctx ParserContext, node *yaml.Node) *Policy {
fieldName := key.Value
val := node.Content[i+1]
switch fieldName {
case "imports":
p.parseImports(ctx, policy, val)
case "name":
policy.SetName(ctx.NewString(val))
case "rule":
Expand All @@ -593,6 +621,39 @@ func (p *parserImpl) ParsePolicy(ctx ParserContext, node *yaml.Node) *Policy {
return policy
}

func (p *parserImpl) parseImports(ctx ParserContext, policy *Policy, node *yaml.Node) {
id := ctx.CollectMetadata(node)
if p.assertYamlType(id, node, yamlList) == nil {
return
}
for _, val := range node.Content {
policy.AddImport(p.parseImport(ctx, policy, val).Name())
}
}

func (p *parserImpl) parseImport(ctx ParserContext, _ *Policy, node *yaml.Node) *Import {
id := ctx.CollectMetadata(node)
imp := NewImport()
if p.assertYamlType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) {
return imp
}
for i := 0; i < len(node.Content); i += 2 {
key := node.Content[i]
ctx.CollectMetadata(key)
fieldName := key.Value
val := node.Content[i+1]
if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle {
val.Line++
val.Column = key.Column + 1
}
switch fieldName {
case "name":
imp.SetName(ctx.NewString(val))
}
}
return imp
}

// ParseRule will parse the current yaml node as though it is the entry point to a rule.
func (p *parserImpl) ParseRule(ctx ParserContext, policy *Policy, node *yaml.Node) *Rule {
r, id := ctx.NewRule(node)
Expand Down
4 changes: 4 additions & 0 deletions policy/testdata/errors/policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@
# limitations under the License.

name: "errors"
imports:
- name: "bad import"
rule:
variables:
- name: want
expression: spec.labels
- name: want
expression: "2"
- name: missing
expression: variables.want.filter(l, !(lin resource.labels))
- name: bad_data
Expand Down
13 changes: 12 additions & 1 deletion policy/testdata/pb/policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,19 @@
# limitations under the License.

name: "pb"

imports:
- name: google.expr.proto3.test.TestAllTypes.NestedEnum
- name: google.expr.proto3.test.ImportedGlobalEnum

rule:
match:
- condition: spec.single_int32 > 10
- condition: >
spec.single_int32 > TestAllTypes{single_int64: 10}.single_int64
output: |
"invalid spec, got single_int32=%d, wanted <= 10".format([spec.single_int32])
- condition: >
spec.standalone_enum == NestedEnum.BAR ||
ImportedGlobalEnum.IMPORT_BAR in spec.imported_enums
output: |
"invalid spec, neither nested nor imported enums may refer to BAR or IMPORT_BAR"

0 comments on commit d62ef29

Please sign in to comment.