Configファイルのマイグレーションがしたい in Rust

# 必要になる場面

たとえばコマンドラインアプリケーションを作っているとき、ユーザー固有の情報をどこかに保存する場面がある。
開発が進み設定できる内容を追加したくなったとき、最新バージョンの期待するフォーマットと設定ファイルの内容をすり合わせる必要が生まれる。

## たとえば

- アクセスキーやトークンを用いてSNSへ投稿するCLIアプリケーションを作ってる場合
- 対応するSNSを増やしたい、など

# なんでこの話題でRust ?

- `type alias`を使ったら、書かなきゃいけないことをコンパイラが教えてくれていい感じだったから。
- 作業録でもある。

# 手順

- バージョン毎にConfigの構造体を作って
- `trait From`を実装してバージョン間のすり合わせを行い
- `type alias`で最新バージョンのsturctを強制する

これを満たすことでマイグレーションを実現する。
以下は、マイグレーションに対応したConfig系sturctを作成する流れ。

## 下準備

以下は本題で話したい事ではないが、無いと不明瞭なコードになってしまうので記載。

```rust
#[macro_use]
extern crate serde_derive;
extern crate toml;

#[derive(Debug)]
enum Error {
    SimpleError(String),
    TomlDeserializeError(::toml::de::Error),
    TomlSerializeError(::toml::ser::Error),
}
type Result<T> = ::std::result::Result<T, Error>;

macro_rules! impl_from_for_errors {                             
    ($( $id:ident : $type:ty => $translate:block ),*) => {
        $(
            impl From<$type> for Error {
                fn from($id: $type) -> Self $translate
            }
        )*
    }
}

impl_from_for_errors!(
    error: String => { Error::SimpleError(error) },
    error: ::toml::de::Error => { Error::TomlDeserializeError(error) },
    error: ::toml::ser::Error => { Error::TomlSerializeError(error) }
);                                                                       
```

`serde_derive`, `toml`はConfigをファイルに保存するため。
`Result<T>`のエイリアスは、独自のエラー型に収束させるため。
この手法は[rust reference v1.6 エラーハンドリング 独自のエラー型を定義する](https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/error-handling.html#%E7%8B%AC%E8%87%AA%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E5%9E%8B%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B)に記載されている。
`impl_from_for_errors!`は、`try!`マクロ対応のための冗長な`From`実装をキュッとしたくて書いた。

## `struct ConfigV01`を定義

```rust
#[derive(Serialize, Deserialize, Debug)]
struct ConfigV01 {
    version: u8,
    access_key: String,
    access_secret: String,
}
```

後にConfigがどんどん増えることを前提に、`V**`サフィックスを付けた構造体を作成する。
`Serialize`, `Deserialize`は`serde_macro`から。

## `enum ConfigList`を定義

```rust
#[derive(Debug)]
enum ConfigList {
    V01(ConfigV01),
}
```

各バージョン毎に生まれる構造体を一緒に扱うため、enumを定義する。
デシリアライズしてこのenumに含まれるいずれかの`variant`を返したり、マイグレーションの時に便利になる。

## `str_to_config()`を実装

```rust
fn str_to_config(contents: &str) -> Result<ConfigList> {
    let parsed = contents.parse::<::toml::Value>?;
    let version = parsed
        .as_table()
        .and_then(|table| table.get("version"))
        .and_then(|value| value.as_integer())
        .ok_or("Missing version".to_string())?;
    let config = match version {
        1 => ConfigList::V01(parsed.try_into()?),
        _ => return Err("Unknown config version".to_string().into()),
    }
    Ok(config)
}
```

ファイルから読み出す以上、このコードは必要と考えて記載。
toml文字列から~~シリアライズ体(なんと言えばよいのか…)~~`toml::value`に変換して、
バージョン番号を取り出した後に、それに応じたConfigへ更に変換している。

バージョン番号取り出し部分とmatch部分がなんだか長ったらしい。

`version`を文字列に変えて、コードネームで識別するのも面白いかもしれない。

## `struct ConfigV02`を定義

```rust
#[derive(Serialize, Deserialize, Debug)]
struct ConfigV02 {
    version: u8,
    access_key: String,
    access_secret: String,
    append_location: bool
}
```

例えば、投稿に位置情報を付与できる機能を作ったとして、ユーザーそれぞれに有効無効を設定できた方が望ましい。
ということで、設定に`append_location`という項目を追加したとする。

設定を追加したので`ConfigList`、`str_to_config()`に追加しておく。

```rust
enum ConfigList {
    V01(ConfigV01),
    V02(ConfigV02),
}

fn str_to_config(contents: &str) -> Result<ConfigList> {

    /* 省略 */

    let config = match version {
        1 => ConfigList::V01(parsed.try_into()?),
        2 => ConfigList::V02(parsed.try_into()?),
        _ => return Err("Unknown config version".to_string().into()),
    }
    Ok(config)
}
```

## type aliasを定義

```rust
type Config = ConfigV02;
```

設定の構造体が複数登場したところで`type alias`を定義する。
設定を利用するコードではこのエイリアスを参照するようにしておけば、設定のバージョンが変わったときの負担が軽減できる。

## `ConfigV01`から変換するために、`trait From`を実装

```rust
impl From<ConfigV01> for ConfigV02 {
    fn from(c: ConfigV01) -> Self{
        ConfigV02 {
            version: 2,
            access_key: c.access_key.clone(),
            access_secret: c.access_secret.clone(),
            append_location: true,
        }
    }
}
```

のちのマイグレーションコードに繋がる、`ConfigV02`を`ConfigV01`から生み出すためのコード。
`version`と新たに追加される(初期値が必要となる)`append_location`には新たな値を与えるところがポイントか。

## `migrate()`を実装

```rust
fn migrate(config: ConfigList) -> Config {
    let mut config = config;
    loop {
        config = match config {
            ConfigList::V01(c) => ConfigList::V02(c.into()),
            ConfigList::V02(c) => return c,
        }
    }
}
```

あとは簡単。`ConfigList::V01`なら`ConfigList::V02`へと昇華させ、
`ConfigList::V02`なら中身を放り出してあげればよい。

# 次のバージョンを書く時に効く

この書き方は次のバージョンを書く時に効く。コンパイラが怒ってくれるから。

## `struct ConfigV03`を定義


```rust
#[derive(Serialize, Deserialize, Debug)]
struct ConfigV03 {
    version: u8,
    access_key: String,
    access_secret: String,
    append_location: bool,
    append_signature: bool,
}
```

例えば、投稿に何らかの署名を追加できるようにしたとする。
ということで、設定に`append_signature`という項目を追加したとする。

## `type alias`は`ConfigV03`を示すようにする

```rust
type Config = ConfigV03;
```

ここで`cargo build`を走らせるとコンパイラが`migrate()`に対して怒ってくれる。

```
error[E0308]: mismatched types                                                                           
  --> src/main.rs:37:46                                                                                  
   |                                                                                                     
37 |                 ConfigList::V02(c) => return c,                                                     
   |                                              ^ expected struct `ConfigV03`, found struct `ConfigV02`
   |                                                                                                     
   = note: expected type `ConfigV03`                                                                     
              found type `ConfigV02`                                                                     
```

## `migrate()`を短絡的に変更

ということで`migrate()`から`ConfigV03`を得られるように書き換える。
短絡的に、つまりは怒られたところの前後の文脈だけを見て書き換えてみる。

```rust
enum ConfigList {
    V01(ConfigV01),
    V02(ConfigV02),
    V03(ConfigV03),
}

fn migrate(config: ConfigList) -> Config {
    let mut config = config;
    loop {
        config = match config {
            ConfigList::V01(c) => ConfigList::V02(c.into()),
            ConfigList::V02(c) => ConfigList::V03(c.into()),
            ConfigList::V03(c) => return c,
        }
    }
}
```

前後の文脈だけで導いたコードを書いた。
ここで`cargo build`を走らせる。

```rust
error[E0277]: the trait bound `ConfigV03: std::convert::From<ConfigV02>` is not satisfied
  --> src/main.rs:38:57
   |
38 |                 ConfigList::V02(c) => ConfigList::V03(c.into()),
   |                                                         ^^^^ the trait `std::convert::From<ConfigV02>` is not implemented for `ConfigV03`
   |
   = note: required because of the requirements on the impl of `std::convert::Into<ConfigV03>` for `ConfigV02`
```

当然`trait From`を実装していないのでコンパイラが怒ってくれる。
この、雰囲気コーディングをしようが正道に導いてくれる点が重要でいいところだなーと感じている。

## `trait From`を`ConfigV03`に実装

```rust
impl From<ConfigV02> for ConfigV03 {
    fn from(c: ConfigV02) -> Self {
        ConfigV03 {
            version: 3,
            access_key: c.access_key.clone(),
            access_secret: c.access_secret.clone(),
            append_location: c.append_location,
            append_signature: true,
        }
    }
}
```

はい。コンパイラが許してくれるようになりました。
これで新しい設定の追加はおしまいです。

新しいバージョンの構造体を定義して、「これを使います！」と`type alias`を更新してから一本道でした。

# まとめ

- 導かれるコーディングは素敵
- コード外のリソースをハンドリングするのは面倒くさい