Allow for interoperability between ent and protobuf/gRPC:
.proto files from an ent schema ent schema from .proto filesThis is a WIP. I've started working on the implementation of .proto file generation and looking for feedback. Will update the design on the other parts soon.
entproto under ent/contrib that takes an ent schema directory as an input and outputs .proto files. In a new package entproto add new annotations to provide necessary mappings between ent and proto (defining stable field numbers, for example). entproto the capability to output a gRPC service definition with basic Add, Delete, Get, Put, and List methods.protoc-gen-ent (example) that outputs .go files with ent.Schemas representing the input proto files. Many organizations use protobuf as their IDL. gRPC is becoming increasingly popular as an RPC transport due to its efficiency and code-generation capabilities. Data access in the protobuf/gRPC world is currently the wild west. Allowing programmatic interoperability between ent and proto/gRPC would open up many possibilities for both ecosystems.
By default, entproto will skip all schemas, unless they explicitly opt-in for proto file generation:
type User struct {
ent.Schema
}
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{entproto.Message()}
}
By default the proto package name for the generated files will be entpb but it can be specified using a functional option:
func (MessageWithPackageName) Annotations() []schema.Annotation {
return []schema.Annotation{entproto.Message(
entproto.PackageName("io.entgo.apps.todo"),
)}
}
All fields must be annotated with entproto.Field to specify their proto field numbers
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").Annotations(entproto.Field(2)),
}
}
The ID field is added to the generated message as well, in the above example it is implicitly defined, but entproto will respect explicitly defined ID fields as well. The above schema would translate to:
message User {
int32 id = 1;
string name = 2
}
Field type mappings:
Ent Type | Proto Type | More considerations
-- | -- | --
TypeBool | bool | 聽
TypeTime | google.protobuf.Timestamp | 聽
TypeJSON | X | 聽
TypeUUID | X | 聽
TypeBytes | bytes | 聽
TypeEnum | Enum | Proto enums like proto fields require stable numbers to be assigned to each value. Therefore we will need to add an extra annotation to map from field value to tag number.
TypeString | string | 聽
TypeOther | X | 聽
TypeInt8 | int32 | 聽
TypeInt16 | int32 | 聽
TypeInt32 | int32 | 聽
TypeInt | int32 | 聽
TypeInt64 | int64 | 聽
TypeUint8 | uint32 | 聽
TypeUint16 | uint32 | 聽
TypeUint32 | uint32 | 聽
TypeUint | uint32 | 聽
TypeUint64 | uint64 | 聽
TypeFloat32 | float | 聽
TypeFloat64 | double | 聽
聽 | 聽 | 聽
Validations:
Edges are annotated in the same way as fields: using entproto.Field annotation to specify the field number for the generated field. Unique relations are mapped to normal fields, non-unique relations are mapped to repeated fields.
For example:
func (BlogPost) Edges() []ent.Edge {
return []ent.Edge{
edge.To("author", User.Type).Unique().Annotations(entproto.Field(4)),
edge.From("categories", Category.Type).Ref("blog_posts").Annotations(entproto.Field(5)),
}
}
func (BlogPost) Fields() []ent.Field {
return []ent.Field{
field.String("title").Annotations(entproto.Field(2)),
field.String("body").Annotations(entproto.Field(3)),
}
}
Is transformed to:
message BlogPost {
int32 id = 1;
string title = 2;
string body = 3;
User author = 4;
repeated Category categories = 5;
}
Validation:
Cyclic dependencies are not supported in protobuf - so back references can only be supported if both messages are output to the same proto package. (In the above example, BlogPost, User and Category must be output to the same proto package).
To facilitate simple bootstrapping of application servers from the proto schema, entproto should support generation of a CRUD gRPC service definition from each ent.Schema. To request the generation of a gRPC service definition, a developer would annotate the schema with:
func (BlogPost) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(
entproto.GenerateService(),
),
}
}
The proto generation creates a service named BlogPostService with 4 methods:
service BlogPostService {
rpc Create ( CreateBlogPostRequest ) returns ( BlogPost );
rpc Get ( GetBlogPostRequest ) returns ( BlogPost );
rpc Update ( UpdateBlogPostRequest ) returns ( BlogPost );
rpc Remove ( RemoveBlogPostRequest ) returns ( google.protobuf.Empty );
}
and generates the supporting input/output message types:
message CreateBlogPostRequest {
BlogPost blog_post = 1;
}
message GetBlogPostRequest {
int32 id = 1;
}
message UpdateBlogPostRequest {
BlogPost blog_post = 1;
}
message DeleteBlogPostRequest {
int32 id = 1;
}
The type of the id field for Get and Delete requests is determined from the gen.Type.ID field type.
TBD
TBD
implementation of interface hooks for pre and post crudl to add custom handling will be great.
fyi. https://github.com/infobloxopen/protoc-gen-gorm
I'd consider using the https://google.aip.dev/133 standards for method naming here.
Thanks @tmc and @kayamax . I will definitely look into this
I'm interested in this sort of interoperability. There's been a bit of discussion on slack and I'd love to have more.
Most helpful comment
I'd consider using the https://google.aip.dev/133 standards for method naming here.