2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

かつて serde_valid なるものを開発していた

Posted at
1 / 16

かつて 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)
}

...このオプションは、無駄ではないだろうか? :thinking:
本当に、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 を使わないから公開する理由がなくて、
公開する理由がないから開発していない。

しかし、誰か欲しい人はいるんじゃあないかなぁ...
と思ったので興味ある人は私に元気をください :bow:

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?