serdeを介したデータのシリアル化。 最近、私はTOML形式でサードパーティのデータソースを操作するためのRustコードを書きました。 他の言語では、TOMLライブラリを使用してデータをロードし、プログラムを実行しますが、Rustのシリアル化ライブラリであるserdeについて聞いたので、試してみることにしました。
詳細-カットの下。
基本
以下は、使用しているデータの簡略化された例です。
manifest-version = "2" # ... ... [renames.oldpkg] to = "newpkg"
これはかなり単純なデータ形式であり、シリアライズ/デシリアライズ可能なRust構造を書くのは非常に簡単です。
#[derive(Serialize, Deserialize)] struct ThirdPartyData { #[serde(rename = "manifest-version")] manifest_version: String, // ... ... renames: BTreeMap<String, BTreeMap<String, String>>, }
この構造は入力データ構造に対応し、 manifest-version
有効なRust識別子でmanifest-version
ないため、私が記述した追加コードはserde(rename = "blah")
属性serde(rename = "blah")
です。
改善された構造
強く型付けされた言語のコミュニティでは、「誤った状態を表現できないようにする」という声明が広まっています。 これは、プログラムがデータの性質について何らかの仮定を行っている場合、型システムを使用してそのことを確認する必要があることを意味します。
manifest-version
フィールドを例にとります。 これは私が興味を持っているデータの一部ではなく、メタ情報、つまり必要なデータに関する情報です。 シリアル化するとき、このフィールドは「2」に設定する必要があります。 逆シリアル化中に「2」でない場合は、=>データ読み取り停止で動作しない別のファイル形式にする必要があります。 データを使用するコードはこのフィールドで動作する必要はなく、何かがこのフィールドの値を変更する場合、これは将来の問題につながります。 誰もフィールドを読んでいないことを確認するための最良の方法は、メモリを無駄にせずに完全に削除することです。
renames
フィールドは、他の問題を作成renames
ます。 間違いなく私に興味のあるデータですが、奇妙なネストされた辞書の形で提示されます。 空の辞書と外部辞書のキーの1つとの対応はどういう意味ですか? マッピング「古い名前」=>「新しい名前」はBTreeMap <String、String>である必要があり、無効な状態は発生しません。 つまり、Rust構造を次のようにしたいのです。
#[derive(Serialize, Deserialize)] struct ThirdPartyData { // `manifest_version`! // ... ... renames: BTreeMap<String, String>, }
残念ながら、これは私が必要とすることをしません:コードはmanifest-version
に正しい値manifest-version
割り当てられていることを確認しません。
最初の試み:自分でやる
サーブderive
マクロがこれを実行できない場合、手動で実行する必要がありますよね? これに基づいて、 serde::Serialize
とserde::Deserialize
ThirdPartyData
構造のserde::Deserialize
をserde::Deserialize
化します。 要するに、うまくいきました! しかし、書くのは面倒で理解しにくいものでした。
構造のシリアル化に関するserdeのドキュメントは単純で、プロセスは単純ですserde::Serializer
で必要なメソッドを呼び出す構造のserialize
メソッドを記述すれば完了です。 ただし、 逆シリアル化のドキュメントははるかに複雑です。構造に逆シリアル化を実装するだけでなく、 serde::Visitor
が実装されているヘルパー構造も必要です。
ドキュメントでは、長いDeserialize
例はi32
ようなプリミティブ型のみの逆シリアル化のスペルを示しています。 構造の逆シリアル化には、 ドキュメントの別のページが必要であり、実装ははるかに複雑です。
私が言ったように、私はそれを機能させましたが、このコードをプロジェクトにコミットしたとき、私はその作業に満足していませんでした。
2回目の試行:フィールド属性
私の仕事の一部は、 Serialize
とDeserialize
手動で実装することでした。そのため、構造内のすべてのフィールドを処理するコードを作成する必要がありましたが、serdeはほとんどを手動で実行できました。
判明したように、serdeで提供される多くのフィールド属性の1つは、 serde(with = "module")
属性 serde(with = "module")
です。 この属性は、フィールドのシリアライズ/デシリアライズに使用されるdeserialize
およびdeserialize
関数を含むモジュールの名前を示します。残りの構造は通常どおりserdeによって処理されます。
renames
フィールドrenames
はrenames
これは素晴らしいことです。 それでも、 Visitor
で作業するためにいくつかの努力をしなければなりませんでしたが、構造内のすべてのフィールドではなく、1つのフィールドに対してのみこれを行う必要がありました。
manifest-version
フィールドを使用する場合、これは役に立ちませんでした。 manifest-version
フィールドが必要ないため、属性を追加できるものはありませんでした。
そこで、ため息をつき、このコードを削除して、別の方法で問題を解決しようとしました。
成功:中間構造の使用
振り返って、解決した問題を確認してください。
- 使いやすいRust構造を作成できますが、入力形式と完全には一致しません
- 入力形式に正確に一致するRust構造を記述できますが、これらの構造は使用するのにそれほど便利ではありません
- 入力フォーマットを便利な構造に直接変換すると、多くの冗長コードを書く必要があります
あなたはすでに何をすべきかを推測していると思います:入力形式を、それと完全に一致するRust構造に正確に変換するためにserdeを使用してから、データを使用に便利なRust構造に手動で変換します。
上記で簡単に説明したバージョンのThirdPartyData
を使用していますが、デシリアライズコードは次のようになります。
impl<'de> serde::Deserialize<'de> for ThirdPartyData { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { use serde::de::Error; // , . #[derive(Deserialize)] struct EncodedThirdPartyData { #[serde(rename = "manifest-version")] pub manifest_version: String, // ... ... pub renames: BTreeMap<String, BTreeMap<String, String>>, } // `Deserialize` , // serde . let input = EncodedThirdPartyData::deserialize(deserializer)?; // `manifest_version` . if input.manifest_version != "2" { return Err(D::Error::invalid_value( ::serde::de::Unexpected::Str(&input.manifest_version), &"2", )); } // `renames` . let mut renames = BTreeMap::new(); for (old_pkg, mut inner_map) in input.renames { let new_pkg = inner_map .remove("to") .ok_or(D::Error::missing_field("to"))?; renames.insert(old_pkg, new_pkg); } // "" // . Ok(Channel { renames: renames, }) } }
中間構造はデシリアライズ可能なデータを所有しているため、追加のメモリ割り当てなしで便利な構造を構築するためにそれを部分に解析できます...さて、 renames
辞書の構造を変更するためにいくつかのBTreeMap
を作成する必要がありますが、キーと値をコピーする必要はありません。
構造をシリアル化するには、同じ中間構造を使用して逆の順序で作業できますが、構造はデータを所有しているため、便利な構造を解析してデータを受信または複製する必要があります。 これらのオプションはあまり魅力的ではないため、 String
型を&str.serde
で置き換える別の構造を使用して、同じ方法でシリアル化します。これは、メモリを割り当てずにシリアル化できることも意味します。
impl serde::Serialize for ThirdPartyData { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { // , // `&str` `String`. #[derive(Serialize)] struct EncodedThirdPartyData<'a> { #[serde(rename = "manifest-version")] manifest_version: &'a str, // ... ... renames: BTreeMap<&'a str, BTreeMap<&'a str, &'a str>>, } // `renames` // . let mut renames = BTreeMap::new(); for (old_pkg, new_pkg) in self.renames.iter() { let mut inner = BTreeMap::new(); inner.insert("to", new_pkg.as_str()); renames.insert(old_pkg.as_str(), inner); } let output = EncodedThirdPartyData { // , // . manifest_version: "2", renames: renames, }; output.serialize(serializer) } }
その結果、ほぼ完全に自動化されたシリアル化/逆シリアル化を備えた構造が得られました。これには、いくつかのチェックと変換を実行するコードの数行が含まれます。