aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/group_impl.rs
blob: 59da9dd9086d42d0da0b55b77f551c28e16003a7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, Result as SynResult, TypePath};

/// Input for the `group!` macro
///
/// # Syntax
///
/// ```rust,ignore
/// /// Only a type path — uses default `crate::ThisProgram` as program
/// group!(std::io::Error);
/// group!(ParseIntError);
///
/// /// With an alias — creates a `pub type Alias = Path;` and uses `Alias` as variant name
/// group!(IoError = std::io::Error);
/// ```
enum GroupInput {
    /// `group!(TypePath)` — variant name is the last path segment
    Plain(TypePath),

    /// `group!(Alias = TypePath)` — variant name is `Alias`, also generates `pub type Alias = TypePath;`
    Aliased { alias: Ident, type_path: TypePath },
}

impl Parse for GroupInput {
    fn parse(input: ParseStream) -> SynResult<Self> {
        // Peek ahead: if the second token is `=`, parse as aliased form
        let fork = input.fork();
        let _first: Ident = fork.parse()?;
        if fork.peek(syn::Token![=]) {
            // Consume the ident and `=` from the real input
            let alias: Ident = input.parse()?;
            let _eq: syn::Token![=] = input.parse()?;
            let type_path: TypePath = input.parse()?;
            Ok(GroupInput::Aliased { alias, type_path })
        } else {
            let type_path: TypePath = input.parse()?;
            Ok(GroupInput::Plain(type_path))
        }
    }
}

/// Convert a type path into a valid module name segment
///
/// e.g. `std::io::Error` -> `internal_group_std_io_error`
fn module_name_from_type(type_path: &TypePath) -> Ident {
    let segments: Vec<String> = type_path
        .path
        .segments
        .iter()
        .map(|seg| seg.ident.to_string().to_lowercase())
        .collect();
    Ident::new(
        &format!("internal_group_{}", segments.join("_")),
        proc_macro2::Span::call_site(),
    )
}

/// Get the last segment name of a type path (the simple type name)
///
/// e.g. `std::io::Error` -> `Error`
fn type_simple_name(type_path: &TypePath) -> Ident {
    type_path
        .path
        .segments
        .last()
        .expect("TypePath must have at least one segment")
        .ident
        .clone()
}

/// Generate the `use` token for the type path inside the generated module.
///
/// - Multi-segment path (e.g. `std::num::ParseIntError`): `use std::num::ParseIntError;`
/// - Single-segment path (e.g. `ParseIntError`): `use super::ParseIntError;`
fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream {
    if type_path.path.segments.len() > 1 {
        // Full path: use it directly
        quote! {
            #[allow(unused_imports)]
            use #type_path;
        }
    } else {
        // Single ident: import from parent scope
        let ident = type_simple_name(type_path);
        quote! {
            #[allow(unused_imports)]
            use super::#ident;
        }
    }
}

pub fn group_macro(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as GroupInput);

    let is_aliased = matches!(input, GroupInput::Aliased { .. });

    let (type_path, type_name, alias_stmt) = match input {
        GroupInput::Plain(type_path) => {
            let type_name = type_simple_name(&type_path);
            (type_path, type_name, quote! {})
        }
        GroupInput::Aliased { alias, type_path } => {
            let type_name = alias.clone();
            let alias_stmt = quote! {
                pub type #alias = #type_path;
            };
            (type_path, type_name, alias_stmt)
        }
    };

    let program_path = crate::default_program_path();

    // Create a unique module name from the type path (use alias name for aliased form)
    let module_name = module_name_from_type(&type_path);
    // Generate the appropriate `use` statement for the type
    let type_use = gen_type_use(&type_path);

    // For aliased form, also import the alias from parent scope
    let alias_use = if is_aliased {
        quote! { use super::#type_name; }
    } else {
        quote! {}
    };

    // Generate the module with the Groupped implementation
    let expanded = quote! {
        #alias_stmt
        #[allow(non_camel_case_types)]
        mod #module_name {
            use #program_path as __MinglingProgram;
            #type_use
            #alias_use

            impl ::mingling::Groupped<__MinglingProgram> for #type_name {
                fn member_id() -> __MinglingProgram {
                    __MinglingProgram::#type_name
                }
            }

            ::mingling::macros::register_type!(#type_name);
        }
    };

    expanded.into()
}