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
148
149
150
151
152
153
154
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{
    Attribute, Data, DataEnum, DataStruct, Fields, Lit, Meta, MetaNameValue, NestedMeta, Path,
};

#[derive(Debug, PartialEq)]
enum OsoAttribute {
    ClassName { name: String },
    Attribute,
}

fn get_single_segment(path: &Path) -> Option<String> {
    if path.segments.len() == 1 {
        Some(path.segments[0].ident.to_string())
    } else {
        None
    }
}

fn get_nested_attr(nested: NestedMeta, oso_attrs: &mut Vec<OsoAttribute>) {
    match nested {
        NestedMeta::Lit(_) => {
            // @TODO: Probably error.

            unimplemented!("Hit a literal instead of a name.");
        }
        NestedMeta::Meta(meta) => {
            match meta {
                Meta::Path(path) => {
                    match get_single_segment(&path) {
                        Some(ref seg) if seg == "attribute" => {
                            oso_attrs.push(OsoAttribute::Attribute);
                        }
                        _ => (),
                    };
                }
                Meta::List(_) => {
                    // I don't know why this case would happen, seems like
                    // polar(foo, bar) would each be two different attributes
                    // nested under polar, not a single nested list.
                    // leaving here till we add all the attributes we need in case it pops up.
                    unimplemented!("Hit the list case");
                }
                Meta::NameValue(MetaNameValue { path, lit, .. }) => {
                    if let Some(ref seg) = get_single_segment(&path) {
                        if seg == "class_name" {
                            // @TODO: Type error if it's not a string.
                            if let Lit::Str(class_name) = lit {
                                oso_attrs.push(OsoAttribute::ClassName {
                                    name: class_name.value(),
                                });
                            }
                        }
                    }
                }
            }
        }
    }
}

fn get_oso_attrs(attr: Attribute, oso_attrs: &mut Vec<OsoAttribute>) {
    let meta = attr.parse_meta().unwrap();
    if let Meta::List(list) = meta {
        match get_single_segment(&list.path) {
            Some(ref seg) if seg == "polar" => {
                for nested in list.nested {
                    get_nested_attr(nested, oso_attrs);
                }
            }
            _ => (),
        }
    }
}

#[proc_macro_derive(PolarClass, attributes(polar))]
pub fn derive_polar_class_impl(ts: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(ts as syn::DeriveInput);

    let type_name = input.ident;
    let mut class_name = type_name.to_string();

    let attrs = input.attrs;
    let mut oso_attrs = vec![];
    for attr in attrs {
        get_oso_attrs(attr, &mut oso_attrs);
    }
    for oso_attr in oso_attrs {
        if let OsoAttribute::ClassName { name } = oso_attr {
            class_name = name;
        }
    }

    let mut getters = vec![];
    let mut constants = vec![];

    match input.data {
        Data::Struct(DataStruct { fields, .. }) => match fields {
            Fields::Named(nf) => {
                for field in nf.named {
                    let mut oso_attrs = vec![];
                    for attr in field.attrs {
                        get_oso_attrs(attr, &mut oso_attrs);
                    }
                    if oso_attrs.contains(&OsoAttribute::Attribute) {
                        let attr = field.ident.unwrap();
                        let name = attr.to_string();
                        getters.push(quote! {
                            .add_attribute_getter(#name, |recv: &#type_name| recv.#attr.clone())
                        });
                    }
                }
            }
            Fields::Unnamed(_uf) => {}
            Fields::Unit => {}
        },
        Data::Enum(DataEnum { variants, .. }) => {
            for variant in variants {
                match variant.fields {
                    Fields::Unit => {
                        let vident = variant.ident;
                        let vname = format!("{}::{}", class_name, vident);
                        constants.push(quote! {
                            .add_constant(#type_name::#vident, #vname)
                        });
                    }
                    _ => {
                        return quote_spanned! { variant.ident.span() => compile_error!("#[derive(PolarClass)] is currently only supported on enums with unit variants."); }.into();
                    }
                }
            }
        }
        _ => {
            return quote_spanned! { type_name.span() => compile_error!("#[derive(PolarClass)] is only supported on structs and enums."); }.into();
        }
    }

    let result = quote! {
        impl oso::PolarClass for #type_name {
            fn get_polar_class_builder() -> oso::ClassBuilder<#type_name> {
                oso::Class::builder()
                    .name(#class_name)
                    #(#getters)*
                    #(#constants)*
            }

            fn get_polar_class() -> oso::Class {
                let builder = #type_name::get_polar_class_builder();
                builder.build()
            }
        }
    };
    result.into()
}