?? 目錄
- ?? 背景
- ?? 簡單了解
? 原理說明
? 快速操作 - ?? 解決問題
- ?? 內(nèi)置能力
- ?? 自定義函數(shù)
- ?? 總結(jié)
- ?? 參考
?? 背景
CEL 是Google 提供的通用的表達(dá)式解析的語法,cel-spec 中描述了這種語法镣隶,各種語言都對(duì)這種語法做了相關(guān)的實(shí)現(xiàn)
這個(gè)東西用途比較廣泛熙掺,拿之前 Knative Eventing 的場景來說,如果我們需要過濾的事件信息比較復(fù)雜,一般的過濾手段比如前綴匹配家制、后綴匹配以及 Knative 提供的 ceSQL 就不夠用了
這個(gè)時(shí)候 CEL 就能更好的處理這個(gè)場景暂雹,我們也以這個(gè)場景為例,看下 CEL 的能力
?? 簡單了解
? 原理說明
cel-go 的實(shí)現(xiàn)是將配置構(gòu)造成了一個(gè)語法樹脓魏,隨后根據(jù)配置來解析提供的數(shù)據(jù)兰吟,最終返回結(jié)果
需要注意的是,這里在構(gòu)造語法樹的時(shí)候茂翔,最終產(chǎn)出的是 protocol buffer 類型的數(shù)據(jù)
-
構(gòu)建語法樹
-
對(duì)提供的數(shù)據(jù)進(jìn)行解析
? 快速操作
可以通過如下兩個(gè)場景來嘗試一下
- 字符替換構(gòu)造
- 數(shù)據(jù)過濾
其中基本步驟如下
- 構(gòu)造了一個(gè) env混蔼,這里聲明了支持解析的數(shù)據(jù)類型與相關(guān)數(shù)據(jù)
- 通過 env.compile 生成了我們提到的語法樹
- 使用 env.Program 來生成處理使用的 prg
- 使用 prg 來處理數(shù)據(jù)
字符構(gòu)造
通過 env.Compile(`"Hello world! I'm " + name + "."`)
構(gòu)造的 Ast 處理數(shù)據(jù) prg.Eval(map[string]any{ "name": "CEL"})
,最終輸出 Hello world! I'm CEL.
package examples
import (
"fmt"
"log"
"github.com/google/cel-go/cel"
)
func ExampleSimple() {
env, err := cel.NewEnv(cel.Variable("name", cel.StringType))
if err != nil {
log.Fatalf("environment creation error: %v\n", err)
}
ast, iss := env.Compile(`"Hello world! I'm " + name + "."`)
// Check iss for compilation errors.
if iss.Err() != nil {
log.Fatalln(iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
log.Fatalln(err)
}
out, _, err := prg.Eval(map[string]any{
"name": "CEL",
})
if err != nil {
log.Fatalln(err)
}
fmt.Println(out)
// Output:Hello world! I'm CEL.
}
數(shù)據(jù)過濾
通過 env.Compile(`request.auth.claims.group == 'admin'`)
構(gòu)造的 Ast 處理數(shù)據(jù)request(auth("user:me@acme.co", claims), time.Now())
珊燎,最終判斷結(jié)果為 true
func exercise2() {
// Construct a standard environment that accepts 'request' as input and uses
// the google.rpc.context.AttributeContext.Request type.
env, err := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast, iss := env.Compile(`request.auth.claims.group == 'admin'`)
if iss.Err() != nil {
glog.Exit(iss.Err())
}
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
// Output: true
claims := map[string]string{"group": "admin"}
out, _, _ := program.Eval(request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println(out)
// Output: true
}
?? 解決問題
以上簡單示例中惭嚣,能夠?qū)?CEL 部分能力有個(gè)大概了解,對(duì)于背景中提到的問題悔政,這里沒能解決
cel.NewEnv
中需要提供待處理數(shù)據(jù)的類型晚吞,否則它無法對(duì)數(shù)據(jù)進(jìn)行處理,cel-go 中無法包含所有的類型谋国,不過它提供了 Declarations
支持自定義一些數(shù)據(jù)類型槽地,cloudevent 中基本都是 map 類型的數(shù)據(jù),所以我們可以自定義一個(gè) map 類型數(shù)據(jù)
比如我們就可以使用 decls.NewMapType(decls.String, decls.Dyn)
來自定義一個(gè) map[string]any
的類型芦瘾,這樣我們就可以輕松處理上面提到的 cloudEvent 中的數(shù)據(jù)
package main
import (
"fmt"
"github.com/golang/glog"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
)
func main() {
env, err := cel.NewEnv(
cel.Declarations(
decls.NewVar("cloudevent", decls.NewMapType(decls.String, decls.Dyn)),
),
)
if err != nil {
glog.Exit(err)
}
ast, iss := env.Compile(`cloudevent.group.admin.name == 'yugougou'`)
if iss.Err() != nil {
glog.Exit(iss.Err())
}
program, _ := env.Program(ast)
claims := map[string]any{"cloudevent": map[string]any{"group": map[string]any{"admin": map[string]any{"name": "yugougou"}}}}
out, _, _ := program.Eval(claims)
fmt.Println("filter cloudevent with admin name yugougou")
fmt.Println(out)
// Output: true
claims = map[string]any{"cloudevent": map[string]any{"group": map[string]any{"admin": map[string]any{"name": "yuzp1996"}}}}
out, _, _ = program.Eval(claims)
fmt.Println("filter cloudevent with admin name yugougou")
fmt.Println(out)
// Output: false
}
輸出結(jié)果
filter cloudevent with admin name yugougou
true
filter cloudevent with admin name yugougou
false
?? 內(nèi)置能力
CEL 定義了很多的語法捌蚊,幫助我們更自由的過濾數(shù)據(jù),不同語言都需要實(shí)現(xiàn)這些語法近弟,參見 標(biāo)準(zhǔn)定義列表
cel-go 就實(shí)現(xiàn)了這些語法缅糟,我們可以方便的使用
在 復(fù)雜些的定義 中,存在一個(gè) matches 的函數(shù)藐吮,matches 就是對(duì)正則表達(dá)式進(jìn)行匹配處理溺拱,判斷數(shù)據(jù)是否符合某個(gè)正則表達(dá)式
這里我們以 matches 為例逃贝,將上面的示例改造一下,就可以通過正則表達(dá)式來做匹配迫摔,結(jié)果也是一致的沐扳,符合預(yù)期的
package main
import (
"fmt"
"github.com/golang/glog"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
)
func main() {
env, err := cel.NewEnv(
cel.Declarations(
decls.NewVar("cloudevent", decls.NewMapType(decls.String, decls.Dyn)),
),
)
if err != nil {
glog.Exit(err)
}
// 由 == 表達(dá)式替換成正則表達(dá)式
ast, iss := env.Compile(`cloudevent.group.admin.name.matches('^yugougou$')`)
if iss.Err() != nil {
glog.Exit(iss.Err())
}
program, _ := env.Program(ast)
claims := map[string]any{"cloudevent": map[string]any{"group": map[string]any{"admin": map[string]any{"name": "yugougou"}}}}
out, _, _ := program.Eval(claims)
fmt.Println("filter cloudevent with admin name yugougou")
fmt.Println(out)
// Output: true
claims = map[string]any{"cloudevent": map[string]any{"group": map[string]any{"admin": map[string]any{"name": "yuzp1996"}}}}
out, _, _ = program.Eval(claims)
fmt.Println("filter cloudevent with admin name yugougou")
fmt.Println(out)
// Output: false
}
輸出結(jié)果
filter cloudevent with admin name yugougou
true
filter cloudevent with admin name yugougou
false
?? 自定義函數(shù)
在 內(nèi)置能力定義 中我們可以看到,CEL 提供了很多內(nèi)置的處理函數(shù)拓展了它的能力句占,這些能力能夠解決我們大多數(shù)的問題
但是很多情況下由于業(yè)務(wù)邏輯復(fù)雜沪摄,或者為了提供更好的用戶體驗(yàn),我們會(huì)需要拓展下這些能力纱烘,CEL 提供了自定義函數(shù)來拓展我們需要的能力
比如我們從數(shù)組中獲取某個(gè)元素杨拐,目前只能通過下標(biāo)的表達(dá)式來獲取,這就很不通用擂啥,因?yàn)檫@會(huì)到之后我們在編寫 CEL 表達(dá)式的時(shí)候哄陶,需要提前知道我們要過濾的數(shù)據(jù)在什么位置
比如有如下數(shù)據(jù),我們想獲取 detail.first.content = abcd
元素的 issue.count
是否大于 0哺壶,那我們只能將 CEL 表達(dá)式寫成 int(root.data[1].issues.count) > 0
屋吨,這就會(huì)帶來上面提到的,需要提前知道要過濾的數(shù)據(jù)在什么位置
root:
data:
- name: "ci-lint"
issue:
count: 0
- name: "ci-lint-1"
detail:
first:
content: abcd
issue:
count: 2
以該場景為例山宾,我們可以定義一個(gè)自定義函數(shù) kvelement(key至扰,value)
,這個(gè)函數(shù)支持通過指定 key 與 value 值來過濾數(shù)組中的元素资锰,這樣我們就可以通過 int(root.data.kvelement('detail.first.content','abcd').issues.count) > 0
來獲取元素敢课,以下是相關(guān)的實(shí)現(xiàn)
package main
import (
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"strings"
)
func main() {
// 準(zhǔn)備待過濾數(shù)據(jù), 我們期望獲取數(shù)組中 name=ci-lint 的元素
data := map[string]interface{}{
"root": map[string]interface{}{
"data": []map[string]interface{}{
{
"name": "ci-lint",
"issues": map[string]interface{}{"count": 0},
},
{
"name": "ci-lint-1",
"detail": map[string]interface{}{
"first": map[string]interface{}{
"content": "abcd",
}},
"issues": map[string]interface{}{"count": 2},
},
},
},
}
// 增加一個(gè) map 的 list 的處理方式
mapType := cel.MapType(cel.StringType, cel.DynType)
arrayType := cel.ListType(mapType)
// Env declaration.
env, _ := cel.NewEnv(
cel.Declarations(
decls.NewVar("root", decls.NewMapType(decls.String, decls.Dyn)),
),
// 將函數(shù)注入到 Env 中,起名為 kvelement绷杜,后續(xù) cel 表達(dá)式中使用的話直秆,也是使用這個(gè)名稱
cel.Function("kvelement",
cel.MemberOverload(
"get_contains_key_value_element",
// 綁定參數(shù),第一個(gè)參數(shù)是該函數(shù)的調(diào)用者鞭盟,后續(xù)的參數(shù)是調(diào)用這個(gè)函數(shù)時(shí)的參數(shù)
[]*cel.Type{arrayType, cel.StringType, cel.DynType},
mapType,
// 綁定函數(shù)
cel.FunctionBinding(ElementContainsKeyValue),
),
),
)
// 使用內(nèi)置能力的方式
ast, iss := env.Compile("int(root.data[1].issues.count) > 0")
if iss != nil {
panic(iss)
}
program, _ := env.Program(ast)
out, _, err := program.Eval(data)
if err != nil {
panic(err)
}
// out is true
fmt.Printf("out is %#v\n", out)
// 使用自定義函數(shù)的方式
ast, iss = env.Compile("int(root.data.kvelement('detail.first.content','abcd').issues.count) > 0")
if iss != nil {
panic(iss)
}
program, _ = env.Program(ast)
out, _, err = program.Eval(data)
if err != nil {
panic(err)
}
// out is true
fmt.Printf("out is %#v", out)
}
func ElementContainsKeyValue(args ...ref.Val) ref.Val {
// 獲取參數(shù)切厘,調(diào)用者是數(shù)組,因此可以通過內(nèi)置的 Lister 來處理
// 第一個(gè)參數(shù)是 key,第二個(gè)是期望的 value
lister := args[0].(traits.Lister)
key := args[1]
value := args[2]
// 處理 lister 的參數(shù)懊缺,對(duì)數(shù)組的所有元素進(jìn)行過濾
iterator := lister.Iterator()
for iterator.HasNext() == types.True {
// 獲取 key 值,key 的提供是通過 . 分割的培他,所以通過 . 獲取每一層的 key 值
keyString, err := getString(key.Value())
if err != nil {
return types.WrapErr(err)
}
keySplits := strings.Split(keyString, ".")
// 獲取數(shù)組中的各個(gè)值
element := iterator.Next()
elementValue, err := getStringInterfaceMap(element.Value())
if err != nil {
return types.WrapErr(err)
}
// 查看提供的 key 的第一層在當(dāng)前數(shù)組元素中是否存在
result := elementValue[keySplits[0]]
if result == nil {
continue
}
// 循環(huán) key 的剩余層并且獲取到對(duì)應(yīng)的值
for _, currentKey := range keySplits[1:] {
middleResult, err := getStringInterfaceMap(result)
if err != nil {
continue
}
if middleResult[currentKey] != nil {
result = middleResult[currentKey]
} else {
continue
}
}
// 使用獲取到的值和期望的值進(jìn)行對(duì)比鹃两,確認(rèn)是否找到相關(guān)的 element
getResult, err := getString(result)
if err != nil {
return types.WrapErr(err)
}
expectResult, err := getString(value.Value())
if err != nil {
return types.WrapErr(err)
}
if getResult == expectResult {
return element
}
}
return types.WrapErr(fmt.Errorf("not matched"))
}
func getStringInterfaceMap(val interface{}) (map[string]interface{}, error) {
msivalue, ok := val.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%#v not map[string]interface{}", msivalue)
}
return msivalue, nil
}
func getString(val interface{}) (string, error) {
stringval, ok := val.(string)
if !ok {
return "", fmt.Errorf("%#v is not string", val)
}
return stringval, nil
}
如代碼示例所示,我們可以將表達(dá)式從 int(root.data[1].issues.count) > 0
替換成 int(root.data.kvelement('detail.first.content','abcd').issues.count) > 0
舀凛,使得 CEL 表達(dá)式更加通用
想要了解更多相關(guān)內(nèi)容俊扳,官方自定義函數(shù)的教程 也推薦參考,官方的示例循序漸進(jìn)猛遍,描述的更加詳細(xì)清楚
?? 總結(jié)
目前只是簡單的描述了 CEL 大概的能力馋记, CEL 還有很多其他能力我還未探索到号坡,比如更多的自定義函數(shù)以及更多的自定義類型等等,這些都極大的豐富了 CEL 的拓展性梯醒,有興趣的可以自行探索
?? 參考
語法參考書 cel-spec ( 定義了 common expression language 包含能力宽堆,不同語言實(shí)現(xiàn)的庫會(huì)按照這里的定義進(jìn)行實(shí)現(xiàn) )
Golang CEL 實(shí)現(xiàn)庫 cel-go (cel-spec Golang 版本的實(shí)現(xiàn))
Golang CEL 代碼實(shí)驗(yàn)室 cel-go codelabs