aboutsummaryrefslogtreecommitdiff
path: root/alacritty_config_derive/src/serde_replace.rs
blob: ddd0cf757673f76c706e6bad15a20652ef40bc54 (plain)
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
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::{
    parse_macro_input, Data, DataStruct, DeriveInput, Error, Field, Fields, Generics, Ident,
};

use crate::{Attr, GenericsStreams, MULTIPLE_FLATTEN_ERROR};

/// Error if the derive was used on an unsupported type.
const UNSUPPORTED_ERROR: &str = "SerdeReplace must be used on a tuple struct";

pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    match input.data {
        Data::Struct(DataStruct { fields: Fields::Unnamed(_), .. }) | Data::Enum(_) => {
            derive_direct(input.ident, input.generics).into()
        },
        Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => {
            derive_recursive(input.ident, input.generics, fields.named).into()
        },
        _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(),
    }
}

pub fn derive_direct(ident: Ident, generics: Generics) -> TokenStream2 {
    quote! {
        impl <#generics> alacritty_config::SerdeReplace for #ident <#generics> {
            fn replace(&mut self, value: toml::Value) -> Result<(), Box<dyn std::error::Error>> {
                *self = serde::Deserialize::deserialize(value)?;

                Ok(())
            }
        }
    }
}

pub fn derive_recursive<T>(
    ident: Ident,
    generics: Generics,
    fields: Punctuated<Field, T>,
) -> TokenStream2 {
    let GenericsStreams { unconstrained, constrained, .. } =
        crate::generics_streams(&generics.params);
    let replace_arms = match match_arms(&fields) {
        Err(e) => return e.to_compile_error(),
        Ok(replace_arms) => replace_arms,
    };

    quote! {
        #[allow(clippy::extra_unused_lifetimes)]
        impl <'de, #constrained> alacritty_config::SerdeReplace for #ident <#unconstrained> {
            fn replace(&mut self, value: toml::Value) -> Result<(), Box<dyn std::error::Error>> {
                match value.as_table() {
                    Some(table) => {
                        for (field, next_value) in table {
                            let next_value = next_value.clone();
                            let value = value.clone();

                            match field.as_str() {
                                #replace_arms
                                _ => {
                                    let error = format!("Field \"{}\" does not exist", field);
                                    return Err(error.into());
                                },
                            }
                        }
                    },
                    None => *self = serde::Deserialize::deserialize(value)?,
                }

                Ok(())
            }
        }
    }
}

/// Create SerdeReplace recursive match arms.
fn match_arms<T>(fields: &Punctuated<Field, T>) -> Result<TokenStream2, syn::Error> {
    let mut stream = TokenStream2::default();
    let mut flattened_arm = None;

    // Create arm for each field.
    for field in fields {
        let ident = field.ident.as_ref().expect("unreachable tuple struct");
        let literal = ident.to_string();

        // Check if #[config(flattened)] attribute is present.
        let flatten = field
            .attrs
            .iter()
            .filter(|attr| (*attr).path().is_ident("config"))
            .filter_map(|attr| attr.parse_args::<Attr>().ok())
            .any(|parsed| parsed.ident.as_str() == "flatten");

        if flatten && flattened_arm.is_some() {
            return Err(Error::new(ident.span(), MULTIPLE_FLATTEN_ERROR));
        } else if flatten {
            flattened_arm = Some(quote! {
                _ => alacritty_config::SerdeReplace::replace(&mut self.#ident, value)?,
            });
        } else {
            // Extract all `#[config(alias = "...")]` attribute values.
            let aliases = field
                .attrs
                .iter()
                .filter(|attr| (*attr).path().is_ident("config"))
                .filter_map(|attr| attr.parse_args::<Attr>().ok())
                .filter(|parsed| parsed.ident.as_str() == "alias")
                .map(|parsed| {
                    let value = parsed
                        .param
                        .ok_or_else(|| format!("Field \"{}\" has no alias value", ident))?
                        .value();

                    if value.trim().is_empty() {
                        return Err(format!("Field \"{}\" has an empty alias value", ident));
                    }

                    Ok(value)
                })
                .collect::<Result<Vec<String>, String>>()
                .map_err(|msg| Error::new(ident.span(), msg))?;

            stream.extend(quote! {
                    #(#aliases)|* | #literal => alacritty_config::SerdeReplace::replace(&mut
            self.#ident, next_value)?,         });
        }
    }

    // Add the flattened catch-all as last match arm.
    if let Some(flattened_arm) = flattened_arm.take() {
        stream.extend(flattened_arm);
    }

    Ok(stream)
}