protoc-gen-sphere-binding
protoc-gen-sphere-binding is a protoc plugin that generates Go struct tags for Sphere binding from .proto files. It is designed to inspect service definitions within your protobuf files and automatically generate corresponding Go struct tags based on sphere binding annotations.
Unlike other protoc plugins that generate new files, this plugin modifies the generated Go structs by injecting appropriate tags for request binding in HTTP handlers.
Installation
To install protoc-gen-sphere-binding, use the following command:
go install github.com/go-sphere/protoc-gen-sphere-binding@latestPrerequisites
You need to have the sphere binding proto definitions in your project. Add the following dependency to your buf.yaml:
deps:
- buf.build/go-sphere/bindingConfiguration Parameters
The behavior of protoc-gen-sphere-binding can be customized with the following parameters:
version: Print the current plugin version and exit. (Default:false)out: The output directory for the modified.protofiles. (Default:api)
Usage with Buf
To use protoc-gen-sphere-binding with buf, you can configure it in your buf.gen.yaml file. Note: protoc-gen-sphere-binding cannot be used with the standard buf.gen.yaml because it does not generate Go code, but rather modifies the .proto files to include Sphere binding tags.
Here is an example configuration:
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/go-sphere/sphere-layout/api
plugins:
- local: protoc-gen-go
out: api
opt:
- paths=source_relative
- local: protoc-gen-sphere-binding
out: api
opt:
- paths=source_relativeHow It Works
protoc-gen-sphere-binding processes protobuf files and adds Go struct tags based on sphere binding annotations. The plugin works in conjunction with protoc-gen-go and should be run after the standard Go code generation to add the binding tags to the generated structs.
Binding Locations
The plugin supports the following binding locations through the sphere.binding.location annotation:
BINDING_LOCATION_BODY: Fields bound to JSON request body (default behavior)BINDING_LOCATION_QUERY: Fields bound to query parameters (addsformtag)BINDING_LOCATION_URI: Fields bound to URI path parameters (addsuritag, removesjsontag)BINDING_LOCATION_HEADER: Fields bound to HTTP headers (addsheadertag)BINDING_LOCATION_FORM: Fields bound to form data (addsformtag)
Proto Definition Example
Here’s a comprehensive example showing different binding locations:
syntax = "proto3";
package shared.v1;
import "buf/validate/validate.proto";
import "google/api/annotations.proto";
import "sphere/binding/binding.proto";
service TestService {
rpc RunTest(RunTestRequest) returns (RunTestResponse) {
option (google.api.http) = {
post: "/v1/test/{path_test1}"
body: "*"
};
}
}
message RunTestRequest {
// URI path parameter
string path_test1 = 1 [(sphere.binding.location) = BINDING_LOCATION_URI];
// Fields without binding annotation go to JSON body
string field_test1 = 2;
// Query parameter
string query_test1 = 3 [(sphere.binding.location) = BINDING_LOCATION_QUERY];
// Header value
string auth_token = 8 [(sphere.binding.location) = BINDING_LOCATION_HEADER];
// Custom tags
repeated int32 ids = 9 [
(sphere.binding.location) = BINDING_LOCATION_QUERY,
(sphere.binding.auto_tags) = "custom:\"ids\""
];
}
// Message with default auto tags
message BodyPathTestRequest {
option (sphere.binding.default_auto_tags) = "db";
message Request {
string name = 1; // Will get: db:"name" json:"name"
string email = 2; // Will get: db:"email" json:"email"
}
Request request = 1;
}
enum TestEnum {
TEST_ENUM_UNSPECIFIED = 0;
TEST_ENUM_VALUE1 = 1;
TEST_ENUM_VALUE2 = 2;
}Generated Code
After running protoc-gen-go followed by protoc-gen-sphere-binding, the generated Go struct will have appropriate tags:
type RunTestRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PathTest1 string `protobuf:"bytes,1,opt,name=path_test1,json=pathTest1,proto3" json:"-" uri:"path_test1"`
FieldTest1 string `protobuf:"bytes,2,opt,name=field_test1,json=fieldTest1,proto3" json:"field_test1"`
QueryTest1 string `protobuf:"bytes,3,opt,name=query_test1,json=queryTest1,proto3" json:"-" form:"query_test1"`
AuthToken string `protobuf:"bytes,8,opt,name=auth_token,json=authToken,proto3" json:"-" header:"auth_token"`
Ids []int32 `protobuf:"varint,9,rep,packed,name=ids,proto3" json:"-" form:"ids" custom:"ids"`
}
type BodyPathTestRequest_Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name" db:"name"`
Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email" db:"email"`
}Advanced Features
Message-Level Configuration
You can set default binding behavior for entire messages:
message SearchRequest {
option (sphere.binding.default_location) = BINDING_LOCATION_QUERY;
option (sphere.binding.default_auto_tags) = "form";
string name = 1; // Will be bound from query with form tag
int32 age = 2; // Will be bound from query with form tag
string email = 3; // Will be bound from query with form tag
}Oneof Support
The plugin also supports oneof fields with default configurations:
message TestRequest {
oneof test_oneof {
option (sphere.binding.default_oneof_location) = BINDING_LOCATION_QUERY;
option (sphere.binding.default_oneof_auto_tags) = "form";
string option_a = 1;
string option_b = 2;
}
}Custom Tags
Add custom Go struct tags using the auto_tags annotation:
message DatabaseModel {
option (sphere.binding.default_auto_tags) = "db";
string name = 1; // Generated: `db:"name" json:"name"`
string email = 2 [(sphere.binding.auto_tags) = "validate:\"email\""];
// Generated: `db:"email" json:"email" validate:"email"`
}Usage in HTTP Handlers
The generated tags work seamlessly with Gin’s binding functions:
func (s *TestService) RunTest(c *gin.Context) {
var req RunTestRequest
// Bind URI parameters
if err := c.ShouldBindUri(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Bind query parameters
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Bind JSON body
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Bind headers
if err := c.ShouldBindHeader(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Process request...
}Integration with Other Generators
protoc-gen-sphere-binding works best when combined with other Sphere generators:
- protoc-gen-go: Generates the base Go structs
- protoc-gen-sphere-binding: Adds binding tags to the structs
- protoc-gen-sphere: Generates HTTP handlers that use the tagged structs
Best Practices
- Use consistent binding locations: Establish patterns for where different types of data should be bound from
- Leverage message-level defaults: Use
default_locationanddefault_auto_tagsto reduce repetition - Be explicit when needed: Override defaults with field-level annotations when necessary
- Test your bindings: Verify that the generated tags work correctly with your HTTP framework
- Keep it simple: Avoid overly complex binding patterns that might confuse API consumers
Troubleshooting
Common Issues
- Tags not appearing: Make sure you’re running
protoc-gen-sphere-bindingafterprotoc-gen-go - Binding not working: Verify that your HTTP framework supports the generated tag format
- Conflicts with existing tags: Check for conflicts between generated tags and existing protobuf tags
Debugging
You can verify the generated tags by examining the generated Go files or using reflection:
import (
"fmt"
"reflect"
)
func inspectTags() {
t := reflect.TypeOf(RunTestRequest{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: %s, Tags: %s\n", field.Name, field.Tag)
}
}