かつて serde_valid なるものを開発してた
結局、 Rust を書かなくなったので止めちゃったけど、
OpenAPI の スキーマ から Rust の型を自動生成するツールを作った過程で、
serde_valid
という crate も昔作ってた。
こんな感じ
use serde_valid::Validate;
#[derive(Validate)]
struct TestStruct {
#[validate(length(min_length = 4, max_length = 4))]
val: String,
}
let s = TestStruct {
val: String::from("test"),
};
assert!(s.validate().is_ok());
気づいた人がいるかもしれないが、 validator がOpenAPI の仕様に
上手くマッチしなかったから作ったライブラリだ。
設計はかなり参考にしたが、下記点が異なる
- OpenAPI(がもとにした JsonSchema)のバリデーションに合わせて再編成
- 構造体だけでなく、列挙体にも対応(OpenAPIのバリデーション対応に必要)
- serdeのデシリアライズと同時にバリデーションを実行可能
基本的な機能
OpenAPI 準拠のバリデーション
テストケースのコードを見てみると、下記のような定義ができるようだ
(もう覚えていない)。
#[derive(Debug, Validate)]
struct TestStruct<'a> {
// Generic validator
#[validate(enumerate(5, 10, 15))]
// Numeric validator
#[validate(multiple_of = 5)]
#[validate(range(minimum = 5, maximum = 5))]
#[validate(range(exclusive_minimum = 4, exclusive_maximum = 6))]
int_value: i32,
// Generic validator
#[validate(enumerate(5.0, 10.0, 15.0))]
// Numeric validator
#[validate(multiple_of = 5.0)]
#[validate(range(minimum = 5.0, maximum = 5.0))]
#[validate(range(exclusive_minimum = 4.0, exclusive_maximum = 6.0))]
float_value: f32,
// Generic validator
#[validate(enumerate("12345", "67890"))]
// String validator
#[validate(length(min_length = 5, max_length = 5))]
#[validate(pattern = r"^\d{5}$")]
string_value: String,
// Generic validator
#[validate(enumerate("12345", "67890"))]
// String validator
#[validate(length(min_length = 5, max_length = 5))]
#[validate(pattern = r"^\d{5}$")]
str_value: &'a str,
// Generic validator
#[validate(enumerate(5, 10, 15))]
// Numeric validator
#[validate(multiple_of = 5)]
#[validate(range(minimum = 5, maximum = 5))]
optional_value: Option<i32>,
// Generic validator
#[validate(enumerate(5, 10, 15))]
// Array validator
#[validate(unique_items)]
#[validate(items(min_items = 3, max_items = 3))]
// Numeric validator
#[validate(multiple_of = 5)]
#[validate(range(minimum = 5, maximum = 15))]
vec_value: Vec<i32>,
// Nested validator
#[validate]
nested_struct: TestInnerStruct<'a>,
// Nested vec validator
#[validate]
// Array validator
#[validate(items(min_items = 1, max_items = 1))]
nested_vec_struct: Vec<TestInnerStruct<'a>>,
}
余談)下記のテストケースはお気に入り。
#[test]
fn test_validate_emoji_string_length() {
assert!(validate_string_length(
"😍👺🙋🏽👨🎤👨👩👧👦",
Some(5),
Some(5)
));
}
ただし、世間一般の文字数の数え方は実装依存なので
serde_valid
が正しすぎて 良くないこと が起こるかもしれない。
使い方
基本的には validator と同様だ。
構造体に Derive でバリデーションを定義し、serde を用いて変換する。
しかし、 私の個人的な好み から、データ変換時にバリデーションを実行する
インターフェースが公開されている。
#[derive(Debug, Validate, Deserialize)]
struct TestStruct{
...
}
// 参考にした validator と同じバリデーション方法
let s = serde::from_value<TestStruct>(json!({"val": 1234})).unwrap();
assert!(s.validate().is_ok());
// 新たに追加したバリデーション方法 serde_valid::form_value
let s = serde_valid::from_value::<TestStruct, _>(json!({ "val": 1234 })).unwrap();
serde_valid
だけを含め、 serde
をプロジェクトから外すことで、
バリデーションの実行を忘れずに開発することが可能だ。
( serde::from_value
を呼ぶ方法がなくなり、 serde_valid::from_value
を強制できるので)
バリデーションで保証されたデータであることを確信したうえで、プロジェクトの開発ができる。
エラーハンドリング
エラー発生時は Web での使用を想定して、 JSON に変換できる構造体を返すようにしている。
#[derive(Debug, Validate, Deserialize)]
struct TestStruct {
#[validate(range(minimum = 0, maximum = 1000))]
val: i32,
}
// エラーの型に応じた構造体を返す。
let err = serde_valid::from_value::<TestStruct, _>(json!({ "val": 1234 })).unwrap_err();
// エラーは文字列に変換すると JSON フォーマットになるので、 API サーバの戻り値として使える。
assert_eq!(
serde_json::from_str::<serde_json::Value>(&err.to_string()).unwrap(),
json!({"val": ["`1234` must be in `0 <= value <= 1000`, but not."]})
);
少しだけ賢いのは、NewType や構造体・列挙体などの種類に応じて
適切な JSON メッセージを返す点だ。
#[derive(Validate)]
struct TestStruct {
#[validate]
named_fields_struct: StructNamedFields,
#[validate]
unnamed_fields_struct: StructUnnamedFields,
#[validate]
single_unnamed_fields_struct: StructSingleUnnamedFields,
#[validate]
named_fields_enum: EnumNamedFields,
#[validate]
unnamed_fields_enum: EnumUnnamedFields,
#[validate]
single_unnamed_fields_enum: EnumSingleUnnamedFields,
}
#[derive(Validate)]
struct StructNamedFields {
#[validate(range(maximum = 0))]
val: i32,
}
#[derive(Validate)]
struct StructSingleUnnamedFields(#[validate(range(maximum = 0))] i32);
#[derive(Validate)]
struct StructUnnamedFields(
#[validate(range(maximum = 0))] i32,
#[validate(range(maximum = 0))] i32,
);
#[derive(Validate)]
enum EnumNamedFields {
Value {
#[validate(range(maximum = 0))]
val: i32,
},
}
#[derive(Validate)]
enum EnumSingleUnnamedFields {
Value(#[validate(range(maximum = 0))] i32),
}
#[derive(Validate)]
enum EnumUnnamedFields {
Value(
#[validate(range(maximum = 0))] i32,
#[validate(range(maximum = 0))] i32,
),
}
let s = TestStruct {
named_fields_struct: StructNamedFields { val: 5 },
unnamed_fields_struct: StructUnnamedFields(5, 5),
single_unnamed_fields_struct: StructSingleUnnamedFields(5),
named_fields_enum: EnumNamedFields::Value { val: 5 },
single_unnamed_fields_enum: EnumSingleUnnamedFields::Value(5),
unnamed_fields_enum: EnumUnnamedFields::Value(5, 5),
};
assert_eq!(
serde_json::to_value(&s.validate().unwrap_err()).unwrap(),
json!({
"named_fields_struct": [{
"val": [
"`5` must be in `value <= 0`, but not."
]
}],
"unnamed_fields_struct": [{
"0": [
"`5` must be in `value <= 0`, but not."
],
"1": [
"`5` must be in `value <= 0`, but not."
]
}],
"single_unnamed_fields_struct": ["`5` must be in `value <= 0`, but not."],
"named_fields_enum": [{
"val": [
"`5` must be in `value <= 0`, but not."
]
}],
"unnamed_fields_enum": [{
"0": [
"`5` must be in `value <= 0`, but not."
],
"1": [
"`5` must be in `value <= 0`, but not."
]
}],
"single_unnamed_fields_enum": ["`5` must be in `value <= 0`, but not."],
})
);
serde_valid
の気に入っていないところ
既に十分、公開可能な水準に達している。しかし、いくつかの理由で公開していない。
エラー型をオプションで選べるようになっている
コンパイルオプションで エラー型を serde_valid
が独自に定義した型ではなく、
serde::de::Error
を返すことができる。
そうした理由は、既存の大規模システムへの移植を容易にするためだ。
結局、 serde::de::Error
は 独自のエラーを返すとき文字列として返す ので、
serde
のエラーメッセージが JSON でデシリアライズできるのなら、 serde_valid
のエラーと見なせる...
という判断だ。
#[cfg(not(feature = "serde_error"))]
fn deserialize_with_validation_from_value(self) -> Result<T, crate::Error<Self::Error>> {
let model: T = serde_json::from_value(self)?;
model
.validate()
.map_err(|err| crate::Error::ValidationError(err))?;
Ok(model)
}
#[cfg(feature = "serde_error")]
fn deserialize_with_validation_from_value(self) -> Result<T, Self::Error> {
let model: T = serde_json::from_value(self)?;
model
.validate()
.map_err(|err| serde::de::Error::custom(err))?;
Ok(model)
}
...このオプションは、無駄ではないだろうか?
本当に、JSON形式で返すのは serde_valid
だけだといえるであろうか?
結局、 serde_valid
を導入したいなら、エラー部分を書き直してもらう方が正しい気がする。
特にエラー型をコンパイルオプションで切り替えるのは良くない判断だった
(エラーハンドリングの方法すらコンパイル方法によって変わってしまう)。
このオプションは消さなければならない(消すのは簡単)。でもまだやってない。
カスタムエラーで複数のフィールドを参照する方法の改善
参考にした validator
と同じく、 serde_valid
も独自のエラーが定義でき、
構造体/列挙体の複数のフィールドの要素を利用して定義できる。
しかし、今はこんな形になっている。
fn user_validation(
val1: &Vec<i32>,
val2: &f32,
literl: f32,
) -> Result<(), serde_valid::validation::Error> {
Ok(())
}
#[derive(Validate)]
struct TestStruct {
// 構造体/列挙体の引数と、リテラル型を指定できる。
#[validate(custom(user_validation(val2, 1.234)))]
val1: Vec<i32>,
val2: f32,
}
let s = TestStruct {
val1: vec![1, 2, 3, 4],
val2: 1.234,
};
assert!(s.validate().is_ok());
自明なものとして、 validate
が定義されたフィールドを第1引数として取り、
残りをユーザに指定させている。
しかし、複数のフィールドにまたがるエラーを、なぜフィールドで定義するのだろうか?
こうあるべきだ。
fn user_validation(
val1: &Vec<i32>,
val2: &f32,
literl: f32,
) -> Result<(), serde_valid::validation::Error> {
Ok(())
}
#[derive(Validate)]
// 構造体/列挙体の引数と、リテラル型を指定できる。
#[validate(custom(user_validation(val1, val2, 1.234)))]
struct TestStruct {
val1: Vec<i32>,
val2: f32,
}
let s = TestStruct {
val1: vec![1, 2, 3, 4],
val2: 1.234,
};
assert!(s.validate().is_ok());
既存のインターフェースの使い方を削除し(簡単)、
構造体のスコープにインターフェースを追加しなければならない(少し大変)。
おまけ
serde_valid
の計画を考えたころ、 serde
の制御を奪ってしまおうとした。
しかし、調査の結果、制御を奪うのは大変だと思ったのでラップする方向に転換した。
serde
は非公式の標準ライブラリになっている感があるが、以下の点が不満だ。
(他の人が不満に思っているのはどこだろう?)
- validation 機能をデフォルトで持っていない
- visitor パターンによる独自型の
Deserialize
Serialize
の実装が面倒 - ファイルから読み取るとき、エラーと解釈された場所(N行目M文字)を取得できない
serde
の代わりになるツールを作っても良かったが、
ほとんどのライブラリ設計者は私のトレイトを継承してくれないであろう...
最後に
結局、担当の製品上、会社で Rust 書かないから Rust 使わなくて、
Rust を使わないから公開する理由がなくて、
公開する理由がないから開発していない。
しかし、誰か欲しい人はいるんじゃあないかなぁ...
と思ったので興味ある人は私に元気をください