Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Support for documentation and example strings in CEL environments
  • Loading branch information
TristonianJones committed Apr 8, 2025
commit d4210bed20f1c095d5c648b9469e6ee804469b63
27 changes: 24 additions & 3 deletions checker/decls/decls.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,29 +91,50 @@ func NewFunction(name string,
Overloads: overloads}}}
}

// NewFunctionWithDoc creates a named function declaration with a description and one or more overloads.
func NewFunctionWithDoc(name, desc string,
overloads ...*exprpb.Decl_FunctionDecl_Overload) *exprpb.Decl {
return &exprpb.Decl{
Name: name,
DeclKind: &exprpb.Decl_Function{
Function: &exprpb.Decl_FunctionDecl{
// Doc: desc,
Overloads: overloads}}}
}

// NewIdent creates a named identifier declaration with an optional literal
// value.
//
// Literal values are typically only associated with enum identifiers.
//
// Deprecated: Use NewVar or NewConst instead.
func NewIdent(name string, t *exprpb.Type, v *exprpb.Constant) *exprpb.Decl {
return newIdent(name, t, v, "")
}

func newIdent(name string, t *exprpb.Type, v *exprpb.Constant, desc string) *exprpb.Decl {
return &exprpb.Decl{
Name: name,
DeclKind: &exprpb.Decl_Ident{
Ident: &exprpb.Decl_IdentDecl{
Type: t,
Value: v}}}
Value: v,
Doc: desc}}}
}

// NewConst creates a constant identifier with a CEL constant literal value.
func NewConst(name string, t *exprpb.Type, v *exprpb.Constant) *exprpb.Decl {
return NewIdent(name, t, v)
return newIdent(name, t, v, "")
}

// NewVar creates a variable identifier.
func NewVar(name string, t *exprpb.Type) *exprpb.Decl {
return NewIdent(name, t, nil)
return newIdent(name, t, nil, "")
}

// NewVarWithDoc creates a variable identifier with a type and a description string.
func NewVarWithDoc(name string, t *exprpb.Type, desc string) *exprpb.Decl {
return newIdent(name, t, nil, desc)
}

// NewInstanceOverload creates a instance function overload contract.
Expand Down
2 changes: 2 additions & 0 deletions common/decls/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ go_library(
importpath = "github.com/google/cel-go/common/decls",
deps = [
"//checker/decls:go_default_library",
"//common:go_default_library",
"//common/functions:go_default_library",
"//common/operators:go_default_library",
"//common/types:go_default_library",
"//common/types/ref:go_default_library",
"//common/types/traits:go_default_library",
Expand Down
133 changes: 131 additions & 2 deletions common/decls/decls.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"strings"

chkdecls "github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/functions"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"

Expand Down Expand Up @@ -54,6 +56,7 @@ func NewFunction(name string, opts ...FunctionOpt) (*FunctionDecl, error) {
// overload instances.
type FunctionDecl struct {
name string
doc string

// overloads associated with the function name.
overloads map[string]*OverloadDecl
Expand Down Expand Up @@ -84,6 +87,23 @@ const (
declarationEnabled
)

// Documentation generates documentation about the Function and its overloads as a common.Doc object.
func (f *FunctionDecl) Documentation() *common.Doc {
children := make([]*common.Doc, len(f.OverloadDecls()))
for i, o := range f.OverloadDecls() {
var examples []*common.Doc
for _, ex := range o.Examples() {
examples = append(examples, common.NewExampleDoc(ex))
}
od := common.NewOverloadDoc(o.ID(), formatSignature(f.Name(), o), examples...)
children[i] = od
}
return common.NewFunctionDoc(
f.Name(),
f.Description(),
children...)
}

// Name returns the function name in human-readable terms, e.g. 'contains' of 'math.least'
func (f *FunctionDecl) Name() string {
if f == nil {
Expand All @@ -92,6 +112,16 @@ func (f *FunctionDecl) Name() string {
return f.name
}

// Description provides an overview of the function's purpose.
//
// Usage examples should be included on specific overloads.
func (f *FunctionDecl) Description() string {
if f == nil {
return ""
}
return f.doc
}

// IsDeclarationDisabled indicates that the function implementation should be added to the dispatcher, but the
// declaration should not be exposed for use in expressions.
func (f *FunctionDecl) IsDeclarationDisabled() bool {
Expand Down Expand Up @@ -202,6 +232,7 @@ func (f *FunctionDecl) Subset(selector OverloadSelector) *FunctionDecl {
}
subset := &FunctionDecl{
name: f.Name(),
doc: f.doc,
overloads: overloads,
singleton: f.singleton,
disableTypeGuards: f.disableTypeGuards,
Expand Down Expand Up @@ -361,6 +392,14 @@ func MaybeNoSuchOverload(funcName string, args ...ref.Val) ref.Val {
// FunctionOpt defines a functional option for mutating a function declaration.
type FunctionOpt func(*FunctionDecl) (*FunctionDecl, error)

// FunctionDocs configures documentation from a list of strings separated by newlines.
func FunctionDocs(docs ...string) FunctionOpt {
return func(fn *FunctionDecl) (*FunctionDecl, error) {
fn.doc = common.MultilineDescription(docs...)
return fn, nil
}
}

// DisableTypeGuards disables automatically generated function invocation guards on direct overload calls.
// Type guards remain on during dynamic dispatch for parsed-only expressions.
func DisableTypeGuards(value bool) FunctionOpt {
Expand Down Expand Up @@ -513,6 +552,7 @@ func newOverloadInternal(overloadID string,
// implementation.
type OverloadDecl struct {
id string
doc string
argTypes []*types.Type
resultType *types.Type
isMemberFunction bool
Expand All @@ -532,6 +572,11 @@ type OverloadDecl struct {
functionOp functions.FunctionOp
}

// Examples returns a list of string examples for the overload.
func (o *OverloadDecl) Examples() []string {
return common.ParseDescriptions(o.doc)
}

// ID mirrors the overload signature and provides a unique id which may be referenced within the type-checker
// and interpreter to optimize performance.
//
Expand Down Expand Up @@ -729,6 +774,14 @@ func matchOperandTrait(trait int, arg ref.Val) bool {
// OverloadOpt is a functional option for configuring a function overload.
type OverloadOpt func(*OverloadDecl) (*OverloadDecl, error)

// OverloadExamples configures example expressions for the overload.
func OverloadExamples(examples ...string) OverloadOpt {
return func(o *OverloadDecl) (*OverloadDecl, error) {
o.doc = common.MultilineDescription(examples...)
return o, nil
}
}

// UnaryBinding provides the implementation of a unary overload. The provided function is protected by a runtime
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
func UnaryBinding(binding functions.UnaryOp) OverloadOpt {
Expand Down Expand Up @@ -800,13 +853,23 @@ func NewVariable(name string, t *types.Type) *VariableDecl {
return &VariableDecl{name: name, varType: t}
}

// NewVariableWithDoc creates a new variable declaration with usage documentation.
func NewVariableWithDoc(name string, t *types.Type, doc string) *VariableDecl {
return &VariableDecl{name: name, varType: t, doc: doc}
}

// VariableDecl defines a variable declaration which may optionally have a constant value.
type VariableDecl struct {
name string
doc string
varType *types.Type
value ref.Val
}

func (v *VariableDecl) Documentation() *common.Doc {
return common.NewVariableDoc(v.Name(), describeCELType(v.Type()), v.Description())
}

// Name returns the fully-qualified variable name
func (v *VariableDecl) Name() string {
if v == nil {
Expand All @@ -815,6 +878,16 @@ func (v *VariableDecl) Name() string {
return v.name
}

// Description returns the usage documentation for the variable, if set.
//
// Good usage instructions provide information about the valid formats, ranges, sizes for the variable type.
func (v *VariableDecl) Description() string {
if v == nil {
return ""
}
return v.doc
}

// Type returns the types.Type value associated with the variable.
func (v *VariableDecl) Type() *types.Type {
if v == nil {
Expand Down Expand Up @@ -856,7 +929,7 @@ func variableDeclToExprDecl(v *VariableDecl) (*exprpb.Decl, error) {
if err != nil {
return nil, err
}
return chkdecls.NewVar(v.Name(), varType), nil
return chkdecls.NewVarWithDoc(v.Name(), varType, v.doc), nil
}

// FunctionDeclToExprDecl converts a go-native function declaration into a protobuf-typed function declaration.
Expand Down Expand Up @@ -890,6 +963,8 @@ func functionDeclToExprDecl(f *FunctionDecl) (*exprpb.Decl, error) {
} else {
overloads[i] = chkdecls.NewOverload(oID, argTypes, resultType)
}
desc := common.MultilineDescription(o.Examples()...)
overloads[i].Doc = desc
} else {
params := []string{}
for pn := range paramNames {
Expand All @@ -902,7 +977,7 @@ func functionDeclToExprDecl(f *FunctionDecl) (*exprpb.Decl, error) {
}
}
}
return chkdecls.NewFunction(f.Name(), overloads...), nil
return chkdecls.NewFunctionWithDoc(f.Name(), f.Description(), overloads...), nil
}

func collectParamNames(paramNames map[string]struct{}, arg *types.Type) {
Expand All @@ -914,6 +989,60 @@ func collectParamNames(paramNames map[string]struct{}, arg *types.Type) {
}
}

func formatSignature(fnName string, o *OverloadDecl) string {
if opName, isOperator := operators.FindReverse(fnName); isOperator {
if opName == "" {
opName = fnName
}
return formatOperator(opName, o)
}
return formatCall(fnName, o)
}

func formatOperator(opName string, o *OverloadDecl) string {
args := o.ArgTypes()
argTypes := make([]string, len(o.ArgTypes()))
for j, a := range args {
argTypes[j] = describeCELType(a)
}
ret := describeCELType(o.ResultType())
switch len(args) {
case 1:
return fmt.Sprintf("%s%s -> %s", opName, argTypes[0], ret)
case 2:
if opName == operators.Index {
return fmt.Sprintf("%s[%s] -> %s", argTypes[0], argTypes[1], ret)
}
return fmt.Sprintf("%s %s %s -> %s", argTypes[0], opName, argTypes[1], ret)
default:
if opName == operators.Conditional {
return fmt.Sprint("bool ? <T> : <T> -> <T>")
}
return formatCall(opName, o)
}
}

func formatCall(funcName string, o *OverloadDecl) string {
args := make([]string, len(o.ArgTypes()))
ret := describeCELType(o.ResultType())
for j, a := range o.ArgTypes() {
args[j] = describeCELType(a)
}
if o.IsMemberFunction() {
target := args[0]
args = args[1:]
return fmt.Sprintf("%s.%s(%s) -> %s", target, funcName, strings.Join(args, ", "), ret)
}
return fmt.Sprintf("%s(%s) -> %s", funcName, strings.Join(args, ", "), ret)
}

func describeCELType(t *types.Type) string {
if t.Kind() == types.TypeKind {
return "type"
}
return t.String()
}

var (
emptyArgs = []*types.Type{}
)
Loading