🔖

CUEのスキーマ定義に使う制約をチートシートっぽくまとめる

2022/11/05に公開約5,000字

いきさつ

先日、こちらの記事で「CUEを使ってみました」と題しましてyamlで書かれた設定ファイルのスキーマ定義、validationを行ってみました。

今回は前回の内容を踏まえ、CUEで定義できる制約の実例を自分向けチートシートとしてまとめたいと思います。随時加筆の予定です。

ゴール

  • CUEで書けるスキーマ定義について、ケースごとに実例を列挙する

こんなときどうするチートシート

文字列(string)

title: string

整数(int)

article_no: int

小数を含む数値

price: number

真偽値(bool)

flag: bool

値の範囲を制限する[1]

article_no: int & >=0 & <=999_999  // 0以上、999,999以内

これでもOK. 同一キーの制約を列挙した場合は掛け算(and)となります。

article_no: int
article_no: >=0 & <=999_999

これでもOK.(ただしここまで分けると読みにくい気が)

article_no: int
article_no: >=0
article_no: <=999_999

個人的には「1. 型の制約」と「2. 値の制約」を分ける程度で十分かなと思っています。

日時形式の文字列

import "time"

createDate: string
createDate: time.Format(time.RFC3339)  // "2006-01-02T15:04:05Z07:00"

Formatの引数には直接"2006-01-02T15:04:05Z07:00"のように書くこともできますが、timeパッケージをimportして定数を用いる方が間違いがありません。
この実装はGoのtimeパッケージに準拠しており、定数の定義も同一とのことです。

https://pkg.go.dev/cuelang.org/go@v0.4.3/pkg/time

省かれるかもしれない値(optional)

title?: string
article_no?: int

キーの後ろに?を付加すると、値を省いてもよいことになります。逆にこの指定がない場合、データ側にキーが含まれないと制約違反になります。(requiredということ)

値がnullになるかもしれない

title: null | string
article_no: null | int

nullと型をパイプ(|)でつなげてOR条件を作ります。ORなので順番は問われませんが、ひとつのスキーマ(あるいはひとつのプロジェクト)内で順番は統一するのが望ましいと思います。

// 読みづらい
title: null | string
article_no: int | null

いくつかの特定の文字列のみ許可する

level:  "debug" | "info" | "warn" | "error" | "fatal"

一般的なログレベルのようなキーを定義する際に有用です。他にも"yes" | "no"のような用途にも使えると思います。列挙した文字列以外が与えられるとエラーとなります。

いくつかの特定の文字列のうちデフォルト値を決める

level:  "debug" | *"info" | "warn" | "error" | "fatal"  // デフォルト"info"

わかりにくいですが、デフォルトにしたい値の先頭に*を付加します。このようにすると、値が省かれていた場合に自動的にデフォルトに指定した値を埋め込みます。

文字列の文字数を制限する(正規表現の利用)

title: string & =~"^.{0,20}$"  // 20文字以内

どちらでもOK.

title: string
title: =~"^.{0,20}$"  // 20文字以内

正規表現を用いて文字列の制約を組み立てることができます。=~で、後続の式に一致すること、!~で一致しないことをチェックします。「文字数を〜」と題しましたが、正規表現が使えるので文字種の制約なども行えるはずです。

複数のパッケージをインポートする

import (
    "time"
    "list"
)

Goの記法が通用します。

リスト

article_no_list: [...int]

この例ではint型のリストを定義しており、下のようなデータが与えられることを期待しています。要素の数は無制限です。

article_no_list: [1, 2, 3, 4, 5]

stringの場合には型の部分を変更し、[...string]のようにします。

リストの要素数を制限する

import "list"

article_no_list: [...int]
article_no_list: list.MaxItems(10)  // 10件以内

MinItemsを使うと最小数を決めることもできます。

import "list"

article_no_list: [...int]
article_no_list: list.MaxItems(1) & list.MaxItems(10)  // 1件以上、10件以内

構造体を定義する

#Article: {  // <-ココ
  ・・・(省略)・・・
}

他の構造の一部として参照可能な構造体を定義する場合、キー名の先頭に#を付加します。ここで定義した名称は「型」のひとつとして指定できるようになるので、利用する場合は下記のようにします。

article: #Article

もちろんリストの型として使うこともできます。

articles: [...#Article]

実装例

上記に紹介した制約を使って、実装例を作ってみました。あまり実例っぽくないですが、ご参考になればと思います。

ディレクトリ構造
$ tree .
.
├── cue_test.go
└── testdata
    ├── sample.json
    └── schema.cue
schema.cue
import (
    "time"
    "list"
)

body: {
    createDate: string
    createDate: time.Format(time.RFC3339)
    no: int & >=0 & <=999_999
    rate: number
    title: string & =~"^.{0,20}$"
    rows: [...#Row]
    rows: list.MaxItems(5)
    level:  *"easy" | "medium" | "high"
}

#Row: {
    no: null | int
    flag: bool
}
sample.json
{
    "body": {
        "createDate": "2022-11-02T15:04:05+09:00",
        "no": 100000,
        "rate": 0.25,
        "title": "テストタイトル",
        "rows": [
            {
                "no": 1,
                "flag": false
            },
            {
                "no": null,
                "flag": true
            }
        ],
        "level": "medium"
    }
}
cue_test.go
package sample_test

import (
	_ "embed"
	"testing"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/cuecontext"
	cueerrors "cuelang.org/go/cue/errors"
)

//go:embed testdata/schema.cue
var cueSchema string

//go:embed testdata/sample.json
var jsonData string

func TestJsonValidation(t *testing.T) {

	// cueのコンテキスト生成、テストデータの読み込みを行います。
	// 今回のテストデータはembedを使って直接stringとして取り込みました。
	c := cuecontext.New()

	schema := []byte(cueSchema)
	cueSchema := c.CompileBytes(schema)

	data := []byte(jsonData)
	cueJson := c.CompileBytes(data)

	// Unifyでとりあえず結合
	u := cueSchema.Unify(cueJson)

	// Validateで整合性チェック
	err := u.Validate(
		cue.Concrete(true), // これを指定しないとoptionalチェックされない
	)
	if err != nil {
		// ValidateでエラーがあればFAILさせて内容を出力
		t.Errorf("validation error!:\n%s", cueerrors.Details(err, nil))
	}
}

この状態でgo testPASSします。スキーマに対してjsonの内容が違反していないことを確認できました。

まとめ

以上です。前の記事と合わせ、CUEを用いてyamlとjsonのvalidationを行ってみました。ご参考になれば幸いです。

ではまた!

脚注
  1. CUEでは数値リテラルを記述する際に_(アンダースコア)を挟むことができ、大きい数値の桁区切りとして用いて値を見やすくするために利用できます。_の有無によって値の意味が変わることはありません。Pythonに似ているなと思いました。
    なお、999999でも99999_9でも99_9999でも同じ挙動となります。区切り文字とみなされない位置の_999999999999_はエラーになるようです。 ↩︎

Discussion

ログインするとコメントできます