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:

ent/schema/user.go

  1. func (User) Annotations() []schema.Annotation {
  2. return []schema.Annotation{
  3. entproto.Message(),
  4. }
  5. }

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:

ent/schema/user.go

  1. // Fields of the User.
  2. func (User) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.String("name").
  5. Unique().
  6. Annotations(
  7. entproto.Field(2),
  8. ),
  9. field.String("email_address").
  10. Unique().
  11. Annotations(
  12. entproto.Field(3),
  13. ),
  14. }
  15. }

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:

ent/generate.go

  1. package ent
  2. //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
  3. //go:generate go run -mod=mod entgo.io/contrib/entproto/cmd/entproto -path ./schema

Let’s re-generate our code:

  1. go generate ./...

Observe that a new directory was created which will contain all protobuf related generated code: ent/proto. It now contains:

  1. ent/proto
  2. └── entpb
  3. ├── entpb.proto
  4. └── generate.go

Two files were created. Let’s look at their contents:

ent/proto/entpb/entpb.proto

  1. // Code generated by entproto. DO NOT EDIT.
  2. syntax = "proto3";
  3. package entpb;
  4. option go_package = "ent-grpc-example/ent/proto/entpb";
  5. message User {
  6. int32 id = 1;
  7. string user_name = 2;
  8. string email_address = 3;
  9. }

Nice! A new .proto file containing a message type definition that maps to our User schema was created!

ent/proto/entpb/generate.go

  1. package entpb
  2. //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:

After installing these dependencies, we can re-run code-generation:

  1. 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:

  1. package main
  2. import (
  3. "testing"
  4. "ent-grpc-example/ent/proto/entpb"
  5. )
  6. func TestUserProto(t *testing.T) {
  7. user := entpb.User{
  8. Name: "rotemtam",
  9. EmailAddress: "rotemtam@example.com",
  10. }
  11. if user.GetName() != "rotemtam" {
  12. t.Fatal("expected user name to be rotemtam")
  13. }
  14. if user.GetEmailAddress() != "rotemtam@example.com" {
  15. t.Fatal("expected email address to be rotemtam@example.com")
  16. }
  17. }

To run it:

  1. go get -u ./... # install deps of the generated package
  2. 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.