式言語 CEL(Common Expression Language) の Go 言語用ライブラリとして cel-go があります。
cel-go では、一般的なプリミティブ型に加えて list, map, JSON, Protocol Buffers の型に対応していますが、ソースコードを見る限り Go の構造体も処理できそうだったので試してみました。
はじめに
基本的に、cel-go は以下の流れで実装します。
- Environment のセットアップ
- NewEnv の実行
- CEL式のパースとチェック
- Compile の実行
- Program の実行
- 評価
- Eval の実行
NewEnv
で使用する変数や追加の型を定義し、CEL式を Compile
と Program
で処理して、Eval
へ変数の値を渡すとCEL式を評価した結果が得られます。
例えば、name
という変数を使って size(name) > 3
(name 変数値の文字数が 3より大きいかどうか)というCEL式を評価する処理は以下のようになります。(エラー処理は適当です)
package main
import (
"fmt"
"log"
"os"
"github.com/google/cel-go/cel"
)
func main() {
name := os.Args[1]
// 1. Environment のセットアップ
env, err := cel.NewEnv(cel.Variable("name", cel.StringType))
if err != nil {
log.Fatal(err)
}
// 2. パースとチェック
ast, iss := env.Compile("size(name) > 3")
if iss.Err() != nil {
log.Fatal(iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
log.Fatal(err)
}
// 3. 評価
out, _, err := prg.Eval(map[string]any{
"name": name,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
fmt.Printf("type=%v, value=%v \n", out.Type(), out.Value())
}
実行結果は以下の通りです。
$ go run sample.go abc
false
type=bool, value=false
$ go run sample.go abcd
true
type=bool, value=true
構造体の処理
それでは、本題の構造体です。
正式な実装方法に関しては記載が無くて分からなかったのですが、とりあえず NewEnv で以下のようにすれば実現できました。
-
ext.NativeTypes
で Go の構造体を定義 - 変数の型指定で ObjectType を使って、構造体の型名(
package名.構造体名
)を文字列で指定
とりあえず、ext.NativeTypes の引数としては reflect.Type
で良さそうだったので、reflect.TypeFor
の戻り値を使っています。
また、以下では main パッケージ内で構造体 Data を定義しているので、ObjectType の引数は main.Data
になります。
処理するCEL式は、Data 構造体の ID や Value の値を文字列として連結するだけのものですが、int 型の値を文字列と +
で直接連結できなかったので string(intの値)
を使って文字列化しています。
package main
import (
"fmt"
"log"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/ext"
)
type Data struct {
ID string
Value int
}
func main() {
env, err := cel.NewEnv(
cel.Variable("data", cel.ObjectType("main.Data")), // 変数の型に Data 構造体を使用
ext.NativeTypes(reflect.TypeFor[Data]()), // Data 構造体を使用するための型設定
)
if err != nil {
log.Fatal(err)
}
ast, iss := env.Compile(`"id=" + data.ID + ", value=" + string(data.Value)`)
if iss.Err() != nil {
log.Fatal(iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
log.Fatal(err)
}
d := Data{"d-1", 1000}
out, _, err := prg.Eval(map[string]any{
"data": d,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
}
実行結果はこのようになり、構造体を処理できました。
$ go run main.go
id=d-1, value=1000