# gRPC - Protobuf
# 前言
gRPC 是一套由 google 發起的傳輸格式,它如同一般 HTTP request 一樣是用於發 request 到 server 並取得 response 的介面 。其 payload 使用 byte 型式來傳輸,並以同樣是 google 開的 protobuf 格式來定義 service 以及 message 格式
會考慮使用 gRPC 主要是在 service 之間的 internal api 使用。一套 service 只需要以 protobuf 格式開一份 api 定義,即可自動產生不同程式語言的 client 實作,省去要維護不同語言的 DTO class/interface 之功夫
Microservice 架構下允許各個 module 使用不同語言及 framework 來實作,並使用 internal api 來讓 services 之間能取得彼此的資訊,譬如當需要 user 資訊時,會需要從 account service 取得
目前專案中大多 service 都是 Java based 的,搭配 [FeignClient](https://www.baeldung.com/spring-cloud-openfeign) 使用 http call 的方式來傳輸資訊,並在 parameter 以及 return type 上使用由 **Java Class** 定義的型別。**JAVA** 定的 **CLASS**,代表這作法只有 **Java** 的 service 能直接輕鬆得使用,其它 language 的 service 要用時就會很頭大了,還要先在該語言定好型別才能 call。這就是問題所在
於是我們決定開始讓各個 service 在互通時改用以 protobuf 作為傳輸介面的 [gRPC](https://pjchender.dev/golang/grpc-getting-started/) 協定,[它標榜的](https://zh.wikipedia.org/zh-tw/GRPC)就是能允許在微服務架構下讓不同 service 能高效的互動,通訊時不再使用 json 字串,而是高效且資料量小的 byte 格式。將來便能使用各語言的工具來將既有的 proto 定義轉成該語言的實作或介面
# DEMO
```protobuf
// user.proto
syntax = "proto3";
package com.rental.protobuf.account;
option java_multiple_files = true;
service UserService {
rpc GetUser (GetUserRequest) returns (User) {}
}
message GetUserRequest {
int32 id = 1;
}
message User {
int32 id = 1;
string name = 2;
optional string phone = 3;
repeated string tags = 4;
map<int32, string> properties = 5;
}
```
經過編譯後會產生`User`的 class 實作、`UserServiceImplBase`以及`UserServiceBlockingStub`
service 專案要實作的話需要 extend `UserServiceImplBase`並實作`getUser`函式
```java
@GrpcService
public class UserServiceImpl extends UserServiceImplBase {
@Override
public void getUser(GetUserRequest request, StreamObserver<User> responseObserver) {
var user = User.newBuilder()
.setId(request.getId())
.setName("foobar")
.build();
responseObserver.onNext(user);
responseObserver.onCompleted();
}
}
```
client 則使用`UserServiceBlockingStub`來呼叫
```java
@Service
@RequiredArgsConstructor
public class ProductService {
@GrpcClient("account")
UserServiceBlockingStub userService;
public Product getProduct() {
var product = new Product();
product.setId(1);
product.setName("iPhone 14");
product.setOwnerId(123);
product = fetchRelatedUserOfProduct(product);
return product;
}
public Product fetchRelatedUserOfProduct(Product product) {
// id: 123
// name: "foobar"
var user = userService.getUser(GetUserRequest.newBuilder().setId(product.getOwnerId()).build());
product.setOwner(user);
return product;
}
}
@Data
class Product {
private int id;
private String name;
private int ownerId;
private User owner;
}
```
在使用 TypeScript 的 NestJS 專案中也可以使用 proto 檔來編譯出相應的 interface 以及 service 定義
```ts
@Injectable()
export class ProductService {
readonly userService: UserServiceClient;
constructor(@Inject('account') private client: ClientGrpc) {
this.userService = client.getService<UserServiceClient>('UserService');
}
getProduct(): Observable<Product> {
const product: Product = { id: 1, name: 'iPhone 14', ownerId: 123 };
return this.fetchRelatedUserOfProduct(product);
}
fetchRelatedUserOfProduct(product: Product): Observable<Product> {
return from(this.userService.getUser({ id: 123 })).pipe(
map((user) => ({ ...product, owner: user })),
);
}
}
```
相較之下,在 Java 中使用 FeignClient 開的話,會是這樣的介面,User 型別直接使用 entity class `AccountUser`
```java
@FeignClient(name = "user-client", url = "http://${service.account.host}:${service.account.port}")
public interface UserClient {
@GetMapping("/internal/users/{id}")
AccountUser getUserById(@PathVariable("id") int id);
}
```
service 專案的實作會是這樣,得先自己開 controller,要去想它的 api request 怎麼訂比較符合 RESTful 規範,再開 service 的 interface 與實作
```java
@RestController
@RequestMapping("/internal/users")
@RequiredArgsConstructor
public class UserInternalController {
private final UserService userService;
@GetMapping("/{id}")
public AccountUser getUserById(@PathVariable int id) {
return userService.getUserById(id);
}
}
public interface UserService {
AccountUser getUserById(int id);
}
@Service
public class UserServiceImpl implements UserService {
public AccountUser getUserById(int id) {
var user = new AccountUser();
user.setId(id);
user.setName("foobar");
return user;
}
}
```
在 Java 中使用的方法一樣簡單
```java
@Service
@RequiredArgsConstructor
public class ProductService {
private final UserClient userClient;
public Product getProduct() {
var product = new Product();
product.setId(1);
product.setOwnerId(123);
product = fetchRelatedUserOfProduct(product);
return product;
}
public Product fetchRelatedUserOfProduct(Product product) {
var user = userClient.getUserById(product.getOwnerId());
product.setOwner(user);
return product;
}
}
@Data
class Product {
private int id;
private String name;
private int ownerId;
private AccountUser owner;
}
```
然而在 TypeScript 專案中就麻煩了,因為語言不通不能直接拿那個用 FeignClient 開的`UserClient`。得先自己開好 User 的 interface、client、填 url 然後再 call,如果哪時候 User 改個什麼 field 又要再手動更新一次
```ts
interface User {
id: number;
name: string;
phone?: string;
tags: string[];
properties: Record<number, string>;
}
@Injectable()
export class UserClient {
getUserById(id: number): Promise<User> {
return fetch(`/account/internal/users/${id}`).then((response) => response.json());
}
}
@Injectable()
export class ProductService {
constructor(private userClient: UserClient) {}
getProduct(): Observable<Product> {
const product: Product = { id: 1, name: 'iPhone 14', ownerId: 123 };
return this.fetchRelatedUserOfProduct(product);
}
fetchRelatedUserOfProduct(product: Product): Observable<Product> {
return from(this.userClient.getUserById({ id: product.ownerId })).pipe(
map((user) => {
return { ...product, owner: user };
}),
);
}
}
```
# 優缺點
## 優點
能透過同一份 proto 的定義,產生不同語言的 interface 以及 api client 實作。一些如 Postman 之類的工具也支援輸入一份 proto 來產出所有可用的 request,相當於寫了一份簡單版的 swagger 來描述 api 定義。同時它以 byte 傳輸的特性也適用在要限制傳輸量時,像是 MQTT 的使用情境
## 缺點
### 麻煩 - 要 mapping
如果只是 java 的 service 互 call 的話,原本它的 class 可以共用,一個 service 要讓它能用 internal api 來 call 時只要開個 controller 然後參數以及回傳值用既有的 class 定一定就好了
然後呢,現在我們要用 gRPC,當 internal api 要回傳一個 entity 的資料時就得再用 protobuf 定義它相對應的 message type。傳輸方要把原本的 entity instance mapping 成由 proto 產生出的 class 的 instance,接收方又要再 mapping 一次把它從 proto class instance 轉回 entity。幸好我們早就有 [MapStruct](https://mapstruct.org/) 可以用了,不然還得一一去寫 `a.setName(b.getName())`這堆瑣碎的 mapping。同時要用 java 中的 OffsetDateTime 時也得想辨法轉成 google 定義的 Timestamp 格式
### 語法不夠豐富
protobuf 並不是所有語法都支援,像是它本身**沒有泛型**的語法,也沒有 extend 語法以致於相同的 field 必需在不同的 message 重覆寫
### 產出的 class 不好改
如果想拿 protobuf 產出的 class 作 api 的 payload 的話,會發現它沒得加驗證用的 annotation,或者想讓它們都 implement 一些方便作資料轉換的 interface 也沒辨法,這些是一些前期使用 protobuf 作主要資料格式時的痛點
### 不能直接轉 json
它本身並沒辨法直接透過 jackson 作與 json 字串之間的轉換。簡單的解法是用 protobuf java lib 提供的`JsonFormat`這個 util 來作 json 轉換,可以用它來寫一個 jackson 的 serializer/deserializer factory,但就是得再多費點功夫,而且能用的選項沒 jackson 那麼多就是
至少它 enum 是沒什麼問題的,我們可以很放心的把所有 enum 都用 protobuf 定義,讓它自動產出 java 的 enum ,就不用在另外寫一個自己的 enum 實作然後 mapping 成 protobuf 產生的。然而若要 enum 轉 json 時用特定的字串的話會因為產出的 class 無法修改而無法達成,除非像我們專案這樣給 enum 定義加 `EnumValueOptions` 並用 refliction 去魔改它的`JsonFormat`來達成
# 主要語法
- syntax: 指定 proto 版本,若不指定則會被視為 proto2
- package
- import: import 其它 proto 定義,共用 message type 或一些自定的 options
- option: file options,主要用於記錄這份 proto 拿來 code generate 成其它語言實作時的設定值
- message: 相當於只能有 fields 的 class 定義
- enum
```protobuf
syntax = "proto3";
package com.rental.protobuf.entity;
import "xxx.proto";
option java_multiple_files = true;
message Enitty {
int32 id = 1;
string name = 2;
}
enum EntityType {
FOO = 0;
BAR = 1;
}
```
# Message
- 每個 field 必需為它指定一個大於 1 的數字編號
- 所有 field 都不可為 null,並且都會有固定的預設值,像是 int32 = 0, string = '',這些預設值是不能修改的
- `optional`可以用於標記一個 field 的值並不存在,但即便如此它一樣不會是 null,只是多了是否有值的標記。去讀取該 field 時若它沒有值的話一樣會得到預設值
- List = `repeated`, Map = `map`,這兩種 field 都不能再冠`optional`上去
```protobuf
syntax = "proto3";
package com.rental.protobuf.product;
import ".../xxx.proto";
option java_multiple_files = true;
message Product {
// every field must have a field number
int32 id = 2;
// field number can be not serial
string name = 9;
// every field is non-nullable, mean that it always have a default (int32 = 0, string = '')
// added an 'optional' keyword if want to mark this field might not present
optional int32 owner_id = 11;
// List<string>
repeated string tags = 12;
// Map<Integer, String>
map<int32, string> properties = 13;
}
```
# Enum
第一個 enum value 的編號必需為 0,它同時會被當作這個 enum 的預設值
```protobuf
enum ProductCategory {
// first enum value must be 0
WEARING = 0;
// better to name it with UPPER_SNAKE_CASE, according to google protobuf style guide
THEME_PARKS = 1;
}
```
同一個 package 下不能有重複的 enum value name,以官方範例會把所有 enum value name 加上它所屬 type 的前綴
```protobuf
enum ProductCategory {
WEARING = 0;
THEME_PARKS = 1;
OTHER = 2;
}
enum ProductType {
FIRST = 0;
SECOND = 1;
// cannot have enum value with same name define in one package
// OTHER = 2;
// so it have to be another name
PRODUCT_TYPE_OTHER = 2;
}
```
enum 可以定義在其它 message 中,便能使用同 package 下其它 enum 已經用過的名稱
```protobuf
// define duplicated enum name nested in message
message Product2 {
ProductType type = 1; // (Product2's ProductType)
enum ProductType {
FIRST = 0;
OTHER = 1;
}
}
// example of using nested enum in other message
message Product3 {
Product2.ProductType type = 1;
}
```
# Import
以 user.proto 為例
```protobuf
// user.proto
syntax = "proto3";
package com.rental.protobuf.user;
message User {
int32 id = 1;
string name = 2;
}
```
在使用 import 來的 message 或 enum 時必需寫它完整的 package name
```protobuf
// product.proto
import "user.proto";
package com.rental.protobuf.product;
message Product {
// must use full package name to use it
com.rental.protobuf.user.User owner = 1;
}
```
import 路徑一般會用它的完整路徑來 import,root path 會依使用這些 proto 的編譯器、IDE、parser 來決定。相對路徑可以用,但不是所有工具都支援相對路徑,所以上面的那兩份 proto 應當依照其 pacakge name 放到相對應的路徑
```protobuf
// user.proto -> com/rental/protobuf/user/user.proto
package com.rental.protobuf.user;
```
```protobuf
// product.proto -> com/rental/protobuf/product/product.proto
import "com/rental/protobuf/product/product.proto";
package com.rental.protobuf.product;
```
# gRPC Service 定義
service 定義是在 protobuf 標準規格之外的擴充,所有 service method 都必需、也只能有一個參數以及回傳值,且它們都必需是 message type,不能是像 int32, string 這類的 scalar value types
```protobuf
// grpc service definition
service UserService {
// every service method must have one, and can only have one parameter and return value
rpc GetUser(GetUserRequest) returns (User) {}
// parameter and return type must be a message type, cannot be a scalar value types (primitive types in java)
// rpc UserExistsById (string) returns (bool) {}
}
message GetUserRequest {
int32 id = 1;
}
```
或者也可以用 google 預先定好的 value wrapper
```protobuf
// use wrappers define by google
import "google/protobuf/wrappers.proto";
service UserService {
rpc UserExistsById (google.protobuf.StringValue) returns (google.protobuf.BoolValue) {}
}
```
要標示回傳的資料有可能為空,可以選擇回傳這個 message type 的 default value,由使用方判斷它是不是空值
```protobuf
service UserService {
rpc GetUser(GetUserRequest) returns (User) {}
}
```
```java
var request = GetUserRequest.newBuilder().setId(product.getOwnerId()).build();
var user = userService.getUser(request);
var isEmptyById = user.getId() == 0;
var isEmptyByDefaultInstance = user.equals(user.getDefaultInstanceForType());
```
但某些情況下它的 default instance 是可以視為有效的回傳值,故此可以再開一個這個 value 的 wrapper 來明確表示它可能為空
```protobuf
service UserService {
rpc GetUser(GetUserRequest) returns (UserValue) {}
}
message UserValue {
optional User value = 1;
}
```
```java
var userValue = userService.getById( ... );
var hasUser = userValue.hasValue();
```
或者開一個獨立的`GetUserResponse`
```protobuf
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
}
message GetUserResponse {
optional User value = 1;
}
```
# 編譯 proto
為了能從 proto 產生其它語言的 client 實作,首先需要有 [protoc](https://github.com/protocolbuffers/protobuf#protocol-compiler-installation) 來 compile proto 檔
```bash
brew install protobuf
```
如果是使用 Apple M1 的 CPU,要編譯成 java 時會需要再安裝 [rosetta](https://support.apple.com/zh-tw/HT211861) compiler 以便執行 intel 架構下的 binary 程式
```bash
softwareupdate --install-rosetta
```
接著可以再安裝 [buf](https://docs.buf.build/introduction) 這套工具來作 format、lint,以及讓 protoc 的 code generate 操作起來更方便
```bash
brew install bufbuild/buf/buf
```
# Java 中使用 gRPC
因為要使用 gRPC 時還要處理 code gen 以及 service injection 的功能,所以它的要裝的 dependency 及 plugins 會相對得多
### 使用 protobuf 功能
```xml
<!-- pom.xml -->
<dependencies>
<!-- main package -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protoc.version}</version>
</dependency>
<!-- utils, like JsonFormat -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>${protoc.version}</version>
</dependency>
<!-- for gRPC service -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-services</artifactId>
<version>${grpc.version}</version>
</dependency>
</dependencies>
```
### 由 proto 檔產生 java class
這套 plugin 預設會取 ./src/main/proto 資料夾下的 .proto 檔案,在 maven install 時去產生相對應的 class 定義,也可以依需要在`configration`中加上`protoSourceRoot`參數來設定從哪開始找 proto
```xml
<!-- pom.xml -->
<build>
<!-- for ${os.detected.classifier} -->
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
<protoSourceRoot>${basedir}/../proto</protoSourceRoot>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
```
它的小缺點是它解析錯誤時的錯誤訊息可能不是很好讀,譬如有一份 proto 中有錯誤,它可能會在別份 proto 中顯示這份 proto 的錯誤,然後又顯示好多次這樣,得再從它的訊息中慢慢去找真正的問題來源在哪
### 使用或實作 gRPC service
這邊主要使用了 [gRPC-Spring-Boot-Starter](https://github.com/yidongnan/grpc-spring-boot-starter) 這套 library 來在 spring 中輕鬆得操作 gRPC service。看它文件時建議看英文文件,即便主要維護者是中文語系的開發人員,但它文件齊全度與 Ant design 相反,是英文文件的內容比較多
```xml
<!-- pom.xml -->
<dependencies>
<!-- use services -->
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
</dependency>
<!-- implement services -->
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-service-spring-boot-starter</artifactId>
</dependency>
<!-- or both -->
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
</dependency>
</dependencies>
```
### 操作 protobuf message
```protobuf
message Product {
int32 id = 1;
string name = 2;
optional int32 owner_id = 3;
repeated string tags = 4;
}
```
Message instance 只能透過它的 builder 來建立,且它是 immutable 的,建立後就無法再修改其中的值
```java
// create message instance
var product =
Product.newBuilder()
.setId(1)
.addTags("tag1")
.addAllTags(List.of("tag2", "tag3"))
.build();
product.getId(); // 1
product.getName(); // ""
product.hasOwnerId(); // false
product.getOwnerId(); // 0 (optional default)
product.getTags(0); // "tag1"
product.getTagsList(); // ["tag1", "tag2", "tag3"]
// message instance are immutable
// product.setName("foobar");
// rebuild message instance
var updatedMessage =
product.toBuilder()
.setName("foobar")
.setOwnerId(11)
.clearTags()
.addAllTags(List.of("tag9"))
.build();
// should not set field to null
product.toBuilder()
.setName(null); // throw NullPointerException
```
### 實作 gRPC service
實作方需要在 properties 中定義要 export 的 port (預設為 9090),host 可以使用預設與 spring 相同的 host
```yml
# application.yml
grpc:
server:
port: ${service.account.grpcPort}
```
再覆寫 plugins 由 proto 定義產出的 `ServiceImplBase` 來實作
```protobuf
service UserService {
rpc GetUser(GetUserRequest) returns (User) {}
}
```
```java
@GrpcService
public class UserServiceImpl extends UserServiceImplBase {
@Override
public void getUser(GetUserRequest request, StreamObserver<User> responseObserver) {
var user = User.newBuilder()
.setId(request.getId())
.setName("foobar")
.setPhoneNumber("0912345678")
.build();
// cannot return result with "return"
// return user;
// instead, use observer.onNext()
responseObserver.onNext(user);
responseObserver.onCompleted();
}
}
```
實作時,在 call`onCompleted()`前必需 call `onNext()`來給它一個回傳值,否則這次的 grpc call 會被視為 canceled,並讓使用它的 client 出 exception。忽略`onNext()`不 call 的話它也不會幫你自動帶預設值回去
如果`onCompleted()`沒 call 到的話,call 它的地方會直接 hang 住。如果是因為出 exception 而沒`onCompleted()`的話,它預設的 interceptor 會幫忙 close 這個 call,但如果是正常執行卻沒 `onCompleted()`的話它就會 hang 給你看,這會很可怕
### 使用 gRPC service
使用方需要在 properties 中定義 server 的位置,並在需要用的地方使用 plugin 產出的 client 實作來 call service。
gRPC 協定預設是使用 TLS 加密傳輸的方法,若要連的 service 沒使用 TLS 的話要再加上`negotiation-type: plaintext`。目前專案中的 gRPC servce 都是沒使用 TLS 的
```yml
# application.yml
grpc:
client:
product:
address: static://${service.account.host}:${service.account.grpcPort}
negotiation-type: plaintext
```
接著再用`@GrpcClient`來注入這個 client。這些 GrpcClient 並沒有參與 Spring bean DI 的流程,故此它不能透過`@Autowired`或 constructor 來注入,只能透過`@GrpcClient`
```java
public class ProductService {
@GrpcClient("account") // related to grpc.client.product
UserServiceBlockingStub userService;
private User getUser(int id) {
var request = GetUserRequest.newBuilder().setId(id).build();
return userService.getUser(request);
}
}
```
這邊要注意的是 plugin 在產 service client 實作時會產出三種不同的實作,分別為
```java
// ServiceStub
void getUser(GetUserRequest request, StreamObserver<User> responseObserver);
// ServiceBlockingStub
User getUser(GetUserRequest request);
// ServiceFutureStub
ListenableFuture<User> getUser(GetUserRequest request);
```
一般會直接使用 ServiceBlockingStub 的實作,除非需要使用非同步的寫法才會考慮另外兩者,記得使用時不要 import 錯 class
# 測試 gRPC service
可以使用有 GUI 的 Postman,或 CLI 的 [gRPCurl](https://github.com/fullstorydev/grpcurl) 來試 call 一個 service
Postman 相對易用,在介面上選擇 Create New -> gRPC Reqeust,並在 url 填上 host 及 port 像是`localhost:9090`,然後 Service definition 中使用 server reflection 即可。message payload 就以 json 型式來填值,不知道怎麼填還能用 Generate Example Message 功能來產生 payload

只差在它目前不會自動將 protobuf message 的 field 從 lower_snake_case 轉為一般 json 用的 camelCase,所以在寫 payload 時得用原本的 field name
gRPCurl 的話,則是必需先 list 出 service,再 list 那個 service 的 method 後再 call。記得都要加`--plaintext`參數,而且這參數必需是第一個參數
```bash
-> grpcurl --plaintext localhost:9090 list
com.rental.protobuf.account.UserService
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
-> grpcurl --plaintext localhost:9090 list com.rental.protobuf.account.UserService
com.rental.protobuf.account.UserService.GetUser
-> grpcurl --plaintext -d '{"id": 123}' localhost:9090 com.rental.protobuf.account.UserService.GetUser
{
"id": 123,
"name": "foobar",
"phone_number": "0912345678"
}
```
# Format
JetBrains IDE 本身對 proto 的 format 並不完全,不會調整到一些多餘的空格以及排版、換行等等的。故此為了能保持 proto 檔能有一致的格式,這邊使用了 buf 的 [format](https://docs.buf.build/format/usage) 功能來強制將 proto 格式化成他們的標準格式,這個 format 本身是[沒有選項](https://docs.buf.build/format/usage#configuration)可以設定的
```bash
brew install buf
buf format -w
```
它預設會掃描資料夾下所有的 .proto 檔。只執行`buf format`的話它只會印出 format 後的樣子在 terminal,再加上`-w`參數才會直接覆寫
如果常常用到,也可以在 IDE 上加入這個 External tool,以便使用 actions 或 hotkey 執行

# Lint
```bash
buf lint
```
直接執行的話,它會以預設的 rules 來 lint,會包含大部份較嚴格的規範
與 format 不同的是它可以透過 buf.yaml 檔案來設定要啟用哪些 rules 或者 category
```yaml
# buf.yaml
version: v1
lint:
use:
- BASIC
except:
- ENUM_VALUE_UPPER_SNAKE_CASE
```
# Style guide
現在使用的 style guid 繼承自 buf 提供的`BASIC` category,主要包含了
### proto 路徑
依其 package name 來擺放至相應的目錄,猶如 java 那樣
```protobuf
// user.proto -> com/rental/protobuf/user/user.proto
package com.rental.protobuf.user;
```
### naming
- message fields: lower_snake_case
- enum member: UPPER_SNAKE_CASE
- message name: PascalCase
- service name: PascalCase
- service method: PascalCase
```protobuf
// Pascal
service UserService {
// Pascal(Pascal)
rpc GetUser(GetUserRequest) returns (User) {}
}
// Pascal
message User {
// lower_snake
optional string phone_number = 1;
}
enum ProductCategory {
// UPPER_SNAKE_CASE
THEME_PARKS = 0;
}
```
service method 如果有專屬的 request parameter 以及 response 的話,它們必需分別有 Request 以及 Response 的後綴,譬如
```proto
rpc GetProductById(GetProductByIdRequest) returns (GetProductByIdResponse) {}
```
在透過 protoc 作 code generate 時,Java、TypeScript 以及 json 的 field 都會自動從 lower_snake_case 轉成 lowerCamel,所以不用擔心產生 class, interface 時會與當前程式語言的 coding style 不符
這邊還需要注意的是在 java 下去讀某 proto 的 descriptor 時,會需要以原本 lower_snake 的名字去找 field,像是`User.getDescriptor().findFieldByName("phone_number")`
# 自定 enum 的 json value
開 enum 時可以直接用 proto 產出 enum,省得多開一個還得再 mapping 一次兩邊的值。當那個 enum 要轉 json 時,預設情況下會是用它的 member name 作為 serialized 後的 value,若要讓它轉成其它字串的話會需要改動那個 enum class
然而就目前而言沒有任何方法可以引響 protobuf 轉 java 後產出的 class 要怎麼實作、加什麼 interface 或 annotation 之類的,故此專案中有實作出一套方法,可以在 proto 定義中加入自定的`EnumValueOptions`,並在轉 json 時使用 option 中定義的字串值
使用時,只需要 import 所需的 proto 並在 enum value 後加 option 即可
```protobuf
// to define enum serialized string value
// import custom enum options
import "com/rental/protobuf/enum_option.proto";
enum ProductCategory {
// use enum value options
PERSONAL_CARE = 0 [(jsonValue) = 'personal-care'];
}
```