🪺

nest_structを使ってネストした構造体を定義する

に公開

モチベーション

例えばgolangであれば匿名構造体を使って、簡潔に定義することができます。

type User struct {
    ID int `json:"id"`
    Profile struct {
        Name string `json:"name"`
        Bio  string `json:"bio"`
    } `json:"profile"`
    Stats struct {
        FollowerCount int `json:"follower_count"`
        FolloweeCount int `json:"followee_count"`
    } `json:"stats"`
}

rustでもこんな感じにネストした構造体を定義したいです。

rustでは nest_struct を使うことで良い感じに解決することができました。
nest_structについて紹介する日本語の記事が無さそうなので、紹介記事を書きます。

nest_structのバージョンは0.5.5を前提とします。

nest_struct

https://crates.io/crates/nest_struct

名前の通りネストした構造体を定義するためのマクロを提供してくれます。

前述のgolangと同等の構造体を定義すると以下のようになります。

use nest_struct::nest_struct;

#[nest_struct]
struct User {
    id: String,
    profile: nest! {
        name: String,
        bio: String,
    },
    stats: nest! {
        follower_count: i64,
        followee_count: i64,
    },
}

cargo expand でマクロを展開すると以下のようになりました。

struct UserProfile {
    name: String,
    bio: String,
}
struct UserStats {
    follower_count: i64,
    followee_count: i64,
}
struct User {
    id: String,
    profile: UserProfile,
    stats: UserStats,
}

あるいは構造体の名前を自分で指定することもできます。

#[nest_struct]
struct User {
    id: String,
    profile: Profile! {
        name: String,
        bio: String,
    },
    stats: Stats! {
        follower_count: i64,
        followee_count: i64,
    },
}

cargo expand

struct Profile {
    name: String,
    bio: String,
}
struct Stats {
    follower_count: i64,
    followee_count: i64,
}
struct User {
    id: String,
    profile: Profile,
    stats: Stats,
}

derive

今回やりたいことはserdeを使うことです。
nest_structはルートの構造体のderiveを継承してくれるので楽ですね。

#[nest_struct]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    id: String,
    profile: nest! {
        name: String,
        bio: String,
    },
    stats: nest! {
        follower_count: i64,
        followee_count: i64,
    },
}

↓ 展開後のイメージ(実際には #[derive()] も展開されて impl のコードになります)

#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserProfile {
    name: String,
    bio: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserStats {
    follower_count: i64,
    followee_count: i64
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    id: String,
    profile: UserProfile,
    stats: UserStats,

User, UserProfile, UserStatsのそれぞれに Debug Clone Serialize Deserialize を実装したコードが生成されました。

Vec, Option

Vec Option にしたいことも多いでしょう。
やってみたら普通にできました。すごい 👏

#[nest_struct]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    id: String,
    profile: Vec<nest! {
        name: String,
        bio: String,
    }>,
    stats: Option<Stats! {
        follower_count: i64,
        followee_count: i64,
    }>,
}

↓ 展開後のイメージ

#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserProfile {
    name: String,
    bio: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Stats {
    follower_count: i64,
    followee_count: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    id: String,
    profile: Vec<UserProfile>,
    stats: Option<Stats>,
}

enum, attribute

enum#[serde()] もいけます。
derive と同様に #[serde()] も継承されます。

#[nest_struct]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
struct User {
    id: String,
    theme: Option<nest! {
        Light,
        Dark,
        Custom {
            colors: nest! {
                #[serde(rename = "bg")]
                background: String,
                text: String,
            }
        },
    }>,
}

↓ 展開後のイメージ

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
struct UserThemeCustomColors {
    #[serde(rename = "bg")]
    background: String,
    text: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
enum UserTheme {
    Light,
    Dark,
    Custom { colors: UserThemeCustomColors },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
struct User {
    id: String,
    theme: Option<UserTheme>,
}

柔軟性

ルートの #[serde()] を継承したくない時や、内部の構造体にだけ追加の derive を定義したい時もあると思います。
そういう時は nest! の中に struct / enum キーワードで定義します。

#[nest_struct]
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct User {
    id: String,
    theme: nest! {
        #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
        enum Theme {
            Light,
            Dark,
            Custom {
                colors: nest! {
                    #[serde(rename = "bg")]
                    background: String,
                    text: String,
                },
            },
        }
    },
}

↓ 展開後のイメージ

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct ThemeCustomColors {
    #[serde(rename = "bg")]
    background: String,
    text: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
enum Theme {
    Light,
    Dark,
    Custom {
        colors: ThemeCustomColors,
    },
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct User {
    id: String,
    theme: UserTheme,
}

この形式の場合、 ThemeCustomColorTheme のattribute( derive#[serde()] )を継承することに注意してください。
「ルートのものを継承する」というより、正確には「最も近い祖先の struct / enum を書いて定義されたものを継承する」という風に思えます。

ここまでできれば基本的なユースケースでは十分でないでしょうか。

ジェネリクスとライフタイム

serdeと併用する時に使うことはあまりないでしょうが、ジェネリクスやライフタイムにも対応しているようです。
例については公式のテストケースを参照してください。(ちゃんと試してない)

https://github.com/ZibanPirate/nest_struct/blob/main/tests/cases/struct/4_generic_type_and_lifetime.rs

Discussion