README.md

Rebar3 gpb rpc plugin
=====

原来 [gpb](https://github.com/tomas-abrahamsson/gpb) 编译出来的文件并没有实现`service`,但是项目需要使用到这个功能做为协议的路由定义,因此有了这个插件

## 应用场景

插件最最要的功能还是通过`gpb`分析协议,然后映射到自定义模板从而生成目标文件

## 例子

[例子](/example) 比较简单,只有一个心跳协议

## 协议解析

[msg.proto](example/proto/msg.proto):

```protobuf
enum cmd{
    msg_base = 0;
}
```

这个文件可以称为`路由模块`,这个文件只定义了一个名为`cmd`的`枚举(enum)`,其意思为`一级协议号(cmd)`,里面的每一个`Key`必须跟`协议的文件名`一致,其值表示`一级协议号(cmd)`

如上: `msg_base` => `msg_base.proto`

[msg_base.proto](example/proto/msg_base.proto):

```protobuf
syntax = "proto3";
import "google/protobuf/empty.proto";

enum c_cmd{
    heartbeat_req   = 0;
    heartbeat_resp  = 1;
}

service msg_base_service{
    rpc heartbeat(heartbeat_req) returns (heartbeat_resp);
    rpc heartbeat_1(heartbeat_req) returns (Empty);
    rpc heartbeat_2(Empty) returns (heartbeat_resp);
}

message heartbeat_req{}
message heartbeat_resp{}
```

从`路由模块`索引到这个这个协议文件,这个文件可以称为`二级路由模块`,里面定义了一个名为`c_cmd`的`枚举(enum)`,其意思为`二级协议号(c_cmd)`,里面的每一个`Key`必须跟`消息名(message)`一致,其值表示`二级协议号(c_cmd)`

另外还需要定义一个`service`,`service`名前缀必须与文件名一直并以`_service`结尾

### proto3版本

里面定义的每一个`rpc`表示:

* 同步协议

  ```protobuf
  rpc heartbeat(heartbeat_req) returns (heartbeat_resp);
  ## 格式如: rpc func(req_message) returns (resp_message);
  ## 意思是: 当服务器收到req_message消息时,会调用func方法,然后发送返回resp_message消息给客户端
  ```

* 异步协议

  ```protobuf
  rpc heartbeat_1(heartbeat_req) returns (Empty);
  ## 格式如: rpc func(req_message) returns (Empty);
  ## 意思是: 当服务器收到req_message消息时,会调用func方法,没有消息返回给客户端

  rpc heartbeat_2(Empty) returns (heartbeat_resp);
  ## 格式如: rpc func(Empty) returns (resp_message);
  ## 意思是: 服务器会主动发送resp_message消息给客户端
  ```

### proto2版本

里面定义的每一个`rpc`表示:

>  proto2版本没有`import "google/protobuf/empty.proto";` 因此需要我们自己定义一个 `undefined`的消息,然后引入到每个协议文件

* 同步协议

  ```protobuf
  rpc heartbeat(heartbeat_req) returns (heartbeat_resp);
  ## 格式如: rpc func(req_message) returns (resp_message);
  ## 意思是: 当服务器收到req_message消息时,会调用func方法,然后发送返回resp_message消息给客户端
  ```

* 异步协议

  ```protobuf
  rpc heartbeat_1(heartbeat_req) returns (undefined);
  ## 格式如: rpc func(req_message) returns (undefined);
  ## 意思是: 当服务器收到req_message消息时,会调用func方法,没有消息返回给客户端

  rpc heartbeat_2(undefined) returns (heartbeat_resp);
  ## 格式如: rpc func(undefined) returns (resp_message);
  ## 意思是: 服务器会主动发送resp_message消息给客户端
  ```

### 使用配置

[rebar.config](example/rebar.config)

```erlang
%% for make *.erl file
{erl_opts, [
    warnings_as_errors,
    {i, "_build/default/plugins/gpb/include"}
]}.

%% 增加插件
{plugins, [
    {rebar3_gpb_rpc_plugin, "0.2.0"},
    {rebar3_gpb_plugin, "1.11.6"}
]}.

%% 增加钩子
%% for make *.proto file
{provider_hooks, [
    {pre, [
%%      for gpb
        {compile, {protobuf, compile}},
        {clean, {protobuf, clean}},
%%      for gpb rpc
        {compile, {gpb_rpc, compile}},
        {clean, {gpb_rpc, clean}}
    ]}
]}.

%% gpb的配置
{gpb_opts, [
    {i, "proto"},
    strings_as_binaries,
%%    use_packages
%%    {nif, true},
    {verify, always},
    {o_erl, "src/proto"},
    {o_hrl, "include/proto"},
    {module_name_suffix, "_pb"}
]}.

%% gpb_rpc的配置
{gpb_rpc_opts, [
    {msg_prefix, "msg_"},  %% 设置协议前缀
    {mod_prefix, "mod_"},  %% 设置生成的erl文件名前缀
    {o_erl, "src/msg"},  %% 输出的erl文件目录
    {o_hrl, "include/msg"}, %% 输出的hrl文件目录
    {erl_tpl, "templates/gpb_rpc/gpb_rpc.erl.tpl"}, %% erl模板地址
    {hrl_tpl, "templates/gpb_rpc/gpb_rpc.hrl.tpl"}  %% hrl模板地址
]}.
```

## 编译结果

```shell
$ make co
```

进入example目录执行上面命令,之后看一下效果

先看[erl模板文件](example/templates/gpb_rpc/gpb_rpc.erl.tpl):

```erlang
-module({{proto_name}}).

-compile(inline).

-include("msg.hrl").
-ifdef(CMD_{{proto_name_upper}}).
-include("{{proto_name}}.hrl").
-include("{{gpb_proto}}.hrl").

-define(THIS_CMD, ?CMD_{{proto_name_upper}}).

-export([
    handle_msg/3,
    decode_input_msg/1,
    decode_output_msg/1,
    decode_input/2,
    decode_output/2,
    encode_msg/2,
    send_msg/3,
    handle_msg/4
]).

{{#callback_list}}
-callback {{handle_func}}(ex_msg:msg(), ex_msg:state()) -> ex_msg:func_reply().
{{/callback_list}}

{{#rpc_list}}
handle_msg(?C_CMD_{{input_upper}}, Binary, State) ->
    Msg = {{gpb_proto}}:decode_msg(Binary, {{input}}),
    Maps = {{handle_mod}}:{{handle_func}}(Msg, State),
    case maps:find(msg, Maps) of
        {ok, RespMsg} ->
            PbBinary = {{gpb_proto}}:encode_msg(RespMsg),
            RespBinary = <<?THIS_CMD:7, ?C_CMD_{{output_upper}}:9, PbBinary/binary>>,
            case maps:find(resp_binary, Maps) of
                {ok, RespBinaryList} when is_list(RespBinaryList) ->
                    Maps#{resp_binary => [RespBinary | RespBinaryList]};
                {ok, OldRespBinary} ->
                    Maps#{resp_binary => [RespBinary, OldRespBinary]};
                error ->
                    Maps#{resp_binary => RespBinary}
            end;
        error -> Maps
    end;
{{/rpc_list}}
{{#input_list}}
handle_msg(?C_CMD_{{input_upper}}, Binary, State) ->
    Msg = {{gpb_proto}}:decode_msg(Binary, {{input}}),
    {{handle_mod}}:{{handle_func}}(Msg, State);
{{/input_list}}
handle_msg(CCmd, _Binary, _State) ->
    #{error => {not_defined_c_cmd, CCmd}}.

decode_input_msg(<<?THIS_CMD:7, CCmd:9, MsgBinary/binary>>) ->
    decode_input(CCmd, MsgBinary).

decode_output_msg(<<?THIS_CMD:7, CCmd:9, MsgBinary/binary>>) ->
    decode_output(CCmd, MsgBinary).

{{#rpc_list}}
handle_msg(?C_CMD_{{input_upper}}, MsgType, Binary, State) ->
    Msg = {{gpb_proto}}:decode_msg(Binary, {{input}}),
    {{handle_mod}}:{{handle_func}}({MsgType, Msg}, State);
{{/rpc_list}}
{{#input_list}}
handle_msg(?C_CMD_{{input_upper}}, MsgType, Binary, State) ->
    Msg = {{gpb_proto}}:decode_msg(Binary, {{input}}),
    {{handle_mod}}:{{handle_func}}({MsgType, Msg}, State);
{{/input_list}}
handle_msg(CCmd, _Msg, _Binary, _State) ->
    {error, {not_defined_c_cmd, CCmd}}.

{{#rpc_list}}
decode_input(?C_CMD_{{input_upper}}, MsgBinary) ->
    {{{handle_mod}}, {{handle_func}}, {{gpb_proto}}:decode_msg(MsgBinary, {{input}})};
{{/rpc_list}}
{{#input_list}}
decode_input(?C_CMD_{{input_upper}}, MsgBinary) ->
    {{{handle_mod}}, {{handle_func}}, {{gpb_proto}}:decode_msg(MsgBinary, {{input}})};
{{/input_list}}
decode_input(CCmd, _MsgBinary) ->
    {error, {not_defined_c_cmd, CCmd}}.

{{#output_list}}
decode_output(?C_CMD_{{output_upper}}, MsgBinary) ->
    {{gpb_proto}}:decode_msg(MsgBinary, {{output}});
{{/output_list}}
decode_output(CCmd, _MsgBinary) ->
    {error, {not_defined_c_cmd, CCmd}}.

encode_msg(CCmd, RespMsg) ->
    RespBinary = {{gpb_proto}}:encode_msg(RespMsg),
    <<?THIS_CMD:7, CCmd:9, RespBinary/binary>>.

send_msg(undefined, _CCmd, _Msg) -> undefined;
send_msg(Pid, CCmd, Msg) ->
    Binary = encode_msg(CCmd, Msg),
    Pid ! {send, Binary},
    ok.

-endif.
```

再来看看生成的文件`src/msg/msg_base.erl`:

```erlang
%% -*- coding: utf-8 -*-
%% Automatically generated, do not edit
%% Generated by gpb_rpc_compile
-module(msg_base).

-compile(inline).

-include("msg.hrl").
-ifdef(CMD_MSG_BASE).
-include("msg_base.hrl").
-include("msg_base_pb.hrl").

-define(THIS_CMD, ?CMD_MSG_BASE).

-export([
    handle_msg/3,
    decode_input_msg/1,
    decode_output_msg/1,
    decode_input/2,
    decode_output/2,
    encode_msg/2,
    send_msg/3,
    handle_msg/4
]).

-callback heartbeat(ex_msg:msg(), ex_msg:state()) -> ex_msg:func_reply().
-callback heartbeat_1(ex_msg:msg(), ex_msg:state()) -> ex_msg:func_reply().

handle_msg(?C_CMD_HEARTBEAT_REQ, Binary, State) ->
    Msg = msg_base_pb:decode_msg(Binary, heartbeat_req),
    Maps = mod_base:heartbeat(Msg, State),
    case maps:find(msg, Maps) of
        {ok, RespMsg} ->
            PbBinary = msg_base_pb:encode_msg(RespMsg),
            RespBinary = <<?THIS_CMD:7, ?C_CMD_HEARTBEAT_RESP:9, PbBinary/binary>>,
            case maps:find(resp_binary, Maps) of
                {ok, RespBinaryList} when is_list(RespBinaryList) ->
                    Maps#{resp_binary => [RespBinary | RespBinaryList]};
                {ok, OldRespBinary} ->
                    Maps#{resp_binary => [RespBinary, OldRespBinary]};
                error ->
                    Maps#{resp_binary => RespBinary}
            end;
        error -> Maps
    end;
handle_msg(?C_CMD_HEARTBEAT_REQ, Binary, State) ->
    Msg = msg_base_pb:decode_msg(Binary, heartbeat_req),
    mod_base:heartbeat_1(Msg, State);
handle_msg(CCmd, _Binary, _State) ->
    #{error => {not_defined_c_cmd, CCmd}}.

decode_input_msg(<<?THIS_CMD:7, CCmd:9, MsgBinary/binary>>) ->
    decode_input(CCmd, MsgBinary).

decode_output_msg(<<?THIS_CMD:7, CCmd:9, MsgBinary/binary>>) ->
    decode_output(CCmd, MsgBinary).

handle_msg(?C_CMD_HEARTBEAT_REQ, MsgType, Binary, State) ->
    Msg = msg_base_pb:decode_msg(Binary, heartbeat_req),
    mod_base:heartbeat({MsgType, Msg}, State);
handle_msg(?C_CMD_HEARTBEAT_REQ, MsgType, Binary, State) ->
    Msg = msg_base_pb:decode_msg(Binary, heartbeat_req),
    mod_base:heartbeat_1({MsgType, Msg}, State);
handle_msg(CCmd, _Msg, _Binary, _State) ->
    {error, {not_defined_c_cmd, CCmd}}.

decode_input(?C_CMD_HEARTBEAT_REQ, MsgBinary) ->
    {mod_base, heartbeat, msg_base_pb:decode_msg(MsgBinary, heartbeat_req)};
decode_input(?C_CMD_HEARTBEAT_REQ, MsgBinary) ->
    {mod_base, heartbeat_1, msg_base_pb:decode_msg(MsgBinary, heartbeat_req)};
decode_input(CCmd, _MsgBinary) ->
    {error, {not_defined_c_cmd, CCmd}}.

decode_output(?C_CMD_HEARTBEAT_RESP, MsgBinary) ->
    msg_base_pb:decode_msg(MsgBinary, heartbeat_resp);
decode_output(CCmd, _MsgBinary) ->
    {error, {not_defined_c_cmd, CCmd}}.

encode_msg(CCmd, RespMsg) ->
    RespBinary = msg_base_pb:encode_msg(RespMsg),
    <<?THIS_CMD:7, CCmd:9, RespBinary/binary>>.

send_msg(undefined, _CCmd, _Msg) -> undefined;
send_msg(Pid, CCmd, Msg) ->
    Binary = encode_msg(CCmd, Msg),
    Pid ! {send, Binary},
    ok.
```

-----

再看看[hrl模板文件](example/templates/gpb_rpc/gpb_rpc.hrl.tpl):

```erl
-ifndef({{proto_name_upper}}_H).
-define({{proto_name_upper}}_H, true).

{{#enums_list}}
%% {{enum_name}}
{{#enum_list}}
-define({{enum_name_upper}}_{{enum_key_upper}}, {{enum_value}}). %% {{enum_key}}
-define({{enum_name_upper}}_{{enum_key_upper}}_KEY, '{{enum_key}}'). %% {{enum_value}}
{{/enum_list}}

{{/enums_list}}
-endif.
```

看看生成的文件:

`msg.hrl`:

```erlang
%% -*- coding: utf-8 -*-
%% Automatically generated, do not edit
%% Generated by gpb_rpc_compile
-ifndef(MSG_H).
-define(MSG_H, true).

%% cmd
-define(CMD_MSG_BASE, 0). %% msg_base
-define(CMD_MSG_BASE_KEY, 'msg_base'). %% 0
```

`msg_base.hrl`:

```erlang
%% -*- coding: utf-8 -*-
%% Automatically generated, do not edit
%% Generated by gpb_rpc_compile
-ifndef(MSG_BASE_H).
-define(MSG_BASE_H, true).

%% c_cmd
-define(C_CMD_HEARTBEAT_REQ, 0). %% heartbeat_req
-define(C_CMD_HEARTBEAT_REQ_KEY, 'heartbeat_req'). %% 0
-define(C_CMD_HEARTBEAT_RESP, 1). %% heartbeat_resp
-define(C_CMD_HEARTBEAT_RESP_KEY, 'heartbeat_resp'). %% 1
```

## 使用

`example.erl`:

```erlang
-module(example).

%% API exports
-export([
    handle_msg/2,
    mk_resp/0
]).

-include("msg_base.hrl").
-include("msg_base_pb.hrl").

%%====================================================================
%% API functions
%%====================================================================

handle_msg(<<Cmd:7, CCmd:9, Binary/binary>>, State) ->
    MsgMod = msg_pb:enum_symbol_by_value_cmd(Cmd),
    MsgMod:handle_msg(CCmd, Binary, State).

mk_resp() ->
    msg_base:encode_msg(?C_CMD_HEARTBEAT_RESP, #heartbeat_resp{}).
```

当外部需要处理消息是,只需要调用`example:handle_msg(Binary, State)`之后会根据之前定义好的协议头和方法去分发消息处理,以上面心跳的例子来说,当收到`<<0:7, 0:9, Binary/binary>>`的协议时,`Cmd=0`, `CCmd=0`,转换成枚举就是`Cmd=msg_base`,`CCmd=heartbeat_req`,然后会分发到处理函数`mod_base:heartbeat(#heartbeat_req{}, State)`

`mod_base.erl`:

```erlang
-module(mod_base).
-behavior(msg_base).
%% API
-export([
    heartbeat/2,
    heartbeat_1/2
]).

-include("msg_base.hrl").
-include("msg_base_pb.hrl").

heartbeat(#heartbeat_req{}, _State) ->
    #{msg => #heartbeat_resp{}}.

heartbeat_1(#heartbeat_req{}, _State) ->
    #{}.
```