Generating Protobufs with entproto
As Ent and Protobuf schemas are not identical, we must supply some annotations on our schema to help entproto
figure out exactly how to generate Protobuf definitions (called “Messages” in protobuf terminology).
The first thing we need to do is to add an entproto.Message()
annotation. This is our opt-in to Protobuf schema generation, we don’t necessarily want to generate proto messages or gRPC service definitions from all of our schema entities, and this annotation gives us that control. To add it, append to ent/schema/user.go
:
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
}
}
Next, we need to annotate each field and assign it a field number. Recall that when defining a protobuf message type, each field must be assigned a unique number. To do that, we add an entproto.Field
annotation on each field. Update the Fields
in ent/schema/user.go
:
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique().
Annotations(
entproto.Field(2),
),
field.String("email_address").
Unique().
Annotations(
entproto.Field(3),
),
}
}
Notice that we did not start our field numbers from 1, this is because ent
implicitly creates the ID
field for the entity, and that field is automatically assigned the number 1. We can now generate our protobuf message type definitions. To do that, we will add to ent/generate.go
a go:generate
directive that invokes the entproto
command-line tool. It should now look like this:
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
//go:generate go run -mod=mod entgo.io/contrib/entproto/cmd/entproto -path ./schema
Let’s re-generate our code:
go generate ./...
Observe that a new directory was created which will contain all protobuf related generated code: ent/proto
. It now contains:
ent/proto
└── entpb
├── entpb.proto
└── generate.go
Two files were created. Let’s look at their contents:
// Code generated by entproto. DO NOT EDIT.
syntax = "proto3";
package entpb;
option go_package = "ent-grpc-example/ent/proto/entpb";
message User {
int32 id = 1;
string user_name = 2;
string email_address = 3;
}
Nice! A new .proto
file containing a message type definition that maps to our User
schema was created!
package entpb
//go:generate protoc -I=.. --go_out=.. --go-grpc_out=.. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --entgrpc_out=.. --entgrpc_opt=paths=source_relative,schema_path=../../schema entpb/entpb.proto
A new generate.go
file was created with an invocation to protoc
, the protobuf code generator instructing it how to generate Go code from our .proto
file. For this command to work, we must first install protoc
as well as 3 protobuf plugins: protoc-gen-go
(which generates Go Protobuf structs), protoc-gen-go-grpc
(which generates Go gRPC service interfaces and clients), and protoc-gen-entgrpc
(which generates an implementation of the service interface). If you do not have these installed, please follow these directions:
To install
protoc-gen-entgrpc
, run:go get -u entgo.io/contrib/entproto/cmd/protoc-gen-entgrpc
After installing these dependencies, we can re-run code-generation:
go generate ./...
Observe that a new file named ent/proto/entpb/entpb.pb.go
was created which contains the generated Go structs for our entities.
Let’s write a test that uses it to make sure everything is wired correctly. Create a new file named pb_test.go
and write:
package main
import (
"testing"
"ent-grpc-example/ent/proto/entpb"
)
func TestUserProto(t *testing.T) {
user := entpb.User{
Name: "rotemtam",
EmailAddress: "rotemtam@example.com",
}
if user.GetName() != "rotemtam" {
t.Fatal("expected user name to be rotemtam")
}
if user.GetEmailAddress() != "rotemtam@example.com" {
t.Fatal("expected email address to be rotemtam@example.com")
}
}
To run it:
go get -u ./... # install deps of the generated package
go test ./...
Hooray! The test passes. We have successfully generated working Go Protobuf structs from our Ent schema. Next, let’s see how to automatically generate a working CRUD gRPC server from our schema.