serdeを使ってAWSのリージョン名をrusoto_coreのenum Regionにserialize/deserializeする

2020.09.01

こんにちは、CX事業本部の夏目です。

RustでAWSのCLIツールを書いているのですが、設定ファイルに書いたリージョン名をrusoto_coreのenumRegionとして扱う方法について共有します。

特に何もしないでserializeしてみる

use rusoto_core::Region;
use serde::{Deserialize, Serialize};

fn main() {
    let data = Bucket {
        name: "test bucket".to_string(),
        region: Region::ApNortheast1,
    };
    println!("debug       : {:?}", data);
    let json = serde_json::to_string(&data).unwrap();
    println!("serialized  : {}", json);
    let deserialized: Bucket = serde_json::from_str(&json).unwrap();
    println!("deserialized: {:?}", deserialized);
}

#[derive(Debug, Serialize, Deserialize)]
struct Bucket {
    name: String,
    region: Region,
}
/Users/natsume.yuta/.cargo/bin/cargo run --color=always
   Compiling serde_region v0.1.0 (/Users/natsume.yuta/workspace/rust/serde_region)
    Finished dev [unoptimized + debuginfo] target(s) in 5.49s
     Running `target/debug/serde_region`
debug       : Bucket { name: "test bucket", region: ApNortheast1 }
serialized  : {"name":"test bucket","region":["ap-northeast-1",null]}
deserialized: Bucket { name: "test bucket", region: ApNortheast1 }

rusoto_coreのenum Regionを特に何もせずにserializeしてみる。
Region::ApNortheast1["ap-northeast-1", null]にserializeされている。

deserializeする場合も同様の表現にする必要があるので、このままだと設定ファイルなどに直接使うのは難しい。

"ap-northeast-1"Region::ApNortheast1として扱うようにしてみる

use rusoto_core::Region;
use serde::de::Unexpected;
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
use std::str::FromStr;

fn main() {
    let data = Bucket {
        name: "test bucket".to_string(),
        region: Region::ApNortheast1,
    };
    println!("debug       : {:?}", data);
    let json = serde_json::to_string(&data).unwrap();
    println!("serialized  : {}", json);
    let deserialized: Bucket = serde_json::from_str(&json).unwrap();
    println!("deserialized: {:?}", deserialized);
}

#[derive(Debug, Serialize, Deserialize)]
struct Bucket {
    name: String,
    #[serde(
        serialize_with = "custom_serialize",
        deserialize_with = "custom_deserialize"
    )]
    region: Region,
}

fn custom_serialize<S>(value: &Region, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(value.name())
}

fn custom_deserialize<'de, D>(deserializer: D) -> Result<Region, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    match Region::from_str(&s) {
        Ok(region) => Ok(region),
        Err(_) => Err(Error::invalid_value(
            Unexpected::Str(&s),
            &"invalid region name",
        )),
    }
}
/Users/natsume.yuta/.cargo/bin/cargo run --color=always
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/serde_region`
debug       : Bucket { name: "test bucket", region: ApNortheast1 }
serialized  : {"name":"test bucket","region":"ap-northeast-1"}
deserialized: Bucket { name: "test bucket", region: ApNortheast1 }

serializeとdeserializeを自分で実装して、リージョン名を直接enum Regionと解釈するようにしてみた。

crate serdeではAttributeに#[serde(serialize_with = "$path")]#[serde(deserialize_with = "$path")]を追記することでserializeやdeserializeを任意の関数にすることができます。

渡す関数は次のようにする必要があります (Tはserialize/deserializeしたい値の型)。

  • serialize_withでは
    fn<S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer
  • deserialize_withでは
    fn<'de, D>(deserializer: D) -> Result<T, D::Error>
    where
        D: serde::Deserializer

まとめ

以上リージョン名を直接rusoto_coreのenum Regionにserialize/deserializeする方法でした。
なにかの役に立てば幸いです。

今回のコードで使用したCargo.tomlの[dependencies]

[dependencies]
rusoto_core = "0.45"
serde = {version = "1", features = ["derive"]}
serde_json = "1"