protovalidate-pythonを使ってみた

# はじめに

Protocol Buffersは、スキーマの中にはMin/MaxなどのValidationを標準ではサポートしていません。

最近、[protovalidate](https://github.com/bufbuild/protovalidate)というものが立ち上がってきています。
Goで使える[proto-gen-validate](https://github.com/bufbuild/protoc-gen-validate)の後継とのことですが、protovalidateはGo, C++, Java, Python, TypeScript(coming soonとのこと)で使えるようになっていて、複雑なValidationもサポートしているとのです。

全体の解説は、[こちら](https://zenn.dev/jy8752/books/84cffcdb89d109)に素晴らしい記事がありますので
ここではPython版を使ってみようと思います。

## 環境準備

protovalidateを利用するために、[buf cli](https://buf.build/product/cli)をインストールします。
Dockerfileに下記を記載します。

[buf cli](https://buf.build/product/cli)は、サーバーにスキーマを送ってコード生成することもできるのですが、
セキュリティ上　問題になることもあるため、今回はprotocもインストールしてLocalでコード生成します。

```Dockerfile
ARG PROTOC_VERSION=27.2
ARG BUF_CLI_VERSION=1.34.0

# Install buf cli 
RUN curl -LO https://github.com/bufbuild/buf/releases/download/v${BUF_CLI_VERSION}/buf-Linux-x86_64.tar.gz \
    && tar -zxvf buf-Linux-x86_64.tar.gz \
    && cp -rf buf/bin/* /usr/bin  \
    && rm -rf ./*    
# Install protoc
RUN curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip \
    && unzip protoc-${PROTOC_VERSION}-linux-x86_64.zip  -d /usr/  \
    && rm  -rf ./*
```

## buf cliの準備

buf.yamlを以下のように作成します。

```yaml
version: v2
deps:
  - buf.build/bufbuild/protovalidate
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE
```

buf.gen.yamlを以下のように作成します。今回はpythonとpyiを生成します。
pluginsの要素を追加すれば他の言語のコードも生成できます。

```yaml
version: v2
plugins:
  - protoc_builtin: python
    out: gen
    include_imports: true
  - protoc_builtin: pyi
    out: gen
    include_imports: true
```

protocのコマンドより便利ですね。

## スキーマ作成

例としてroi.protoを作成します。4000 x 3000の画像をCropするときの設定です。

VS codeで作業をする場合は、[Buf](https://marketplace.visualstudio.com/items?itemName=bufbuild.vscode-buf)を拡張機能としてインストールすると良いです。


```proto
syntax = "proto3";
package roi;

import "buf/validate/validate.proto";

message Roi {
  uint32 top = 1;
  uint32 left = 2;
  uint32 height = 3 [(buf.validate.field).uint32 = {gte: 1}];
  uint32 width = 4 [(buf.validate.field).uint32 = {gte: 1}];
  option (buf.validate.message).cel = {
    id: "roi.width_sum"
    message: "left + width <=  4000"
    expression: "this.left + this.width <= 4000"
  };
  option (buf.validate.message).cel = {
    id: "roi.height_sum"
    message: "top + height <=  3000"
    expression: "this.top + this.height <= 3000"
  };
}

```
`package roi;`の部分で警告が出ますが、今回は無視します。

## コード生成

下記のコマンドで依存関係を取得します。buf.lockが生成されます。

```bash
buf dep update
```

下記のコマンドでコード生成を行います。

```bash
buf generate
```

genディレクトリ以下にroi_pb2.pyとその他のファイルが生成されます。

## テスト

下記のコードでテストができます。
sys.path.appendは無理矢理感ありますが、とりあえずこれで動きます。

```python
import sys
sys.path.append("gen")
import protovalidate
from gen.roi_pb2 import Roi

def validate_roi(roi):
    print(roi.top, roi.left,roi.height,roi.width)
    try:
        protovalidate.validate(roi)
        print("Validation Success")
    except protovalidate.ValidationError as e:
        print("Validation Failed")
        print(e.violations)
    
if __name__ == "__main__":
    validate_roi(Roi(left=0,top=0,width=4000,height=3000))
    validate_roi(Roi(left=10,top=10,width=0,height=0))
    validate_roi(Roi(left=0,top=1,width=4000,height=3000))
```

出力は下記です。Validationのどこで失敗したかもわかっていいですね。

```bash
0 0 3000 4000
Validation Success
10 10 0 0
Validation Failed
violations {
  field_path: "height"
  constraint_id: "uint32.gte"
  message: "value must be greater than or equal to 1"
}
violations {
  field_path: "width"
  constraint_id: "uint32.gte"
  message: "value must be greater than or equal to 1"
}

1 0 3000 4000
Validation Failed
violations {
  constraint_id: "roi.height_sum"
  message: "top + height <=  3000"
}
```

## まとめ

protovalidateのpython版を使ってみました。

まだ、ベータ版とのことですが[企業としてはSchema管理のSaaSもやっている](https://buf.build/)ようで、
既に複数の言語に対応しているため将来有望に思えます。