CUE言語に入門してみた

2022.07.11

こんにちは、AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。

みなさんはCUE言語(CUE lang)を使っていますか?

CUEはGo言語との相性も良く、KubernetesのYAMLファイルをCUEに置き換えたりすることもでき、話題のCIツールのDaggerでも利用されています。

筆者は「JSON のすごいやつ」程度の認識しかなかったのですが、学んでみたいツールにCUEが利用されているため、今回公式のチュートリアルを通してCUEに入門してみました!!

なお、本記事では練習用のデータとして、筆者が大好きなお笑い芸人の名前などが出てきますが、特に意味はございませんのでスルーしていただけますと幸いです。

そもそもCUEとはなんなのか

公式サイトには以下のように記載されています。

CUE is an open-source data validation language and inference engine with its roots in logic programming. Although the language is not a general-purpose programming language, it has many applications, such as data validation, data templating, configuration, querying, code generation and even scripting. The inference engine can be used to validate data in code or to include it as part of a code generation pipeline.

論理プログラミングをルーツとしたオープンソースのデータ検証言語であり、推論エンジンらしいです。

汎用的なプログラミング用途の言語ではないようですが、単に設定ファイル用の言語というわけではなくスクリプトを書いたりすることもできるようです。

Aboutページを読み進めると、CUEの歴史とか特徴を書いてくださっていますが、やっぱり自分で使ってみないと良くわからないので、チュートリアルをやってみることにしました。

その前に、公式の手順に従って cue が利用できるように準備します。

筆者の場合、Macを利用しているため Homebrew でインストールを行いました。

# インストール
brew install cue-lang/tap/cue
# 確認
cue version
> cue version v0.4.3 darwin/arm64

CUEの特徴

以下公式のチュートリアルに沿って、特徴をみていきます。

CUEはJSONのスーパーセットだ

JSONのスーパーセット(上位集合)ということで、JSONにはない以下のような特徴があるようです。

  • C言語スタイルのコメントがかける
    • JSONではコメントかけないので地味に嬉しい
  • 各フィールドの末尾に , を書かなくて良い(list形式は除く)
  • listの最後の要素の末尾に ,をつけて良い
    • JSONだとリストの最後の要素の末尾にカンマがあるとNG
  • 一番外側の {} が省略可能

公式の例です。

CUE言語

one: 1
two: 2

// A field using quotes.
"two-and-a-half": 2.5

list: [
	1,
	2,
	3,
]

JSONの場合

{
    "list": [
        1,
        2,
        3
    ],
    "one": 1,
    "two": 2,
    "two-and-a-half": 2.5
}

私も書いてみましたが、すっきりしていてわかりやすくて良いですね。

ツッコミ: "飯塚"
小ボケ: "豊本"
大ボケ: "角田"

members: [
  "飯塚",
  "豊本",
  "角田",
]

original_member: {
  アルファルファ: [
    "飯塚",
    "豊本",
  ]
  プラスドライバー: ["角田"]
}

以下のように、JSONとして出力することもできます。

cue export json.cue

{
    "ツッコミ": "飯塚",
    "小ボケ": "豊本",
    "大ボケ": "角田",
    "members": [
        "飯塚",
        "豊本",
        "角田"
    ],
    "original_member": {
        "アルファルファ": [
            "飯塚",
            "豊本"
        ],
        "プラスドライバー": [
            "角田"
        ]
    }
}

CUEでは型と値を同様に扱う

CUEでは 値と型という概念を統合しており、以下例のように実際のデータ・型などを定義するスキーマ・そしてその中間にあるCUE特有の制約を定義しています。 (公式チュートリアルの例を抜粋しております。)

データ

moscow: {
  name:    "Moscow"
  pop:     11.92M
  capital: true
}

スキーマ

municipality: {
  name:    string
  pop:     int
  capital: bool
}

CUE独特の制約(スキーマとデータの中間)

largeCapital: {
  name:    string
  pop:     >5M
  capital: true
}

なお、5M という書き方が気になったのですが、Go言語のように数値リテラルの任意の場所に _ で見やすくしたりすることもできるようです。

公式の例は以下のような感じです。

a: int
a: 4 // type int

b: number
b: 4.0 // type float

c: int
c: 4.0

d: 4  // will evaluate to type int (default)

e: [
    1_234,       // 1234
    5M,          // 5_000_000
    1.5Gi,       // 1_610_612_736
    0x1000_0000, // 268_435_456
]

私も練習がてら広範なスキーマから、徐々に狭いインスタンスに落とし込む練習をしてみました。

types.cue

// 広範な定義
member: {
  name: string
  age: int
}

boke: member
tukkomi: member

// より具体的な制約を落とし込んでいく
boke: {
  feature: "glasses"
}

tukkomi: {
  feature: "noGlasses"
}

飯塚: tukkomi
豊本: boke
角田: boke

// 競合しないためOK
飯塚: {
  name: "飯塚 悟志"
  age: 49
}

豊本: {
  name: "豊本 明長"
  age: 47
}

角田: {
  name: "角田 晃広"
  age: 49
}

以下のコマンドを実行すると、正常に評価されているのがわかります。

cue eval types.cue

member: {
    name: string
    age:  int
}
boke: {
    name:    string
    feature: "glasses"
}
tukkomi: {
    name:    string
    feature: "noGlasses"
    age:     int
}
飯塚: {
    name:    "飯塚 悟志"
    feature: "noGlasses"
    age:     49
}
豊本: {
    name:    "豊本 明長"
    feature: "glasses"
    age:     47
}
角田: {
    name:    "角田 晃広"
    feature: "glasses"
    age:     49
}

なお、定義した型と異なるデータでフィールドを埋めようとするとエラーがでます。

角田: {
    name:    "角田 晃広"
    feature: "glasses"
    // int指定だが文字列を埋め込んでみる
    age:     "中年"
}
角田.age: conflicting values int and "中年" (mismatched types int and string):
    ./cue/types.cue:4:8
    ./cue/types.cue:36:8

CUEでの重複したフィールドの取り扱い

JSONでは以下のようにキーの重複は許されませんが、CUEでは定義が競合しない限り、重複が許容されます。

公式から抜粋した例です。

JSONの場合(キー重複のため利用できない)

{
  "a": 123,
  "b": 456,
  "a": 789
}

CUEの場合

a: 4
a: 4

s: { b: 2 }
s: { c: 2 }

l: [ 1, 2 ]
l: [ 1, 2 ]

以下の評価値を見ればわかりやすいですが、競合が発生しない限り定義をマージしてくれるようです。

a: 4
s: {
    b: 2
    c: 2
}
l: [1, 2]

なお、事前の定義と競合すると以下のようにエラーがでました。

a: 4
// 4であり8であることはないため、競合してエラー
a: 8
a: conflicting values 8 and 4:
    ./cue/duplicates.cue:1:4
    ./cue/duplicates.cue:3:4

CUEで制約をかける

先ほどの型による制限の他、より具体的に制約することができます。

constraints.cue

member: {
  name: string
  // 45より大きい整数に制限
  age: >45
  // glasses または noGlassesに制限
  feature: "glasses" | "noGlasses"
}

boke: member
tukkomi: member

// データインスタンスに応じてより制約を厳しく
boke: {
  feature: "glasses"
}

tukkomi: {
  feature: "noGlasses"
}

飯塚: tukkomi
豊本: boke
角田: boke

飯塚: {
  name: "飯塚 悟志"
  age: 49
}

豊本: {
  name: "豊本 明長"
  age: 47
}

角田: {
  name: "角田 晃広"
  age: 49
}

以下のように評価されます。

member: {
    name:    string
    age:     >45
    feature: "glasses" | "noGlasses"
}
boke: {
    name:    string
    age:     >45
    feature: "glasses"
}
tukkomi: {
    name:    string
    age:     >45
    feature: "noGlasses"
}
飯塚: {
    name:    "飯塚 悟志"
    age:     49
    feature: "noGlasses"
}
豊本: {
    name:    "豊本 明長"
    age:     47
    feature: "glasses"
}
角田: {
    name:    "角田 晃広"
    age:     49
    feature: "glasses"
}

CUEでの構造体(Strcts)の定義とバリデーション

構造体は以下のように定義できます。(公式サイトより抜粋)

// #でstructs(構造体)を定義して制約を書く
#a: {
    // ?がついているフィールドは存在しなても良いが、存在する場合は型・制約を満たす必要がある
    foo?: int
    bar?: string
    baz?: string
}
// 実際のオブジェクト側で&を用いて制約を受けいれる
b: #a & {
    foo:  3
    baz?: 2  // baz?: _|_
}

以下のように正常に評価されます。

b: {
    foo: 3
}

構造体についてより詳細に見たい場合は、公式サイトのドキュメントをご参照ください。参考: Structs | CUE

さらに、構造体を使って既存のyamlファイルやJSONファイルのバリデーションを行うことができます。

こちらも公式より抜粋した例となります。

schema.cue

#Language: {
	tag:  string
	name: =~"^\\p{Lu}" // Must start with an uppercase letter.
}
languages: [...#Language]

data.yaml

languages:
  - tag: en
    name: English
  - tag: nl
    name: dutch
  - tag: no
    name: Norwegian

バリデーション用のコマンド cue vet schema.cue data.yaml

languages.1.name: invalid value "dutch" (does not match =~"^\\p{Lu}"):
    ./schema.cue:3:8
    ./data.yaml:5:12

これは既存のJSONやyamlによる設定ファイルを検証するのにも良さそうですね。

筆者も試しに書いてみました。

CUEによる型・制約の定義

validations.cue

#Member: {
  name: string
  age: >45
  // なくても良いけどあったら制約を満たすべき項目
  role?: "boke" | "tukkomi"
}

members: [...#Member]

検証したいJSONデータ

{
  "members": [
    {
      "name": "飯塚 悟志",
      "age": 49
    },
    {
      "name": "豊本 明長",
      "age": 47,
      "role": "boke"
    },
    {
      "name": "角田 晃広",
      "age": 49
    }
  ]
}

cue vet cue/validation.cue cue/members.json のコマンドで検証しますが、データに不整合がないため正常終了します。

一方以下のようにわざと不正なJSONファイルを用意して、コマンドを実行すると以下のように不整合である点を指摘しくれます。

invalid_members.json

{
  "members": [
    {
      "name": "飯塚 悟志",
      "age": 49,
      "role": "champion of KOC"
    },
    {
      "name": "豊本 明長",
      "age": 47,
      "role": "boke",
      "unenessecary_filed": "hoge"
    },
    {
      "name": "角田 晃広",
      "age": 1
    }
  ]
}
members.0.role: 2 errors in empty disjunction:
members.0.role: conflicting values "boke" and "champion of KOC":
    ./cue/invalid_members.json:6:15
    ./cue/validation.cue:5:10
    ./cue/validation.cue:8:11
    ./cue/validation.cue:8:14
members.0.role: conflicting values "tukkomi" and "champion of KOC":
    ./cue/invalid_members.json:6:15
    ./cue/validation.cue:5:19
    ./cue/validation.cue:8:11
    ./cue/validation.cue:8:14
members.1: field not allowed: unenessecary_filed:
    ./cue/invalid_members.json:12:7
    ./cue/validation.cue:1:10
    ./cue/validation.cue:8:11
    ./cue/validation.cue:8:14
members.2.age: invalid value 1 (out of bound >45):
    ./cue/validation.cue:3:8
    ./cue/invalid_members.json:16:14

CUEによる条件分岐・繰り返し

CUE 言語 であるため条件分岐・繰り返し処理も可能です。

繰り返し、オペレーター、条件分岐などの例はExpressions | CUEにわかりやすくまとめてくださっています。

condition.cue

member: {
  name: string
}

if member.name == "飯塚" {
  member: {
    age: 49
  }
}

member: {
  name: "飯塚"
}

評価値

member: {
    name: "飯塚"
    age:  49
}

繰り返し

for.cue

name_list: ["飯塚", "豊本", "角田"]

for v in name_list {
  "\(v)": {
      name: v
  }
}
name_list: ["飯塚", "豊本", "角田"]
飯塚: {
    name: "飯塚"
}
豊本: {
    name: "豊本"
}
角田: {
    name: "角田"
}

CUEを使うと何が嬉しいのか

ここまでチュートリアルをしてきて感じたCUEのメリットは以下の通りです

  • わかりやすく・面倒ではない記法で書くことができる
    • JSONのように末尾に , が必要ない
    • JSONのようにキー名を "で囲む必要もなく楽
    • yamlのようにインデントを気にする必要がない
  • 柔軟な表現が可能
    • 繰り返し処理や条件分岐も可能
  • スキーマの定義とバリデーションがCUE単体で完結する
    • フィールドと型を記載したJSONを読み込んで、プログラミング言語側で詳しく検証・・・などが不要
  • Go言語から楽に使うことができる

JSONやyamlとして出力することも可能であるため、Go言語を使う際にJSONやymlの置き換えとして使用するのはありだと思います。

公式のユースケースも読むと、より理解が深まると思います。

一方でJSONやyamlに比べて学ぶことも多くあり、情報量も少なそうなので全てをCUEに置き換えるのは難しいかと思います。

zenn.devのCUE言語(cuelang)に入門しようの記事にも記載されているように、既存の設定ファイルを小さく検証するところから始めるのが良いかと思いました。

なお、CUEはオンラインエディターでも動作を試して遊ぶことができますので、是非触って楽しんでみてください!