Skip to content
protoc-gen-route

protoc-gen-route

protoc-gen-route is a protoc plugin that generates routing code from .proto files. It is designed to inspect service definitions within your protobuf files and automatically generate corresponding route handlers based on a specified template. This plugin creates Go code that provides structured routing with operation constants, extra data handling, server interfaces, and codec interfaces for seamless integration with various transport protocols.

Features

  • Generates operation constants for each service method
  • Creates extra data mappings from proto options
  • Provides server and codec interfaces for type-safe implementations
  • Supports custom request and response models
  • Generates handler functions with automatic request/response conversion
  • Integrates with the sphere options framework
  • Supports flexible template customization

Installation

To install protoc-gen-route, use the following command:

go install github.com/go-sphere/protoc-gen-route@latest

Prerequisites

You need to have the sphere options proto definitions in your project. Add the following dependency to your buf.yaml:

deps:
  - buf.build/go-sphere/options

Configuration Parameters

The behavior of protoc-gen-route can be customized with the following parameters:

  • version: Print the current plugin version and exit. (Default: false)
  • options_key: The key for the option extension in your proto file that contains routing information. (Default: route)
  • file_suffix: The suffix for the generated files. (Default: _route.pb.go)
  • template_file: Path to a custom Go template file. If not provided, the default internal template is used.
  • request_model: (Required) The fully qualified Go type for the request model (e.g., github.com/gin-gonic/gin.Context).
  • response_model: (Required) The fully qualified Go type for the response model.
  • extra_data_model: The fully qualified Go type for an additional data model to be used in the template.
  • extra_data_constructor: A function that constructs and returns a pointer to the extra_data_model. (Required if extra_data_model is set).

Usage with Buf

To use protoc-gen-route with buf, you can configure it in your buf.gen.yaml file. Here is an example configuration:

version: v2
managed:
  enabled: true
  disable:
    - file_option: go_package_prefix
      module: buf.build/go-sphere/options
plugins:
  - local: protoc-gen-go
    out: api
    opt:
      - paths=source_relative
  - local: protoc-gen-route
    out: api
    opt:
      - paths=source_relative
      - options_key=bot
      - request_model=github.com/go-sphere/telegram-bot/telegram;Update
      - response_model=github.com/go-sphere/telegram-bot/telegram;Message
      - extra_data_model=github.com/go-sphere/telegram-bot/telegram;MethodExtraData
      - extra_data_constructor=github.com/go-sphere/telegram-bot/telegram;NewMethodExtraData

Proto Definition Example

Here’s how to define services with routing options in your .proto files:

syntax = "proto3";

package bot.v1;

import "sphere/options/options.proto";

service MenuService {
  // UpdateCount handles count update operations
  rpc UpdateCount(UpdateCountRequest) returns (UpdateCountResponse) {
    option (sphere.options.options) = {
      key: "bot"
      extra: {
        key: "command"
        value: "start"
      }
      extra: {
        key: "callback_query"
        value: "start"
      }
    };
  }
  
  rpc ProcessMenu(ProcessMenuRequest) returns (ProcessMenuResponse) {
    option (sphere.options.options) = {
      key: "bot"
      extra: {
        key: "callback_query"
        value: "menu_.*"
      }
    };
  }
}

message UpdateCountRequest {
  int64 value = 1;
  int64 offset = 2;
}

message UpdateCountResponse {
  int64 value = 1;
}

message ProcessMenuRequest {
  string menu_id = 1;
  string action = 2;
}

message ProcessMenuResponse {
  string result = 1;
}

Generated Code

The plugin generates Go code with the following components for each service:

Operation Constants

const OperationBotMenuServiceUpdateCount = "/bot.v1.MenuService/UpdateCount"
const OperationBotMenuServiceProcessMenu = "/bot.v1.MenuService/ProcessMenu"

Extra Data Variables

var ExtraBotDataMenuServiceUpdateCount = telegram.NewMethodExtraData(map[string]string{
    "callback_query": "start",
    "command":        "start",
})

var ExtraBotDataMenuServiceProcessMenu = telegram.NewMethodExtraData(map[string]string{
    "callback_query": "menu_.*",
})

Helper Functions

func GetExtraBotDataByMenuServiceOperation(operation string) *telegram.MethodExtraData {
    switch operation {
    case OperationBotMenuServiceUpdateCount:
        return ExtraBotDataMenuServiceUpdateCount
    case OperationBotMenuServiceProcessMenu:
        return ExtraBotDataMenuServiceProcessMenu
    default:
        return nil
    }
}

Server Interface

type MenuServiceBotServer interface {
    UpdateCount(context.Context, *UpdateCountRequest) (*UpdateCountResponse, error)
    ProcessMenu(context.Context, *ProcessMenuRequest) (*ProcessMenuResponse, error)
}

Codec Interface

type MenuServiceBotCodec interface {
    DecodeUpdateCountRequest(*telegram.Update) (*UpdateCountRequest, error)
    EncodeUpdateCountResponse(*UpdateCountResponse) (*telegram.Message, error)
    DecodeProcessMenuRequest(*telegram.Update) (*ProcessMenuRequest, error)
    EncodeProcessMenuResponse(*ProcessMenuResponse) (*telegram.Message, error)
}

Registration Function

func RegisterMenuServiceBotServer(server MenuServiceBotServer, codec MenuServiceBotCodec) map[string]telegram.Handler {
    handlers := make(map[string]telegram.Handler)
    
    handlers[OperationBotMenuServiceUpdateCount] = func(ctx context.Context, update *telegram.Update) (*telegram.Message, error) {
        req, err := codec.DecodeUpdateCountRequest(update)
        if err != nil {
            return nil, err
        }
        
        resp, err := server.UpdateCount(ctx, req)
        if err != nil {
            return nil, err
        }
        
        return codec.EncodeUpdateCountResponse(resp)
    }
    
    handlers[OperationBotMenuServiceProcessMenu] = func(ctx context.Context, update *telegram.Update) (*telegram.Message, error) {
        req, err := codec.DecodeProcessMenuRequest(update)
        if err != nil {
            return nil, err
        }
        
        resp, err := server.ProcessMenu(ctx, req)
        if err != nil {
            return nil, err
        }
        
        return codec.EncodeProcessMenuResponse(resp)
    }
    
    return handlers
}

Common Use Cases

Bot Command Routing

The plugin is commonly used for routing bot commands in messaging platforms:

service BotService {
  rpc Start(StartRequest) returns (StartResponse) {
    option (sphere.options.options) = {
      key: "bot"
      extra: {
        key: "command"
        value: "/start"
      }
      extra: {
        key: "callback_query"
        value: "start_.*"
      }
    };
  }
}

options_key in buf.gen.yaml selects the key field (bot in the official templates). Command names and callback patterns belong in extra.

Event Routing

You can also use it for general event routing:

service EventService {
  rpc HandleUserCreated(UserCreatedRequest) returns (UserCreatedResponse) {
    option (sphere.options.options) = {
      key: "event_type"
      text: "user.created"
    };
  }
}

Integration Example

Here’s how you might integrate the generated code in a bot application:

package main

import (
    "context"
    "log"
    
    botv1 "myproject/api/bot/v1"
    "github.com/go-sphere/telegram-bot/telegram"
)

// Implement the server interface
type MenuService struct{}

func (s *MenuService) UpdateCount(ctx context.Context, req *botv1.UpdateCountRequest) (*botv1.UpdateCountResponse, error) {
    // Your business logic here
    newValue := req.Value + req.Offset
    return &botv1.UpdateCountResponse{
        Value: newValue,
    }, nil
}

func (s *MenuService) ProcessMenu(ctx context.Context, req *botv1.ProcessMenuRequest) (*botv1.ProcessMenuResponse, error) {
    // Your business logic here
    return &botv1.ProcessMenuResponse{
        Result: fmt.Sprintf("Processed menu %s with action %s", req.MenuId, req.Action),
    }, nil
}

// Implement the codec interface
type MenuServiceCodec struct{}

func (c *MenuServiceCodec) DecodeUpdateCountRequest(update *telegram.Update) (*botv1.UpdateCountRequest, error) {
    // Parse the telegram update into your request struct
    // This would typically extract data from callback data or command parameters
}

func (c *MenuServiceCodec) EncodeUpdateCountResponse(resp *botv1.UpdateCountResponse) (*telegram.Message, error) {
    // Convert your response into a telegram message
    return &telegram.Message{
        Text: fmt.Sprintf("Count updated to: %d", resp.Value),
    }, nil
}

// Similar implementations for other methods...

func main() {
    server := &MenuService{}
    codec := &MenuServiceCodec{}
    
    // Register handlers
    handlers := botv1.RegisterMenuServiceBotServer(server, codec, render)
    
    // Use with your bot framework
    for operation, handler := range handlers {
        extraData := botv1.GetExtraBotDataByMenuServiceOperation(operation)
        // Register with your bot router using the operation and extra data
        log.Printf("Registered handler for %s with extra data: %v", operation, extraData)
    }
}

Advanced Configuration

Custom Templates

You can provide your own template file for custom code generation:

plugins:
  - local: protoc-gen-route
    out: api
    opt:
      - paths=source_relative
      - template_file=./templates/custom_route.tmpl
      - options_key=custom

Multiple Option Keys

You can generate routing code for different transport protocols by using different option keys:

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse) {
    option (sphere.options.options) = {
      key: "http"
      text: "GET /users/{id}"
    };
    option (sphere.options.options) = {
      key: "bot"
      extra: {
        key: "command"
        value: "user_info"
      }
    };
    option (sphere.options.options) = {
      key: "event"
      text: "user.get"
    };
  }
}

text is valid when the option value is a single string. Use extra when one key carries several named fields, as the bot template does.

Best Practices

  1. Use meaningful operation keys: Choose keys that clearly indicate the transport or routing mechanism
  2. Group related operations: Keep related service methods in the same service for better organization
  3. Implement proper error handling: Ensure your codec implementations handle errors gracefully
  4. Use consistent naming: Follow consistent naming patterns for your services and methods
  5. Document your options: Include comments explaining the routing options for each method

Troubleshooting

Common Issues

  1. Missing option annotations: Ensure all methods that should generate routes have the appropriate sphere.options.options annotations
  2. Template errors: Check your custom template syntax if using a custom template file
  3. Missing dependencies: Verify that all required model types are available and properly imported

Integration with Other Generators

protoc-gen-route works well alongside other Sphere generators:

  • Use with protoc-gen-sphere for HTTP routing
  • Combine with protoc-gen-sphere-errors for consistent error handling
  • Works with any transport layer that can use the generated interfaces
Last updated on