Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Yuuka - Introduction

Yuuka is a Rust procedural macro library that lets you define complex, deeply nested struct and enum hierarchies using a concise, JSON-like DSL syntax. It is built on top of serde for seamless serialization and deserialization.

Installation

Add the following to your Cargo.toml:

[dependencies]
yuuka = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

serde and serde_json are optional but commonly used alongside yuuka for serialization support.

Core Macros

Yuuka exports three procedural macros:

MacroPurpose
derive_struct!Define nested struct hierarchies with a JSON-like DSL
derive_enum!Define enum types with various variant forms
auto!Construct instances of types generated by the above macros with simplified syntax

See also:

  • Attributes & Visibility — Extra derive macros, attribute propagation, visibility control, and cross-crate usage
  • Examples — Real-world examples and generated code structure

Quick Start

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};
use yuuka::{derive_struct, auto};

derive_struct!(
    #[derive(PartialEq, Serialize, Deserialize)]
    GameConfig {
        title: String,
        window: Window {
            width: u32,
            height: u32,
            fullscreen: bool,
        },
        plugins: [Plugin {
            name: String,
            enabled: bool,
        }],
    }
);

let config = auto!(GameConfig {
    title: "My Game".to_string(),
    window: {
        width: 1920,
        height: 1080,
        fullscreen: true,
    },
    plugins: vec![
        Plugin {
            name: "Audio".to_string(),
            enabled: true,
        },
    ],
});
}

This single derive_struct! call automatically generates three independent structs — GameConfig, Window, and Plugin — all with #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]. The auto! macro then allows constructing instances using { } blocks for anonymous/inline sub-structs without knowing their generated names.

Documentation Index

DocumentDescription
derive_struct!Struct definition macro — nested structs, anonymous structs, Vec/Option types, default values, inline enums, reference types
derive_enum!Enum definition macro — unit/struct/tuple variants, nested enums, default values
auto!Instance construction macro — simplified syntax for anonymous types, enum paths, spread expressions
Attributes & VisibilityDerive macros, attribute propagation, #[macros_recursive], field-level attributes, visibility, #[macro_export], cross-crate usage
ExamplesReal-world examples, generated code structure explanation

derive_struct! Macro

The derive_struct! macro is the core of yuuka. It lets you define complex nested struct hierarchies using a concise, JSON-like DSL. All inline types are automatically extracted into independent top-level struct/enum definitions.

Basic Syntax

#![allow(unused)]
fn main() {
use yuuka::derive_struct;

derive_struct!(
    Root {
        field1: String,
        field2: i32,
    }
);
}

This generates a struct with #[derive(Debug, Clone, Default)] automatically applied:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Default)]
pub(crate) struct Root {
    pub field1: String,
    pub field2: i32,
}
}

Nested Structs

Define inline sub-structs directly inside a parent struct by specifying FieldName { ... } as the type:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        info: Info {
            name: String,
            detail: Detail {
                level: u32,
                score: f64,
            },
        },
    }
);
}

This generates three independent structs: Root, Info, and Detail. Each is a normal Rust struct with all fields public within the generated module.

You can nest to arbitrary depth:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        a: A {
            b: B {
                c: C {
                    d: D {
                        value: String,
                    },
                },
            },
        },
    }
);
}

Anonymous Structs

Omit the type name to create auto-named structs:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        data: {
            value: String,
        },
    }
);
}

The anonymous struct is automatically named _Root_0_anonymous. When there are multiple anonymous types, they are numbered sequentially:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        a: {
            b: String,
        },
        c: {
            d: f64,
        },
    }
);
// Generates: _Root_0_anonymous (for a), _Root_1_anonymous (for c)
}

Anonymous structs can be deeply nested:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        a: {
            b: String,
            c: {
                d: f64 = std::f64::consts::PI,
                e: {
                    f: bool = false,
                },
            },
            g: {
                h: i32 = -114514,
            },
        },
    }
);

let root = Root::default();
assert_eq!(root.a.c.d, std::f64::consts::PI);
assert!(!root.a.c.e.f);
assert_eq!(root.a.g.h, -114514);
}

Tip: Use the auto! macro to construct anonymous struct instances without knowing the generated names.


Array (Vec) Types

Use [Type { ... }] syntax to define Vec<Type> fields:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        items: [Item {
            name: String,
            count: u32,
        }],
    }
);
// Generates field: items: Vec<Item>
}

Anonymous Array Elements

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        items: [{
            name: String,
        }],
    }
);
// Generates: items: Vec<_Root_0_anonymous>
}

Enum Arrays

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        statuses: [enum Status {
            Active,
            Inactive,
        }],
    }
);
// Generates: statuses: Vec<Status>
}

Anonymous Enum Arrays

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        values: [enum {
            Momoi,
            Midori,
            Yuzu,
            Arisu,
        }],
    }
);
// Generates: values: Vec<_Root_0_anonymous>
}

Optional (Option) Types

Append ? to the field name to wrap the type in Option<T>:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        required: String,
        optional?: String,
    }
);
// Generates:
//   required: String,
//   optional: Option<String>,
}

Option with Inline Struct

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        detail?: Detail {
            info: String,
        },
    }
);
// Generates: detail: Option<Detail>
}

Option with Anonymous Struct

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        data?: {
            value: String,
        },
    }
);
// Generates: data: Option<_Root_0_anonymous>
}

Option with Inline Enum

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        status?: enum Status {
            Active,
            Inactive,
        },
    }
);
// Generates: status: Option<Status>
}

Option Inside Enum Variants

The ? syntax also works inside enum struct variants:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        action?: enum Action {
            Midori { detail?: String },
        },
    }
);
}

Default Values

Assign default values with = after the type:

#![allow(unused)]
fn main() {
derive_struct!(
    Config {
        host: String = "localhost".to_string(),
        port: u16 = 8080,
        debug: bool = false,
    }
);

let config = Config::default();
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 8080);
assert_eq!(config.debug, false);
}

Behavior

  • Fields with an explicit = value use that value in the generated impl Default.
  • Fields without = value use Default::default() (e.g., 0 for numbers, "" for String, false for bool).
  • If any field has a custom default, the macro generates a manual impl Default block instead of #[derive(Default)].

Default Values for Arrays

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        // Empty by default
        items: [Item {
            name: String = "unnamed".to_string(),
        }],
    }
);

let root = Root::default();
assert_eq!(root.items.len(), 0); // Vec is empty by default
}

With an explicit array default:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        items: [Item {
            name: String = "unnamed".to_string(),
        }] = vec![Item { name: "first".to_string() }],
    }
);

let mut root = Root::default();
assert_eq!(root.items.len(), 1);
assert_eq!(root.items[0].name, "first");

// New items get the Item-level defaults
root.items.push(Default::default());
assert_eq!(root.items[1].name, "unnamed");
}

Default Values for Enums

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(PartialEq)]
    Root {
        member: enum Member {
            Momoi,
            Midori,
            Yuzu,
            Arisu,
        } = Midori,
    }
);

let root = Root::default();
assert_eq!(root.member, Member::Midori);
}

Enum array with defaults:

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(PartialEq)]
    Root {
        members: [enum Member {
            Momoi,
            Midori,
            Yuzu,
            Arisu,
        } = Midori] = vec![Member::Arisu],
    }
);

let mut root = Root::default();
assert_eq!(root.members[0], Member::Arisu); // From the Vec default
root.members.push(Default::default());
assert_eq!(root.members[1], Member::Midori); // From the enum default
}

Inline Enums

Define enums inline within struct fields:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        status: enum Status {
            Active,
            Inactive,
        },
    }
);
}

Variant Forms

Enum variants support three forms:

Unit variants — no associated data:

#![allow(unused)]
fn main() {
a: enum Member {
    Momoi,
    Midori,
    Yuzu,
    Arisu,
}
}

Struct-like variants — named fields, which can themselves contain inline structs and enums:

#![allow(unused)]
fn main() {
a: enum Member {
    Momoi {
        skill: Skill {
            name: String,
        },
    },
    Midori { skills: Vec<String>, level: usize },
    Yuzu {
        skill: SkillYuzu {
            name: String,
        },
        level: usize,
    },
    Arisu { level: usize },
}
}

Tuple-like variants — positional data, supporting inline structs, multiple fields, and static types:

#![allow(unused)]
fn main() {
a: enum Member {
    Momoi(Skill { name: String }),
    Midori(Vec<String>, usize),
    Yuzu(SkillYuzu { name: String }, usize),
    Arisu(usize),
}
}

Nested Enums

Enums can be nested inside enum variants:

#![allow(unused)]
fn main() {
// Enum in struct-like variant
derive_struct!(
    Root {
        a: enum Member {
            Arisu {
                ty: enum ArisuType {
                    Arisu,
                    Key,
                },
            },
        },
    }
);
let _ = Root { a: Member::Arisu { ty: ArisuType::Key } };

// Enum in tuple-like variant
derive_struct!(
    Root {
        a: enum Member {
            Arisu(enum ArisuType {
                Arisu,
                Key,
            }),
        },
    }
);
let _ = Root { a: Member::Arisu(ArisuType::Key) };
}

Enum Arrays in Variants

#![allow(unused)]
fn main() {
// Vec<enum> in struct-like variant
derive_struct!(
    Root {
        a: enum Member {
            Arisu {
                ty: [enum ArisuType {
                    Arisu,
                    Key,
                }],
            },
        },
    }
);

// Vec<enum> in tuple-like variant
derive_struct!(
    Root {
        a: enum Member {
            Arisu([enum ArisuType {
                Arisu,
                Key,
            }]),
        },
    }
);
}

Reference Types

Use path syntax to reference externally defined types:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Default)]
struct ExternalType {
    data: f64,
}

derive_struct!(
    Root {
        name: String,
        external: super::ExternalType,
    }
);
}

Note: Because generated types live inside a module (__Root), you typically need super:: to reference types from the outer scope. The exact path depends on where the external type is defined relative to the macro invocation.

Field names are flexible — both snake_case and PascalCase names work:

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        a_b: String,
        B: i32,
        c: super::C,
    }
);
}

derive_enum! Macro

The derive_enum! macro defines standalone enum types with the same DSL syntax style as derive_struct!. It supports all three variant forms, nested types, and default values.

Basic Syntax

#![allow(unused)]
fn main() {
use yuuka::derive_enum;

derive_enum!(
    enum Status {
        Active,
        Inactive,
    }
);
}

This generates an enum with #[derive(Debug, Clone)] automatically applied.


Variant Forms

Unit Variants

Simple variants with no associated data:

#![allow(unused)]
fn main() {
derive_enum!(
    enum Direction {
        North,
        South,
        East,
        West,
    }
);
}

Struct-like Variants

Variants with named fields. Fields can use inline struct definitions:

#![allow(unused)]
fn main() {
derive_enum!(
    enum Action {
        Move { x: f64, y: f64 },
        Attack {
            target: Target {
                id: u64,
                name: String,
            },
            damage: u32,
        },
    }
);
// Generates an independent `Target` struct alongside the `Action` enum.
}

Tuple-like Variants

Variants with positional data. Can contain inline structs, enums, and static types:

#![allow(unused)]
fn main() {
derive_enum!(
    enum Message {
        Text(String),
        Data(Payload { content: Vec<u8>, size: usize }),
        Multi(String, i32, bool),
    }
);
}

Mixed Variants

All three forms can coexist in a single enum:

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(PartialEq, Serialize, Deserialize)]
    enum Router {
        Home,
        User { id: u64, name: String },
        Error(String),
    }
);
}

Nested Enums

Enum variants can contain other inline enums:

In Tuple Variants

#![allow(unused)]
fn main() {
derive_enum!(
    enum Group {
        Millennium(enum Millennium {
            GameDevelopment(enum GameDevelopment {
                Momoi,
                Midori,
                Yuzu,
                Arisu,
            }),
            CAndC,
            Veritas,
        }),
    }
);

let _ = Group::Millennium(Millennium::GameDevelopment(GameDevelopment::Yuzu));
}

Anonymous Nested Enums

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(PartialEq)]
    enum Root {
        A,
        B(i32),
        C { a: String, b: i32 },
        D(enum {
            E,
            F(i32),
            G { a: String, b: i32 },
        }),
    }
);
}

Anonymous enums inside variants are named like _Root_0_anonymous. You can reference them via the module:

#![allow(unused)]
fn main() {
let _ = Root::D(__Root::_Root_0_anonymous::E);
}

Tip: Use auto! to avoid dealing with generated anonymous names. auto!(Root::D::E) resolves the path automatically.

Deeply Nested Anonymous Enums

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(PartialEq)]
    enum A {
        B(enum {
            C(enum {
                D(enum {
                    E(enum {
                        F,
                        G(String),
                    }),
                }),
            }),
        }),
    }
);

// Manual construction:
let _ = A::B(_A_0_anonymous::C(_A_1_anonymous::D(_A_2_anonymous::E(_A_3_anonymous::F))));

// With auto!:
use yuuka::auto;
let _ = auto!(A::B::C::D::E::F);
let _ = auto!(A::B::C::D::E::G("hello".to_string()));
}

Default Values

Specify a default variant with = VariantName after the closing brace:

#![allow(unused)]
fn main() {
derive_enum!(
    enum Theme {
        Light,
        Dark,
        System,
    } = Dark
);

let theme = Theme::default();
// theme == Theme::Dark
}

Default for Tuple Variants

#![allow(unused)]
fn main() {
derive_enum!(
    enum Value {
        Int(i32),
        Text(String),
    } = Int(0)
);
}

Default for Nested Anonymous Enums

#![allow(unused)]
fn main() {
derive_enum!(
    enum Group {
        Millennium(enum {
            GameDevelopment(enum GameDevelopment {
                Momoi,
                Midori,
                Yuzu,
                Arisu,
            } = Yuzu),
            CAndC,
            Veritas,
        } = GameDevelopment(Default::default())),
    } = Millennium(Default::default())
);

// Group::default() == Group::Millennium(GameDevelopment(Yuzu))
}

Note: When no default value is specified, the generated impl Default uses unimplemented!(), which will panic at runtime if called. Always specify a default if you plan to use Default::default().


Extra Derive and Attribute Macros

Just like derive_struct!, you can pass #[derive(...)] and attribute macros:

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(Serialize, Deserialize)]
    #[serde(rename_all = "snake_case")]
    enum Member {
        SaibaMomoi,
        SaibaMidori,
        HanaokaYuzu,
        TendouAris,
    } = SaibaMidori
);

let json = serde_json::to_string(&Member::default()).unwrap();
assert_eq!(json, r#""saiba_midori""#);
}

See Attributes & Visibility for full details on attribute macros, recursive propagation, and variant-level attributes.

auto! Macro

The auto! macro simplifies constructing instances of types generated by derive_struct! and derive_enum!. Its primary value is resolving anonymous type names automatically — you write human-readable paths while the macro expands them to the correct generated names.

Why auto!?

When you use anonymous structs or enums, yuuka generates names like _Root_0_anonymous. Constructing these manually is verbose and fragile:

#![allow(unused)]
fn main() {
derive_struct!(Root {
    data: {
        name: String,
        score: f64,
    },
});

// Without auto! — you must know the generated name
let val = Root {
    data: _Root_0_anonymous {
        name: "test".to_string(),
        score: 99.5,
    },
};

// With auto! — just use { }
let val = auto!(Root {
    data: {
        name: "test".to_string(),
        score: 99.5,
    },
});
}

Struct Construction

Basic Struct

#![allow(unused)]
fn main() {
derive_struct!(Root {
    a: String,
    b: i32,
});

let obj = auto!(Root {
    a: "hello".to_string(),
    b: 42,
});
}

Nested Anonymous Structs

#![allow(unused)]
fn main() {
derive_struct!(Root {
    a: String,
    b: i32,
    c: f64,
    d: {
        e: String = "world".to_string(),
        f: i32,
    },
});

let obj = auto!(Root {
    a: "hello".to_string(),
    b: 42,
    c: std::f64::consts::PI,
    d: {
        f: 24,
        ..Default::default()
    },
});
assert_eq!(obj.d.e, "world"); // From default
assert_eq!(obj.d.f, 24);      // Explicitly set
}

Spread Expression

Use ..Default::default() to fill remaining fields with defaults, just like standard Rust struct update syntax:

#![allow(unused)]
fn main() {
let obj = auto!(Root {
    a: "hello".to_string(),
    b: 42,
    c: 3.14,
    d: {
        f: 24,
        ..Default::default()  // e gets its default "world"
    },
});
}

Enum Construction

Unit Variant

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(PartialEq)]
    enum Root {
        A,
        B(i32),
        C { a: String, b: i32 },
    }
);

assert_eq!(auto!(Root::A), Root::A);
}

Tuple Variant

#![allow(unused)]
fn main() {
assert_eq!(auto!(Root::B(42)), Root::B(42));
}

Struct-like Variant

#![allow(unused)]
fn main() {
assert_eq!(
    auto!(Root::C {
        a: "hello".to_string(),
        b: 42,
    }),
    Root::C {
        a: "hello".to_string(),
        b: 42,
    }
);
}

Anonymous Enum Path Resolution

This is where auto! truly shines. For anonymous enums nested inside tuple variants, auto! resolves the path through multiple levels:

Single Level

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(PartialEq)]
    enum Root {
        D(enum {
            E,
            F(i32),
            G { a: String, b: i32 },
        }),
    }
);

// Without auto! — verbose
let _ = Root::D(__Root::_Root_0_anonymous::E);

// With auto! — clean
assert_eq!(auto!(Root::D::E), Root::D(__Root::_Root_0_anonymous::E));

assert_eq!(auto!(Root::D::F(42)), Root::D(__Root::_Root_0_anonymous::F(42)));

assert_eq!(
    auto!(Root::D::G {
        a: "hello".to_string(),
        b: 42,
    }),
    Root::D(__Root::_Root_0_anonymous::G {
        a: "hello".to_string(),
        b: 42,
    })
);
}

Deeply Nested Paths

auto! can resolve paths through arbitrarily deep anonymous enum nesting:

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(PartialEq)]
    enum A {
        B(enum {
            C(enum {
                D(enum {
                    E(enum {
                        F,
                        G(String),
                    }),
                }),
            }),
        }),
    }
);

// Resolves: A::B → _A_0_anonymous::C → _A_1_anonymous::D → _A_2_anonymous::E → _A_3_anonymous::F
assert_eq!(
    auto!(A::B::C::D::E::F),
    A::B(_A_0_anonymous::C(_A_1_anonymous::D(_A_2_anonymous::E(_A_3_anonymous::F))))
);

assert_eq!(
    auto!(A::B::C::D::E::G("hello".to_string())),
    A::B(_A_0_anonymous::C(_A_1_anonymous::D(_A_2_anonymous::E(
        _A_3_anonymous::G("hello".to_string())
    ))))
);
}

Mixed Usage

You can nest auto! calls inside other auto! calls or regular struct construction:

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(PartialEq)]
    Root {
        outer: {
            a: enum B {
                C {
                    c: i32,
                    d: f64,
                },
            },
        },
    }
);

let val = auto!(Root {
    outer: {
        a: auto!(B::C { c: 42, d: std::f64::consts::PI }),
    },
});

assert_eq!(val.outer.a, B::C { c: 42, d: std::f64::consts::PI });
}

Cross-Module Usage

auto! works across module boundaries as long as the types and their helper macros are in scope:

#![allow(unused)]
fn main() {
#[macro_use]
mod definitions {
    use yuuka::derive_struct;

    derive_struct!(
        #[derive(PartialEq)]
        pub Root {
            a: String,
            b: i32,
        }
    );
}

mod usage {
    use yuuka::auto;
    use super::definitions::*;

    #[test]
    fn test() {
        assert_eq!(
            auto!(Root {
                a: "hello".to_string(),
                b: 42,
            }),
            Root {
                a: "hello".to_string(),
                b: 42,
            }
        );
    }
}
}

When using anonymous types across modules, ensure the defining module is marked with #[macro_use]:

#![allow(unused)]
fn main() {
#[macro_use]
mod definitions {
    use yuuka::derive_struct;

    derive_struct!(Root {
        data: {
            value: String,
        },
    });
}

mod usage {
    use yuuka::auto;
    use super::definitions::*;

    fn create() {
        let val = auto!(Root {
            data: {
                value: "hello".to_string(),
            },
        });
    }
}
}

For cross-crate usage, see Attributes & Visibility — Cross-Crate Usage.

Attributes & Visibility

This document covers how to control derive macros, attribute macros, visibility, and cross-crate export for types generated by derive_struct! and derive_enum!.


Extra Derive Macros

Place #[derive(...)] before the type name to add derive macros to the generated root type:

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};
use yuuka::derive_struct;

derive_struct!(
    #[derive(Serialize, Deserialize)]
    Root {
        name: String,
        value: i32,
    }
);
}

Note: Debug and Clone are always derived automatically. You don’t need to specify them.

The same works for derive_enum!:

#![allow(unused)]
fn main() {
use yuuka::derive_enum;

derive_enum!(
    #[derive(Serialize, Deserialize)]
    enum Status {
        Active,
        Inactive,
    }
);
}

Attribute Macros

Place attribute macros after #[derive(...)]:

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(Serialize, Deserialize)]
    #[serde(rename_all = "camelCase")]
    Root {
        user_name: String,
        home_dir: String,
    }
);

let json = serde_json::to_string(&Root {
    user_name: "langyo".to_string(),
    home_dir: "/home/langyo".to_string(),
}).unwrap();
assert_eq!(json, r#"{"userName":"langyo","homeDir":"/home/langyo"}"#);
}

Recursive Attribute Propagation

Use #[macros_recursive(...)] to propagate attributes to all nested inline types:

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(Serialize, Deserialize)]
    #[macros_recursive(serde(rename_all = "camelCase"))]
    Root {
        nick_name: {
            chinese: {
                simplified_chinese: {
                    first_name: {
                        origin: String = "早濑".to_string(),
                        meme: String = "旱濑".to_string(),
                    },
                    last_name: String = "优香".to_string(),
                },
                traditional_chinese: {
                    first_name: String = "早瀨".to_string(),
                    last_name: String = "優香".to_string(),
                },
            },
            japanese: {
                first_name: String = "早瀬".to_string(),
                last_name: String = "ユウカ".to_string(),
            },
        },
    }
);

let json = serde_json::to_string(&Root::default()).unwrap();
// All nested levels use camelCase: "nickName", "simplifiedChinese", "firstName", etc.
}

#[macros_recursive(...)] applies the specified attributes to every struct and enum generated in the hierarchy — not just the root type.


Field-level Attributes

Place attributes directly before a field name:

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(Serialize, Deserialize)]
    #[serde(rename_all = "camelCase")]
    Root {
        nick_name: String,
        #[serde(rename = "location")]
        live_in: String,
    }
);

// "live_in" serializes as "location" instead of "liveIn"
}

Variant-level Attributes for Enums

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(Serialize, Deserialize)]
    #[serde(rename_all = "snake_case")]
    enum Member {
        SaibaMomoi,
        SaibaMidori,
        #[serde(rename = "yuzu")]
        HanaokaYuzu,
        TendouAris,
    } = HanaokaYuzu
);

let json = serde_json::to_string(&Member::default()).unwrap();
assert_eq!(json, r#""yuzu""#);
}

Type-level Attributes on Inline Types

You can apply #[derive(...)] and attributes to inline struct/enum types defined in a field. Place them before the field name, using #[derive(...)] to separate field attributes from type attributes:

Named Inline Types

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(Serialize, Deserialize)]
    #[serde(deny_unknown_fields)]
    Root {
        nick_name: String,
        #[serde(rename = "position")]
        #[derive(PartialEq)]
        #[serde(rename_all = "UPPERCASE")]
        location: Location {
            country: String,
            address: String,
        },
    }
);

// Root gets #[serde(deny_unknown_fields)]
// Location gets #[derive(PartialEq)] and #[serde(rename_all = "UPPERCASE")]
// The field "location" is renamed to "position"
}

Anonymous Inline Types

For anonymous types, use #[derive] (empty derive) as a separator:

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(Serialize, Deserialize)]
    #[serde(deny_unknown_fields)]
    Root {
        nick_name: String,
        #[serde(rename = "position")]
        #[derive]
        #[serde(rename_all = "UPPERCASE")]
        location: {
            country: String = "kivotos".to_string(),
            address: String = "777".to_string(),
        },
    }
);

// The empty #[derive] separates field-level attributes (above) from type-level attributes (below)
}

On Enum Variants

The same pattern works for enum tuple variants:

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(Serialize, Deserialize)]
    #[serde(deny_unknown_fields)]
    enum Group {
        #[serde(rename = "777")]
        #[derive(PartialEq)]
        #[serde(rename_all = "UPPERCASE")]
        Millennium(enum Millennium {
            GameDevelopment(enum GameDevelopment {
                Momoi,
                Midori,
                Yuzu,
                Arisu,
            }),
            #[serde(rename = "C&C")]
            CAndC,
            Veritas,
        }),
    }
);
}

And for anonymous enum variants:

#![allow(unused)]
fn main() {
derive_enum!(
    #[derive(Serialize, Deserialize)]
    enum Group {
        #[serde(rename = "777")]
        #[derive]
        #[serde(rename_all = "UPPERCASE")]
        Millennium(enum {
            GameDevelopment(enum GameDevelopment {
                Momoi, Midori, Yuzu, Arisu,
            } = Yuzu),
            CAndC,
        } = GameDevelopment(Default::default())),
    } = Millennium(Default::default())
);
}

Visibility

pub Modifier

Use pub to make generated types and their module public:

#![allow(unused)]
fn main() {
derive_struct!(
    pub Root {
        name: String,
    }
);

derive_enum!(
    pub enum Status {
        Active,
        Inactive,
    }
);
}

This generates pub mod __Root and pub use __Root::*, making all types accessible from outside the current module.

Default Visibility

Without pub, types are pub(crate):

#![allow(unused)]
fn main() {
derive_struct!(
    Root {
        name: String,
    }
);
// Generates: pub(crate) mod __Root { ... }
// Generates: pub(crate) use __Root::*;
}

Note: pub declarations are typically used at the module or crate level (outside of functions). Inside test functions, visibility doesn’t matter.


Cross-Crate Usage

To export generated types and their auto! helper macros for use in other crates, use #[macro_export]:

Library Crate

#![allow(unused)]
fn main() {
use yuuka::{derive_struct, derive_enum};

derive_struct!(
    #[derive(PartialEq)]
    #[macro_export]
    pub TestStruct {
        a: i32,
        b: String,
        c: {
            d: i32,
            e: String,
        },
    }
);

derive_enum!(
    #[macro_export]
    #[derive(PartialEq)]
    pub enum TestEnum {
        A(i32),
        B(String),
        C(enum C {
            D(i32),
            E(String),
            F(enum F {
                G(i32),
                H(String),
            }),
        }),
    }
);
}

Note: #[macro_export] can be placed before or after #[derive(...)] — both positions work.

Consuming Crate

#![allow(unused)]
fn main() {
use yuuka::auto;
use my_lib::*;

let test_struct = auto!(TestStruct {
    a: 1,
    b: "Hello".to_string(),
    c: {
        d: 2,
        e: "World".to_string(),
    },
});

let test_enum = auto!(TestEnum::C::F::H("Hello".to_string()));
assert_eq!(test_enum, TestEnum::C(C::F(F::H("Hello".to_string()))));
}

How It Works

#[macro_export] makes the generated macro_rules! helper macros (like __auto_TestStruct!) available at the crate root level. Without this attribute, the helper macros are only visible within the defining crate, and auto! won’t work from external crates.

Cargo.toml Setup

For the library crate, ensure it can be linked properly:

[lib]
crate-type = ["rlib", "dylib"]

Examples

Real-world usage examples and an explanation of the code generated by yuuka macros.


Language Pack (i18n)

A typical use case: defining a nested language pack structure that maps directly to JSON files. Supports serialization/deserialization with serde.

use anyhow::Result;
use serde::{Serialize, Deserialize};
use yuuka::{derive_struct, auto};

fn main() -> Result<()> {
    derive_struct!(
        #[derive(PartialEq, Serialize, Deserialize)]
        LanguagePack {
            是: String,
            否: String,
            确认: String,
            取消: String,
            保存: String,
            主页: {
                启动: String,
                设置: String,
            },
            设置: {
                虚拟机路径: String,
                程序本体路径: String,
                网络配置: {
                    网络配置: String,
                    是否启用代理: String,
                    代理地址: String,
                    是否启用IPV6: String,
                },
            },
        }
    );

    let config = auto!(LanguagePack {
        是: "Yes".to_string(),
        否: "No".to_string(),
        确认: "Confirm".to_string(),
        取消: "Cancel".to_string(),
        保存: "Save".to_string(),
        主页: {
            启动: "Start".to_string(),
            设置: "Settings".to_string(),
        },
        设置: {
            虚拟机路径: "VM Path".to_string(),
            程序本体路径: "Program Path".to_string(),
            网络配置: {
                网络配置: "Network Config".to_string(),
                是否启用代理: "Enable Proxy".to_string(),
                代理地址: "Proxy Address".to_string(),
                是否启用IPV6: "Enable IPV6".to_string(),
            },
        },
    });

    // Deserialize from JSON
    let json_raw = r#"
    {
        "是": "Yes", "否": "No", "确认": "Confirm",
        "取消": "Cancel", "保存": "Save",
        "主页": { "启动": "Start", "设置": "Settings" },
        "设置": {
            "虚拟机路径": "VM Path",
            "程序本体路径": "Program Path",
            "网络配置": {
                "网络配置": "Network Config",
                "是否启用代理": "Enable Proxy",
                "代理地址": "Proxy Address",
                "是否启用IPV6": "Enable IPV6"
            }
        }
    }"#;

    let config_from_json = serde_json::from_str::<LanguagePack>(json_raw)?;
    assert_eq!(config, config_from_json);
    assert_eq!(config.设置.网络配置.代理地址, "Proxy Address");

    Ok(())
}

Key takeaways:

  • Field names can be non-ASCII (Chinese characters, etc.) — they work as both Rust identifiers and JSON keys.
  • The auto! macro handles anonymous sub-struct construction (主页, 设置, 网络配置) seamlessly.
  • The generated types are fully serde-compatible for round-trip JSON serialization.

Server Router Configuration

A more complex example modeling a reverse proxy / server router configuration with nested arrays and inline enums.

use anyhow::Result;
use serde::{Serialize, Deserialize};
use yuuka::{derive_struct, auto};

fn main() -> Result<()> {
    derive_struct!(
        #[derive(PartialEq, Serialize, Deserialize)]
        Config {
            port: u16,
            services: [Service {
                domain: Vec<String>,
                rules: [Rule {
                    pattern: String,
                    method: enum Method {
                        Redirect { url: String },
                        Proxy { host: String },
                        StaticFile { path: String },
                        StaticDir { path: String },
                    },
                }],
            }],
        }
    );

    let config = auto!(Config {
        port: 8080,
        services: vec![Service {
            domain: vec!["example.com".to_string()],
            rules: vec![
                Rule {
                    pattern: "^/$".to_string(),
                    method: Method::Redirect {
                        url: "https://example.com/index.html".to_string(),
                    },
                },
                Rule {
                    pattern: "^/api".to_string(),
                    method: Method::Proxy {
                        host: "http://localhost:8081".to_string(),
                    },
                },
                Rule {
                    pattern: "^/static".to_string(),
                    method: Method::StaticDir {
                        path: "/var/www/static".to_string(),
                    },
                },
            ],
        }],
    });

    // This structure maps directly to/from JSON
    let json = serde_json::to_string_pretty(&config)?;
    let config_from_json: Config = serde_json::from_str(&json)?;
    assert_eq!(config, config_from_json);

    Ok(())
}

Key takeaways:

  • [Service { ... }] generates services: Vec<Service> with Service as an independent struct.
  • Nested [Rule { ... }] inside Service generates another Vec<Rule> with inline Rule struct.
  • enum Method { ... } defines an enum inline, with struct-like variants for different routing methods.
  • The entire configuration can be loaded from / saved to JSON.

Generated Code Structure

Understanding what yuuka generates helps debug and work effectively with the library.

When you write:

#![allow(unused)]
fn main() {
derive_struct!(
    #[derive(Serialize)]
    pub Root {
        name: String,
        child: Child {
            value: i32,
        },
    }
);
}

The macro generates approximately:

#![allow(unused)]
fn main() {
#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals, dead_code)]
pub mod __Root {
    use super::*;

    #[derive(Debug, Clone, Serialize, Default)]
    pub struct Root {
        pub name: String,
        pub child: Child,
    }

    #[derive(Debug, Clone, Serialize, Default)]
    pub struct Child {
        pub value: i32,
    }

    // Helper macros for auto!
    macro_rules! __auto_Root {
        (name $($tt:tt)*) => { $($tt)* };
        (child { $($tt:tt)* }) => { ::yuuka::auto!(Child { $($tt)* }) };
        (child $($tt:tt)*) => { $($tt)* };
        // ... more rules for each field
    }

    macro_rules! __auto_Child {
        (value $($tt:tt)*) => { $($tt)* };
        // ... more rules for each field
    }
}
pub use __Root::*;
}

Key aspects

  1. Module wrapping: All types go into a module named __TypeName to avoid name collisions. Everything is re-exported with use __TypeName::*.

  2. Automatic derives: Debug and Clone are always added. Your custom #[derive(...)] macros are appended.

  3. Default implementation: If no fields have custom defaults → #[derive(Default)]. If any field has = value → manual impl Default { ... }.

  4. Helper macros: For each type, a __auto_TypeName! macro is generated. These are macro_rules! macros that the auto! proc-macro calls to resolve field types — particularly anonymous struct/enum names.

  5. Super imports: use super::* brings the outer scope into the module, which is why external types need super:: prefix when referenced.

Module naming convention

InputModule name
Root { ... }__Root
Config { ... }__Config
Anonymous field in Root_Root_0_anonymous, _Root_1_anonymous, …
Anonymous field in enum A_A_0_anonymous, _A_1_anonymous, …