この記事は、連載
CUEはデータの表現やスキーマ定義やバリデーションなどを行うことができる言語です。元々Google社内で、Borg
CUEは現在まだv0.
弊社
また、CUEはGoと親和性が高く、GoからCUEへの変換や、CUEの定義をGoで使用できる仕組みが用意されています。
この記事では、CUEの基本情報の紹介から、Go APIを使用したポータビリティの高いスキーマ/バリデーション定義の考察などを行います。なお、以下のリポジトリのサンプルコードを使用します。
CUEではブラウザ上で気軽にCUEの振る舞いを確かめられるCUE Playgroundというサイトも存在します。適宜使用してみてください。
CUEの特徴
ここではCUEの特徴をざっくりとだけ紹介します。
まず、データの表現に関してです。CUEはJSONのスーパーセットです。そのため、JSONで表現可能なデータはすべてCUEで表現できます。例えば、電話番号、年齢、アクティブユーザーかどうかを示すユーザーを表してみましょう。
JSONやYAMLにおけるデータ管理の問題点の一つは、JSONやYAMLだけで型などの制約をかけることができない点です。しかしCUEでは型や柔軟な制約を値と似たような形で定義できます。
#userinfo: {
TEL: string // TEL は string 型
Age: <999999 // Age は 999999 より小さい
Is_activated: true // Is_activated はtrue
}
CUEでは型や制約や値をすべてまとめて同じものとして扱うTypes are valuesというコンセプトがあります。
型というのは
そしてCUEでは、同じフィールドに対して値を重ねがけすることができます。
こういった形です。急にややこしい見た目になってきましたね。
CUEでは順番によって値が上書きされることはなく、どのような順番で条件を指定していっても最終的な結果が同じになるという特徴があります。すべての条件は上書きされることはありません[2]。複数の制約を定義していき、条件を徐々に絞っていっていくようなイメージです。上記の例では、例えばAgeに対するフィールドがたくさんあるので、通常のプログラミング言語を想像すると混乱するかもしれません。しかしこれらはすべて、Ageに対する条件を指定しており、上から順番に
strings.
CUEの文法としては他にも、別の構造体の値を参照できたり、デフォルト値を指定できたり、標準パッケージ内の便利な関数をimportできたり、if文やfor文を使用できたりします。表現力があると言えるでしょう。
ここまででCUEの基本的な概念を紹介してきました。CUEのことをもっと知りたいという方は以下のサイトを参照することをお勧めします。
CUEによるスキーマやバリデーションのポータビリティ
CUEには様々な使用用途がありますが、その良さの一つはバリデーションに特化した仕組みと表現力のある一つの言語でスキーマやバリデーション、値を表現できる点、そしてそれを様々な形式に変換できる点だと考えています。
CUEのデータはJSON、YAML、OpenAPI、JSON Schema、Protobufなど様々な形式のデータへと変換できます[5]。また逆に、様々な形式のデータからCUEのファイルを生成することもできます。このように様々なデータ形式の種になれるのが一つ大きな強みです。
CUEで値を定義しておけば、OpenAPIやJSON Schemaとして各言語の実装にスキーマやバリデーションを配ったりできるのです。例えば、CUEはKubernetesなどの巨大なYAMLを生成するために使用することもできます。CUEでデータを型や制約と共に表現することで、巨大なYAMLを制約などで安全にバリデーションしながら、行数も抑えてデータを表現できます[6]。
またCUEはGoとの親和性が高く、最初に紹介したようにCUEにはGoからCUEの変換や、CUEの定義をGoで使用できる仕組みも用意されています。
昨今のマイクロサービス化の流れ等によって、様々なサービス上で同じデータ構造を扱う必要があるケースが増えています。そのため、CUEにバリデーションなどのスキーマに関する定義をすべて詰めこんでおいて、それぞれのサービスがそのCUE定義を参照することで、バリデーションやスキーマ定義をCUEのみで管理することが可能になれば嬉しいかもしれません。
構造体のタグにCUEの定義を埋め込む
ここからはGo APIを用いて、具体的にGoの上でCUEをどのように扱えるのかを見ていきます。
GoにおいてCUEを使用する最も簡単な方法です。サンプルコードでは、構造体のタグにて、Ageが200以下であるという制約を定義しています。cuegoパッケージのValidate関数を使用することで、このタグの定義に沿ったバリデーションを行うことができます。
// 通常のGoにおけるバリデーションの実装例
func (i *User) Validate() error {
if i.Age >= 200 {
return ErrInvalidUser
}
return nil
}
// cueタグに書かれた制約をもとにしたバリデーションの実装例
func (i *User) ValidateWithCUE() error {
return cuego.Validate(i)
}
テストを実行することで、ValidateWithCUEがきちんと動作していることが確認できます。
cuegoパッケージには、他にもcueの制約やデフォルト値などから不足している値を埋めるComplete関数などがあり、Goの中でcueを様々な用途で使用できます。
GoのコードからCUEの定義を生成する
CUEではGoの構造体からCUEの定義を生成するための方法が提供されています。
サンプルコード
cue get go github.com/sanposhiho/cue-sample/go/sample1/ --local
生成されるCUEファイルは以下のようになっています。
#User: {
TEL: string
Age: uint & <200
}
しっかり型の情報とcueタグの制約が記載されていることが確認できます。
このGoのコードからCUEの定義を生成するというのは、先ほど紹介したユースケースの一つである、KubernetesのマニフェストをCUEから生成する際にも役に立ちます。Kubernetesではすべての型定義がGoの構造体として行われています[7]。そのため、PodなどのKubernetesにおける基本的な型定義をGoの構造体から直接CUEに変換することで、かなりの作業数を減らすことができます。
CUEの定義からGoのコードを生成する
CUEの定義からGoのコードを生成することも可能です。ただし、コマンドとしては提供されていないので、CUEからGoに変換するGoのプログラムを記述する必要があります。
サンプルコード
- cue_
gen. go生成プログラム:generate. go
事前にuser.
- User構造体:user.
go
するとcue_
テストを実行することで、Validateがきちんと動作していることが確認できます。
終わりに
この記事ではCUEの紹介とその活用例、Go APIの具体的な使用方法の紹介などを行いました。CUEのみでバリデーションやスキーマ、データを表現でき、そこから様々な形式のデータを作成する橋渡しとなれること、特にGoにおいて柔軟な使用が可能であることが検証できたかなと思います。
CUEはこの記事執筆時でv0.