common expression language 自定義函數(shù)

?? 目錄

  • ?? 背景
  • ?? 簡單了解
    ? 原理說明
    ? 快速操作
  • ?? 解決問題
  • ?? 內(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)建語法樹


    Three phases of parsing an expression (parse and check)
  • 對(duì)提供的數(shù)據(jù)進(jìn)行解析


    Three phases of parsing an expression (evaluate)

? 快速操作

可以通過如下兩個(gè)場景來嘗試一下

  1. 字符替換構(gòu)造
  2. 數(shù)據(jù)過濾

其中基本步驟如下

  1. 構(gòu)造了一個(gè) env混蔼,這里聲明了支持解析的數(shù)據(jù)類型與相關(guān)數(shù)據(jù)
  2. 通過 env.compile 生成了我們提到的語法樹
  3. 使用 env.Program 來生成處理使用的 prg
  4. 使用 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ù)雜些的定義

復(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市茸习,隨后出現(xiàn)的幾起案子畜隶,更是在濱河造成了極大的恐慌,老刑警劉巖号胚,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件籽慢,死亡現(xiàn)場離奇詭異,居然都是意外死亡猫胁,警方通過查閱死者的電腦和手機(jī)箱亿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弃秆,“玉大人届惋,你說我怎么就攤上這事〖蒈睿” “怎么了盼樟?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長锈至。 經(jīng)常有香客問我晨缴,道長,這世上最難降的妖魔是什么峡捡? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任击碗,我火速辦了婚禮,結(jié)果婚禮上们拙,老公的妹妹穿的比我還像新娘稍途。我一直安慰自己,他們只是感情好砚婆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布械拍。 她就那樣靜靜地躺著,像睡著了一般装盯。 火紅的嫁衣襯著肌膚如雪坷虑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天埂奈,我揣著相機(jī)與錄音迄损,去河邊找鬼。 笑死账磺,一個(gè)胖子當(dāng)著我的面吹牛芹敌,可吹牛的內(nèi)容都是我干的痊远。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼氏捞,長吁一口氣:“原來是場噩夢啊……” “哼碧聪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起幌衣,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤矾削,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后豁护,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哼凯,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年楚里,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了断部。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡班缎,死狀恐怖蝴光,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情达址,我是刑警寧澤蔑祟,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站沉唠,受9級(jí)特大地震影響疆虚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜满葛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一径簿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嘀韧,春花似錦篇亭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谊却,卻和暖如春蹂随,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背因惭。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绩衷,地道東北人蹦魔。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓激率,卻偏偏與公主長得像,于是被迫代替她去往敵國和親勿决。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乒躺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容