Skip to content

Commit 55657d8

Browse files
Document optional library and increase docs coverage (#1162)
* Document optional library and increase docs coverage * Fix doc merge on function decl merge
1 parent 0f9133d commit 55657d8

File tree

15 files changed

+258
-50
lines changed

15 files changed

+258
-50
lines changed

‎cel/decls.go‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,7 @@ func Constant(name string, t *Type, v ref.Val) EnvOption {
142142

143143
// Variable creates an instance of a variable declaration with a variable name and type.
144144
func Variable(name string, t *Type) EnvOption {
145-
return func(e *Env) (*Env, error) {
146-
e.variables = append(e.variables, decls.NewVariable(name, t))
147-
return e, nil
148-
}
145+
return VariableWithDoc(name, t, "")
149146
}
150147

151148
// VariableWithDoc creates an instance of a variable declaration with a variable name, type, and doc string.
@@ -201,14 +198,7 @@ func Function(name string, opts ...FunctionOpt) EnvOption {
201198
if err != nil {
202199
return nil, err
203200
}
204-
if existing, found := e.functions[fn.Name()]; found {
205-
fn, err = existing.Merge(fn)
206-
if err != nil {
207-
return nil, err
208-
}
209-
}
210-
e.functions[fn.Name()] = fn
211-
return e, nil
201+
return FunctionDecls(fn)(e)
212202
}
213203
}
214204

‎cel/env_test.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,14 @@ func TestEnvToConfig(t *testing.T) {
378378
name: "optional lib - alt last()",
379379
opts: []EnvOption{
380380
OptionalTypes(),
381-
Function("last", MemberOverload("string_last", []*Type{StringType}, StringType)),
381+
Function("last",
382+
FunctionDocs(`return the last value in a list, or last character in a string`),
383+
MemberOverload("string_last", []*Type{StringType}, StringType)),
382384
},
383385
want: env.NewConfig("optional lib - alt last()").
384386
AddExtensions(env.NewExtension("optional", math.MaxUint32)).
385-
AddFunctions(env.NewFunction("last",
387+
AddFunctions(env.NewFunctionWithDoc("last",
388+
`return the last value in a list, or last character in a string`,
386389
env.NewMemberOverload("string_last", env.NewTypeDesc("string"), []*env.TypeDesc{}, env.NewTypeDesc("string")),
387390
)),
388391
},

‎cel/library.go‎

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919
"math"
2020

21+
"github.com/google/cel-go/common"
2122
"github.com/google/cel-go/common/ast"
2223
"github.com/google/cel-go/common/decls"
2324
"github.com/google/cel-go/common/env"
@@ -421,16 +422,29 @@ func (lib *optionalLib) CompileOptions() []EnvOption {
421422
Types(types.OptionalType),
422423

423424
// Configure the optMap and optFlatMap macros.
424-
Macros(ReceiverMacro(optMapMacro, 2, optMap)),
425+
Macros(ReceiverMacro(optMapMacro, 2, optMap,
426+
MacroDocs(`perform computation on the value if present and return the result as an optional`),
427+
MacroExamples(
428+
common.MultilineDescription(
429+
`// sub with the prefix 'dev.cel' or optional.none()`,
430+
`request.auth.tokens.?sub.optMap(id, 'dev.cel.' + id)`),
431+
`optional.none().optMap(i, i * 2) // optional.none()`))),
425432

426433
// Global and member functions for working with optional values.
427434
Function(optionalOfFunc,
435+
FunctionDocs(`create a new optional_type(T) with a value where any value is considered valid`),
428436
Overload("optional_of", []*Type{paramTypeV}, optionalTypeV,
437+
OverloadExamples(`optional.of(1) // optional(1)`),
429438
UnaryBinding(func(value ref.Val) ref.Val {
430439
return types.OptionalOf(value)
431440
}))),
432441
Function(optionalOfNonZeroValueFunc,
442+
FunctionDocs(`create a new optional_type(T) with a value, if the value is not a zero or empty value`),
433443
Overload("optional_ofNonZeroValue", []*Type{paramTypeV}, optionalTypeV,
444+
OverloadExamples(
445+
`optional.ofNonZeroValue(null) // optional.none()`,
446+
`optional.ofNonZeroValue("") // optional.none()`,
447+
`optional.ofNonZeroValue("hello") // optional.of('hello')`),
434448
UnaryBinding(func(value ref.Val) ref.Val {
435449
v, isZeroer := value.(traits.Zeroer)
436450
if !isZeroer || !v.IsZeroValue() {
@@ -439,18 +453,26 @@ func (lib *optionalLib) CompileOptions() []EnvOption {
439453
return types.OptionalNone
440454
}))),
441455
Function(optionalNoneFunc,
456+
FunctionDocs(`singleton value representing an optional without a value`),
442457
Overload("optional_none", []*Type{}, optionalTypeV,
458+
OverloadExamples(`optional.none()`),
443459
FunctionBinding(func(values ...ref.Val) ref.Val {
444460
return types.OptionalNone
445461
}))),
446462
Function(valueFunc,
463+
FunctionDocs(`obtain the value contained by the optional, error if optional.none()`),
447464
MemberOverload("optional_value", []*Type{optionalTypeV}, paramTypeV,
465+
OverloadExamples(
466+
`optional.of(1).value() // 1`,
467+
`optional.none().value() // error`),
448468
UnaryBinding(func(value ref.Val) ref.Val {
449469
opt := value.(*types.Optional)
450470
return opt.GetValue()
451471
}))),
452472
Function(hasValueFunc,
473+
FunctionDocs(`determine whether the optional contains a value`),
453474
MemberOverload("optional_hasValue", []*Type{optionalTypeV}, BoolType,
475+
OverloadExamples(`optional.of({1: 2}).hasValue() // true`),
454476
UnaryBinding(func(value ref.Val) ref.Val {
455477
opt := value.(*types.Optional)
456478
return types.Bool(opt.HasValue())
@@ -459,21 +481,43 @@ func (lib *optionalLib) CompileOptions() []EnvOption {
459481
// Implementation of 'or' and 'orValue' are special-cased to support short-circuiting in the
460482
// evaluation chain.
461483
Function("or",
462-
MemberOverload("optional_or_optional", []*Type{optionalTypeV, optionalTypeV}, optionalTypeV)),
484+
FunctionDocs(`chain optional expressions together, picking the first valued optional expression`),
485+
MemberOverload("optional_or_optional", []*Type{optionalTypeV, optionalTypeV}, optionalTypeV,
486+
OverloadExamples(
487+
`optional.none().or(optional.of(1)) // optional.of(1)`,
488+
common.MultilineDescription(
489+
`// either a value from the first list, a value from the second, or optional.none()`,
490+
`[1, 2, 3][?x].or([3, 4, 5][?y])`)))),
463491
Function("orValue",
464-
MemberOverload("optional_orValue_value", []*Type{optionalTypeV, paramTypeV}, paramTypeV)),
492+
FunctionDocs(`chain optional expressions together picking the first valued optional or the default value`),
493+
MemberOverload("optional_orValue_value", []*Type{optionalTypeV, paramTypeV}, paramTypeV,
494+
OverloadExamples(
495+
common.MultilineDescription(
496+
`// pick the value for the given key if the key exists, otherwise return 'you'`,
497+
`{'hello': 'world', 'goodbye': 'cruel world'}[?greeting].orValue('you')`)))),
465498

466499
// OptSelect is handled specially by the type-checker, so the receiver's field type is used to determine the
467500
// optput type.
468501
Function(operators.OptSelect,
469-
Overload("select_optional_field", []*Type{DynType, StringType}, optionalTypeV)),
502+
FunctionDocs(`if the field is present create an optional of the field value, otherwise return optional.none()`),
503+
Overload("select_optional_field", []*Type{DynType, StringType}, optionalTypeV,
504+
OverloadExamples(
505+
`msg.?field // optional.of(field) if non-empty, otherwise optional.none()`,
506+
`msg.?field.?nested_field // optional.of(nested_field) if both field and nested_field are non-empty.`))),
470507

471508
// OptIndex is handled mostly like any other indexing operation on a list or map, so the type-checker can use
472509
// these signatures to determine type-agreement without any special handling.
473510
Function(operators.OptIndex,
474-
Overload("list_optindex_optional_int", []*Type{listTypeV, IntType}, optionalTypeV),
511+
FunctionDocs(`if the index is present create an optional of the field value, otherwise return optional.none()`),
512+
Overload("list_optindex_optional_int", []*Type{listTypeV, IntType}, optionalTypeV,
513+
OverloadExamples(`[1, 2, 3][?x] // element value if x is in the list size, else optional.none()`)),
475514
Overload("optional_list_optindex_optional_int", []*Type{OptionalType(listTypeV), IntType}, optionalTypeV),
476-
Overload("map_optindex_optional_value", []*Type{mapTypeKV, paramTypeK}, optionalTypeV),
515+
Overload("map_optindex_optional_value", []*Type{mapTypeKV, paramTypeK}, optionalTypeV,
516+
OverloadExamples(
517+
`map_value[?key] // value at the key if present, else optional.none()`,
518+
common.MultilineDescription(
519+
`// map key-value if index is a valid map key, else optional.none()`,
520+
`{0: 2, 2: 4, 6: 8}[?index]`))),
477521
Overload("optional_map_optindex_optional_value", []*Type{OptionalType(mapTypeKV), paramTypeK}, optionalTypeV)),
478522

479523
// Index overloads to accommodate using an optional value as the operand.
@@ -482,45 +526,62 @@ func (lib *optionalLib) CompileOptions() []EnvOption {
482526
Overload("optional_map_index_value", []*Type{OptionalType(mapTypeKV), paramTypeK}, optionalTypeV)),
483527
}
484528
if lib.version >= 1 {
485-
opts = append(opts, Macros(ReceiverMacro(optFlatMapMacro, 2, optFlatMap)))
529+
opts = append(opts, Macros(ReceiverMacro(optFlatMapMacro, 2, optFlatMap,
530+
MacroDocs(`perform computation on the value if present and produce an optional value within the computation`),
531+
MacroExamples(
532+
common.MultilineDescription(
533+
`// m = {'key': {}}`,
534+
`m.?key.optFlatMap(k, k.?subkey) // optional.none()`),
535+
common.MultilineDescription(
536+
`// m = {'key': {'subkey': 'value'}}`,
537+
`m.?key.optFlatMap(k, k.?subkey) // optional.of('value')`),
538+
))))
486539
}
487540

488541
if lib.version >= 2 {
489542
opts = append(opts, Function("last",
543+
FunctionDocs(`return the last value in a list if present, otherwise optional.none()`),
490544
MemberOverload("list_last", []*Type{listTypeV}, optionalTypeV,
545+
OverloadExamples(
546+
`[].last() // optional.none()`,
547+
`[1, 2, 3].last() ? optional.of(3)`),
491548
UnaryBinding(func(v ref.Val) ref.Val {
492549
list := v.(traits.Lister)
493-
sz := list.Size().Value().(int64)
494-
495-
if sz == 0 {
550+
sz := list.Size().(types.Int)
551+
if sz == types.IntZero {
496552
return types.OptionalNone
497553
}
498-
499554
return types.OptionalOf(list.Get(types.Int(sz - 1)))
500555
}),
501556
),
502557
))
503558

504559
opts = append(opts, Function("first",
560+
FunctionDocs(`return the first value in a list if present, otherwise optional.none()`),
505561
MemberOverload("list_first", []*Type{listTypeV}, optionalTypeV,
562+
OverloadExamples(
563+
`[].first() // optional.none()`,
564+
`[1, 2, 3].first() ? optional.of(1)`),
506565
UnaryBinding(func(v ref.Val) ref.Val {
507566
list := v.(traits.Lister)
508-
sz := list.Size().Value().(int64)
509-
510-
if sz == 0 {
567+
sz := list.Size().(types.Int)
568+
if sz == types.IntZero {
511569
return types.OptionalNone
512570
}
513-
514571
return types.OptionalOf(list.Get(types.Int(0)))
515572
}),
516573
),
517574
))
518575

519576
opts = append(opts, Function(optionalUnwrapFunc,
577+
FunctionDocs(`convert a list of optional values to a list containing only value which are not optional.none()`),
520578
Overload("optional_unwrap", []*Type{listOptionalTypeV}, listTypeV,
579+
OverloadExamples(`optional.unwrap([optional.of(1), optional.none()]) // [1]`),
521580
UnaryBinding(optUnwrap))))
522581
opts = append(opts, Function(unwrapOptFunc,
582+
FunctionDocs(`convert a list of optional values to a list containing only value which are not optional.none()`),
523583
MemberOverload("optional_unwrapOpt", []*Type{listOptionalTypeV}, listTypeV,
584+
OverloadExamples(`[optional.of(1), optional.none()].unwrapOpt() // [1]`),
524585
UnaryBinding(optUnwrap))))
525586
}
526587

‎cel/macro_test.go‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cel
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/cel-go/common"
21+
"github.com/google/cel-go/common/ast"
22+
)
23+
24+
func TestGlobalVarArgMacro(t *testing.T) {
25+
noopExpander := func(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
26+
return nil, nil
27+
}
28+
varArgMacro := GlobalVarArgMacro("varargs", noopExpander)
29+
if varArgMacro.ArgCount() != 0 {
30+
t.Errorf("ArgCount() got %d, wanted 0", varArgMacro.ArgCount())
31+
}
32+
if varArgMacro.Function() != "varargs" {
33+
t.Errorf("Function() got %q, wanted 'varargs'", varArgMacro.Function())
34+
}
35+
if varArgMacro.MacroKey() != "varargs:*:false" {
36+
t.Errorf("MacroKey() got %q, wanted 'varargs:*:false'", varArgMacro.MacroKey())
37+
}
38+
if varArgMacro.IsReceiverStyle() {
39+
t.Errorf("IsReceiverStyle() got %t, wanted false", varArgMacro.IsReceiverStyle())
40+
}
41+
}
42+
43+
func TestReceiverVarArgMacro(t *testing.T) {
44+
noopExpander := func(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
45+
return nil, nil
46+
}
47+
varArgMacro := ReceiverVarArgMacro("varargs", noopExpander)
48+
if varArgMacro.ArgCount() != 0 {
49+
t.Errorf("ArgCount() got %d, wanted 0", varArgMacro.ArgCount())
50+
}
51+
if varArgMacro.Function() != "varargs" {
52+
t.Errorf("Function() got %q, wanted 'varargs'", varArgMacro.Function())
53+
}
54+
if varArgMacro.MacroKey() != "varargs:*:true" {
55+
t.Errorf("MacroKey() got %q, wanted 'varargs:*:true'", varArgMacro.MacroKey())
56+
}
57+
if !varArgMacro.IsReceiverStyle() {
58+
t.Errorf("IsReceiverStyle() got %t, wanted true", varArgMacro.IsReceiverStyle())
59+
}
60+
}
61+
62+
func TestDocumentation(t *testing.T) {
63+
noopExpander := func(meh MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
64+
return nil, nil
65+
}
66+
varArgMacro := ReceiverVarArgMacro("varargs", noopExpander,
67+
MacroDocs(`convert variable argument lists to a list literal`),
68+
MacroExamples(`fn.varargs(1,2,3) // fn([1, 2, 3])`))
69+
doc, ok := varArgMacro.(common.Documentor)
70+
if !ok {
71+
t.Fatal("macro does not implement Documenter interface")
72+
}
73+
d := doc.Documentation()
74+
if d.Kind != common.DocMacro {
75+
t.Errorf("Documentation() got kind %v, wanted DocMacro", d.Kind)
76+
}
77+
if d.Name != varArgMacro.Function() {
78+
t.Errorf("Documentation() got name %q, wanted %q", d.Name, varArgMacro.Function())
79+
}
80+
if d.Description != `convert variable argument lists to a list literal` {
81+
t.Errorf("Documentation() got description %q, wanted %q", d.Description, `convert variable argument lists to a list literal`)
82+
}
83+
if len(d.Children) != 1 {
84+
t.Fatalf("macro documentation children got: %d", len(d.Children))
85+
}
86+
if d.Children[0].Description != `fn.varargs(1,2,3) // fn([1, 2, 3])` {
87+
t.Errorf("macro documentation Children[0] got %s, wanted %s", d.Children[0].Description,
88+
`fn.varargs(1,2,3) // fn([1, 2, 3])`)
89+
}
90+
}

‎cel/prompt.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const (
116116
defaultPersona = `You are a software engineer with expertise in networking and application security
117117
authoring boolean Common Expression Language (CEL) expressions to ensure firewall,
118118
networking, authentication, and data access is only permitted when all conditions
119-
are satisified.`
119+
are satisfied.`
120120

121121
defaultFormatRules = `Output your response as a CEL expression.
122122

‎cel/prompt_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ import (
2222
"github.com/google/cel-go/test"
2323
)
2424

25-
//go:embed testdata/basic.prompt.md
25+
//go:embed testdata/basic.prompt.txt
2626
var wantBasicPrompt string
2727

28-
//go:embed testdata/macros.prompt.md
28+
//go:embed testdata/macros.prompt.txt
2929
var wantMacrosPrompt string
3030

31-
//go:embed testdata/standard_env.prompt.md
31+
//go:embed testdata/standard_env.prompt.txt
3232
var wantStandardEnvPrompt string
3333

3434
func TestPromptTemplate(t *testing.T) {

‎cel/testdata/BUILD.bazel‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ genrule(
1919

2020
filegroup(
2121
name = "prompts",
22-
srcs = glob(["*.prompt.md"]),
22+
srcs = glob(["*.prompt.txt"]),
2323
)

‎cel/testdata/basic.prompt.md‎ renamed to ‎cel/testdata/basic.prompt.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
You are a software engineer with expertise in networking and application security
22
authoring boolean Common Expression Language (CEL) expressions to ensure firewall,
33
networking, authentication, and data access is only permitted when all conditions
4-
are satisified.
4+
are satisfied.
55

66
Output your response as a CEL expression.
77

‎cel/testdata/macros.prompt.md‎ renamed to ‎cel/testdata/macros.prompt.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
You are a software engineer with expertise in networking and application security
22
authoring boolean Common Expression Language (CEL) expressions to ensure firewall,
33
networking, authentication, and data access is only permitted when all conditions
4-
are satisified.
4+
are satisfied.
55

66
Output your response as a CEL expression.
77

‎cel/testdata/standard_env.prompt.md‎ renamed to ‎cel/testdata/standard_env.prompt.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
You are a software engineer with expertise in networking and application security
22
authoring boolean Common Expression Language (CEL) expressions to ensure firewall,
33
networking, authentication, and data access is only permitted when all conditions
4-
are satisified.
4+
are satisfied.
55

66
Output your response as a CEL expression.
77

0 commit comments

Comments
 (0)