Cel-go is an amazing library for evaluating expressions. The extensive proto support just makes it better.

Let’s explore an interesting problem: if we were to build a evaluation service without embedding the actual proto, i.e. without bundling the generated code that defines the struct, fields etc, how can we still evaluate the expressions that requests the contents of the proto messages?

Let’s start with this proto:

syntax = "proto3";

package main;

option go_package = ".;main";

message Foo {
  string foo = 1;
}
func messageBytes() []byte {
	foo := &Foo{Foo: "foo message"}
	b, err := proto.Marshal(foo)
	if err != nil {
		panic(err)
	}
	return b
}

func descBytes() []byte {
	set := &descriptorpb.FileDescriptorSet{
		File: []*descriptorpb.FileDescriptorProto{
			protodesc.ToFileDescriptorProto(File_foo_proto),
		},
	}
	b, err := proto.Marshal(set)
	if err != nil {
		panic(err)
	}
	return b
}

Now if we know the bytes of Foo message using messageBytes and the proto descriptor also in bytes (every generated code has it). Then we declare it as var x and evaluate x.foo. And we should be able to get value foo message right?

Turns out it’s all supported within cel-go. We just register the all the file descriptor related to message foo and use dynamic message to wrap the bytes:

func exercise9() {
	// step 1: register the proto descriptors
	fileSet := &descriptorpb.FileDescriptorSet{}
	err := proto.Unmarshal(descBytes(), fileSet)
	if err != nil { panic(err) }

	fooFullName := "main.Foo" // `main` is the proto package name
	reg, err := protodesc.NewFiles(fileSet)
	if err != nil { panic(err) }

	// step 2: wrap it with dynamicpb message
	desc, err := reg.FindDescriptorByName(protoreflect.FullName(fooFullName))
	if err != nil { panic(err) }
	msg := dynamicpb.NewMessage(desc.(protoreflect.MessageDescriptor))
	err = proto.Unmarshal(messageBytes(), msg.Interface())
	if err != nil { panic(err) }

	// step 3: cel magic
	env, _ := cel.NewEnv(
		cel.TypeDescs(fileSet),
		cel.Declarations(decls.NewVar("x", decls.NewObjectType(fooFullName))),
	)
	ast, iss := env.Compile(`x.foo`)
	if iss.Err() != nil {
		glog.Exit(iss.Err())
	}
	// Turn on optimization.
	vars := map[string]interface{}{"x": msg}
	program, _ := env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
	eval(program, vars)
	
	// output: foo message
}

Full source at github