cueで複数のprotobufファイルからschemaを読み込む

# cueで複数のprotobufファイルからschemaを読み込む

[grpc-testing](https://github.com/ryoya-fujimoto/grpc-testing) ではprotobufファイルからcueを自動生成する機能があり、protobufのschemaに従ってリクエスト内容を記述することができます。

しかし実はprotobufの`import`で他ファイルから定義を引っ張ってくる場合、素直にcueを生成するだけではcueのコンパイルが通りません。

```protobuf
syntax = "proto3";

package user;

option go_package = "github.com/ryoya-fujimoto/device;device";

import "proto/user/resource.proto"; // <--これ

message Device {
  uint64 id = 1;
  user.User user = 2;
}
```

こんな感じのprotobufから生成したcueのコンパイルを目指します。
今回作ったサンプルファイルなどは https://github.com/ryoya-fujimoto/cue-module-sample に公開してあります。

## 公式のドキュメント

cue + protobufなドキュメントは https://cuelang.org/docs/integrations/protobuf/ にあります。その中の

>Extract CUE from multiple interdependent .proto files

という項目に説明があります。ざっくりいうと「ちょっとめんどくさいけど`option go_package`に従ってcueのmoduleを配置するとできるよ」みたいなことが書いてあります。

ではcueのmoduleとはなんぞやというと、https://cuelang.org/docs/concepts/packages/ にドキュメントがあります。なるほど、この通りにやってやれば行けそうです。

## まずは素直にcueを生成してみる。

サンプル用のprotobufファイルを以下のように配置します。

```
.
├── README.md
└── proto
    ├── device
    │   └── resource.proto
    └── user
        └── resource.proto
```

それぞれのファイルは [user/resource.proto](https://github.com/ryoya-fujimoto/cue-module-sample/blob/master/proto/user/resource.proto), [device/resource.proto](https://github.com/ryoya-fujimoto/cue-module-sample/blob/master/proto/device/resource.proto) のようになっており、device/resource.proto からuser packageをimportしています。

これらのprotobufファイルから`cue import`コマンドでcueファイルを生成します。

```bash
cue import -I ./ proto/device/resource.proto
cue import -I ./ proto/user/resource.proto
```

すると以下のようなファイルが生成されます。

```yaml
# proto/device/resource.proto.cue
package device

import "github.com/ryoya-fujimoto/user"

Device: {
	id?:   uint64      @protobuf(1)
	user?: _user_.User @protobuf(2,type=user.User)
}
_user_ = user

# proto/user/resource.proto.cue
package user

User: {
	email?: string @protobuf(1)
	name?:  string @protobuf(2)
}
```

しかしこれをcueでコンパイル(json出力)してみようとすると

```
$ cue export proto/device/resource.proto.cue
cannot find package "github.com/ryoya-fujimoto/user"
```

と怒られます。`user` packageが解決できていません。同じく生成した`user`のcueファイルを含めてみても

```
$ cue export proto/user/resource.proto.cue proto/device/resource.proto.cue
found packages user (resource.proto.cue) and device (resource.proto.cue) in /Users/ryoya/github/cue-module-sample
```

ダメそうです。ちなみにuserだけだとうまくいきます。

```
$ cue export proto/user/resource.proto.cue
{
    "User": {}
}
```

## moduleに配置する

ということで、[ドキュメント](https://cuelang.org/docs/concepts/packages/) の通りにファイルを配置していきます。

まずは`cue mod init`でcueのモジュールを作成します。この辺りは`go mod init`と思想が似ています。

```
cue mod init github.com/ryoya-fujimoto
```

すると`cud.mod`という **ディレクトリ** が生成され、`module.cue`にモジュール名が記述されます。

```
$ cue mod init github.com/ryoya-fujimoto
$ tree
.
├── README.md
├── cue.mod
│   ├── module.cue
│   ├── pkg
│   └── usr
└── proto
    ├── device
    │   ├── resource.proto
    │   └── resource.proto.cue
    └── user
        ├── resource.proto
        └── resource.proto.cue

6 directories, 6 files
$ cat cue.mod/module.cue
module: "github.com/ryoya-fujimoto"
```

そして`user/resource.proto`の`option go_package`の通りにディレクトリを作成し、その下にcueファイルを置きます。このときディレクトリは`module.cue`に記載されたモジュール名から下のパスで生成します。

```
$ mkdir user
$ cp proto/user/resource.proto.cue user
$ tree
.
├── README.md
├── cue.mod
│   ├── module.cue
│   ├── pkg
│   └── usr
├── proto
│   ├── device
│   │   ├── resource.proto
│   │   └── resource.proto.cue
│   └── user
│       ├── resource.proto
│       └── resource.proto.cue
└── user  <- 新規作成
    └── resource.proto.cue  <- コピー

7 directories, 7 files
```

すると無事`user`packageが解決され、`device/resource.proto.cue`がコンパイルできるようになります。

```
$ cue export proto/device/resource.proto.cue
{
    "Device": {}
}
```

せっかくなので、`device` packageもimportするようにして色々なデータを入れてみましょう。

```bash
$ mkdir device
$ cp proto/device/resource.proto.cue device
$ cat << EOS > device.cue
import "github.com/ryoya-fujimoto/device"

someDevice: device.Device & {
  id: 1
  user: {
    email: "user@example.com"
    name: "user1"
  }
}
EOS

$ cue export device.cue
{
    "someDevice": {
        "id": 1,
        "user": {
            "name": "user1",
            "email": "user@example.com"
        }
    }
}
```

`user` packageが読み込まれているので、型に違反するとちゃんとエラーになります。

```bash
$ cat << EOS > device.cue
import "github.com/ryoya-fujimoto/device"

someDevice: device.Device & {
  id: 1
  user: {
    email: "user@example.com"
    name: 1
  }
}
EOS
$ cue export device.cue
someDevice.user.name: conflicting values string and 1 (mismatched types string and int):
    ./device.cue:3:13
    ./device.cue:7:11
    ./user/resource.proto.cue:5:10
```

## まとめ

公式ドキュメントの通りに進めるだけですが、初見だとだいぶハマりそうなのでドキュメントにしました。cueのmoduleやpackage周りの思想はまだよく分かってないところが多いですが、goのライブラリをそのまま読み込めるみたいなので、その辺も試すと面白いかもしれません。

これを自動生成？ で、できらぁ・・・