cueで複数のprotobufファイルからschemaを読み込む
grpc-testing ではprotobufファイルからcueを自動生成する機能があり、protobufのschemaに従ってリクエスト内容を記述することができます。
しかし実はprotobufのimport
で他ファイルから定義を引っ張ってくる場合、素直にcueを生成するだけではcueのコンパイルが通りません。
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, device/resource.proto のようになっており、device/resource.proto からuser packageをimportしています。
これらのprotobufファイルからcue import
コマンドでcueファイルを生成します。
cue import -I ./ proto/device/resource.proto
cue import -I ./ proto/user/resource.proto
すると以下のようなファイルが生成されます。
# 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に配置する
ということで、ドキュメント の通りにファイルを配置していきます。
まずは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するようにして色々なデータを入れてみましょう。
$ 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が読み込まれているので、型に違反するとちゃんとエラーになります。
$ 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のライブラリをそのまま読み込めるみたいなので、その辺も試すと面白いかもしれません。
これを自動生成? で、できらぁ・・・