CUEのスキーマ定義に使う制約をチートシートっぽくまとめる
いきさつ
先日、こちらの記事で「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
パッケージに準拠しており、定数の定義も同一とのことです。
省かれるかもしれない値(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
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
}
{
"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"
}
}
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 test
にPASS
します。スキーマに対してjsonの内容が違反していないことを確認できました。
まとめ
以上です。前の記事と合わせ、CUEを用いてyamlとjsonのvalidationを行ってみました。ご参考になれば幸いです。
ではまた!
-
CUEでは数値リテラルを記述する際に
_
(アンダースコア)を挟むことができ、大きい数値の桁区切りとして用いて値を見やすくするために利用できます。_
の有無によって値の意味が変わることはありません。Pythonに似ているなと思いました。
なお、999999
でも99999_9
でも99_9999
でも同じ挙動となります。区切り文字とみなされない位置の_999999
や999999_
はエラーになるようです。 ↩︎
Discussion