From 4ddb608563d985060d69594d1004550a680ae3bd Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 31 Aug 2022 22:48:38 +0000 Subject: Add IPC config subcommand This patch adds a new mechanism for changing configuration options without editing the configuration file, by sending options to running instances through `alacritty msg`. Each window will load Alacritty's configuration file by default and then accept IPC messages for config updates using the `alacritty msg config` subcommand. By default all windows will be updated, individual windows can be addressed using `alacritty msg config --window-id "$ALACRITTY_WINDOW_ID"`. Each option will replace the config's current value and cannot be reset until Alacritty is restarted or the option is overwritten with a new value. Configuration options are passed in the format `field.subfield=value`, where `value` is interpreted as yaml. Closes #472. --- alacritty_config_derive/Cargo.toml | 6 +- .../src/config_deserialize/de_enum.rs | 71 +++++++ .../src/config_deserialize/de_struct.rs | 181 +++++++++++++++++ .../src/config_deserialize/mod.rs | 22 ++ alacritty_config_derive/src/de_enum.rs | 66 ------ alacritty_config_derive/src/de_struct.rs | 226 --------------------- alacritty_config_derive/src/lib.rs | 77 +++++-- alacritty_config_derive/src/serde_replace.rs | 113 +++++++++++ alacritty_config_derive/tests/config.rs | 38 +++- 9 files changed, 492 insertions(+), 308 deletions(-) create mode 100644 alacritty_config_derive/src/config_deserialize/de_enum.rs create mode 100644 alacritty_config_derive/src/config_deserialize/de_struct.rs create mode 100644 alacritty_config_derive/src/config_deserialize/mod.rs delete mode 100644 alacritty_config_derive/src/de_enum.rs delete mode 100644 alacritty_config_derive/src/de_struct.rs create mode 100644 alacritty_config_derive/src/serde_replace.rs (limited to 'alacritty_config_derive') diff --git a/alacritty_config_derive/Cargo.toml b/alacritty_config_derive/Cargo.toml index 8584d0d2..c901815e 100644 --- a/alacritty_config_derive/Cargo.toml +++ b/alacritty_config_derive/Cargo.toml @@ -16,7 +16,11 @@ syn = { version = "1.0.53", features = ["derive", "parsing", "proc-macro", "prin proc-macro2 = "1.0.24" quote = "1.0.7" +[dev-dependencies.alacritty_config] +path = "../alacritty_config" +version = "0.1.0" + [dev-dependencies] +serde = { version = "1.0.117", features = ["derive"] } serde_yaml = "0.8.14" -serde = "1.0.117" log = "0.4.11" diff --git a/alacritty_config_derive/src/config_deserialize/de_enum.rs b/alacritty_config_derive/src/config_deserialize/de_enum.rs new file mode 100644 index 00000000..73634e73 --- /dev/null +++ b/alacritty_config_derive/src/config_deserialize/de_enum.rs @@ -0,0 +1,71 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{DataEnum, Generics, Ident}; + +use crate::serde_replace; + +pub fn derive_deserialize(ident: Ident, generics: Generics, data_enum: DataEnum) -> TokenStream { + let visitor = format_ident!("{}Visitor", ident); + + // Create match arm streams and get a list with all available values. + let mut match_arms_stream = TokenStream2::new(); + let mut available_values = String::from("one of "); + for variant in data_enum.variants.iter().filter(|variant| { + // Skip deserialization for `#[config(skip)]` fields. + variant.attrs.iter().all(|attr| { + !crate::path_ends_with(&attr.path, "config") || attr.tokens.to_string() != "(skip)" + }) + }) { + let variant_ident = &variant.ident; + let variant_str = variant_ident.to_string(); + available_values = format!("{}`{}`, ", available_values, variant_str); + + let literal = variant_str.to_lowercase(); + + match_arms_stream.extend(quote! { + #literal => Ok(#ident :: #variant_ident), + }); + } + + // Remove trailing `, ` from the last enum variant. + available_values.truncate(available_values.len().saturating_sub(2)); + + // Generate deserialization impl. + let mut tokens = quote! { + struct #visitor; + impl<'de> serde::de::Visitor<'de> for #visitor { + type Value = #ident; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str(#available_values) + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + match s.to_lowercase().as_str() { + #match_arms_stream + _ => Err(E::custom( + &format!("unknown variant `{}`, expected {}", s, #available_values) + )), + } + } + } + + impl<'de> serde::Deserialize<'de> for #ident { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(#visitor) + } + } + }; + + // Automatically implement [`alacritty_config::SerdeReplace`]. + tokens.extend(serde_replace::derive_direct(ident, generics)); + + tokens.into() +} diff --git a/alacritty_config_derive/src/config_deserialize/de_struct.rs b/alacritty_config_derive/src/config_deserialize/de_struct.rs new file mode 100644 index 00000000..4245764f --- /dev/null +++ b/alacritty_config_derive/src/config_deserialize/de_struct.rs @@ -0,0 +1,181 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::{Error, Field, Generics, Ident, Type}; + +use crate::{serde_replace, Attr, GenericsStreams, MULTIPLE_FLATTEN_ERROR}; + +/// Use this crate's name as log target. +const LOG_TARGET: &str = env!("CARGO_PKG_NAME"); + +pub fn derive_deserialize( + ident: Ident, + generics: Generics, + fields: Punctuated, +) -> TokenStream { + // Create all necessary tokens for the implementation. + let GenericsStreams { unconstrained, constrained, phantoms } = + crate::generics_streams(&generics.params); + let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields); + let visitor = format_ident!("{}Visitor", ident); + + // Generate deserialization impl. + let mut tokens = quote! { + #[derive(Default)] + #[allow(non_snake_case)] + struct #visitor <#unconstrained> { + #phantoms + } + + impl <'de, #constrained> serde::de::Visitor<'de> for #visitor <#unconstrained> { + type Value = #ident <#unconstrained>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut config = Self::Value::default(); + + // NOTE: This could be used to print unused keys. + let mut unused = serde_yaml::Mapping::new(); + + while let Some((key, value)) = map.next_entry::()? { + match key.as_str() { + #match_assignments + _ => { + unused.insert(serde_yaml::Value::String(key), value); + }, + } + } + + #flatten + + Ok(config) + } + } + + impl <'de, #constrained> serde::Deserialize<'de> for #ident <#unconstrained> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(#visitor :: default()) + } + } + }; + + // Automatically implement [`alacritty_config::SerdeReplace`]. + tokens.extend(serde_replace::derive_recursive(ident, generics, fields)); + + tokens.into() +} + +// Token streams created from the fields in the struct. +#[derive(Default)] +struct FieldStreams { + match_assignments: TokenStream2, + flatten: TokenStream2, +} + +/// Create the deserializers for match arms and flattened fields. +fn fields_deserializer(fields: &Punctuated) -> FieldStreams { + let mut field_streams = FieldStreams::default(); + + // Create the deserialization stream for each field. + for field in fields.iter() { + if let Err(err) = field_deserializer(&mut field_streams, field) { + field_streams.flatten = err.to_compile_error(); + return field_streams; + } + } + + field_streams +} + +/// Append a single field deserializer to the stream. +fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result<(), Error> { + let ident = field.ident.as_ref().expect("unreachable tuple struct"); + let literal = ident.to_string(); + let mut literals = vec![literal.clone()]; + + // Create default stream for deserializing fields. + let mut match_assignment_stream = quote! { + match serde::Deserialize::deserialize(value) { + Ok(value) => config.#ident = value, + Err(err) => { + log::error!(target: #LOG_TARGET, "Config error: {}: {}", #literal, err); + }, + } + }; + + // Iterate over all #[config(...)] attributes. + for attr in field.attrs.iter().filter(|attr| crate::path_ends_with(&attr.path, "config")) { + let parsed = match attr.parse_args::() { + Ok(parsed) => parsed, + Err(_) => continue, + }; + + match parsed.ident.as_str() { + // Skip deserialization for `#[config(skip)]` fields. + "skip" => return Ok(()), + "flatten" => { + // NOTE: Currently only a single instance of flatten is supported per struct + // for complexity reasons. + if !field_streams.flatten.is_empty() { + return Err(Error::new(attr.span(), MULTIPLE_FLATTEN_ERROR)); + } + + // Create the tokens to deserialize the flattened struct from the unused fields. + field_streams.flatten.extend(quote! { + let unused = serde_yaml::Value::Mapping(unused); + config.#ident = serde::Deserialize::deserialize(unused).unwrap_or_default(); + }); + }, + "deprecated" | "removed" => { + // Construct deprecation/removal message with optional attribute override. + let mut message = format!("Config warning: {} has been {}", literal, parsed.ident); + if let Some(warning) = parsed.param { + message = format!("{}; {}", message, warning.value()); + } + + // Append stream to log deprecation/removal warning. + match_assignment_stream.extend(quote! { + log::warn!(target: #LOG_TARGET, #message); + }); + }, + // Add aliases to match pattern. + "alias" => { + if let Some(alias) = parsed.param { + literals.push(alias.value()); + } + }, + _ => (), + } + } + + // Create token stream for deserializing "none" string into `Option`. + if let Type::Path(type_path) = &field.ty { + if crate::path_ends_with(&type_path.path, "Option") { + match_assignment_stream = quote! { + if value.as_str().map_or(false, |s| s.eq_ignore_ascii_case("none")) { + config.#ident = None; + continue; + } + #match_assignment_stream + }; + } + } + + // Create the token stream for deserialization and error handling. + field_streams.match_assignments.extend(quote! { + #(#literals)|* => { #match_assignment_stream }, + }); + + Ok(()) +} diff --git a/alacritty_config_derive/src/config_deserialize/mod.rs b/alacritty_config_derive/src/config_deserialize/mod.rs new file mode 100644 index 00000000..b1923377 --- /dev/null +++ b/alacritty_config_derive/src/config_deserialize/mod.rs @@ -0,0 +1,22 @@ +use proc_macro::TokenStream; +use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields}; + +/// Error if the derive was used on an unsupported type. +const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on an enum or struct with fields"; + +mod de_enum; +mod de_struct; + +pub fn derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + match input.data { + Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => { + de_struct::derive_deserialize(input.ident, input.generics, fields.named) + }, + Data::Enum(data_enum) => { + de_enum::derive_deserialize(input.ident, input.generics, data_enum) + }, + _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(), + } +} diff --git a/alacritty_config_derive/src/de_enum.rs b/alacritty_config_derive/src/de_enum.rs deleted file mode 100644 index 98247c0c..00000000 --- a/alacritty_config_derive/src/de_enum.rs +++ /dev/null @@ -1,66 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::{DataEnum, Ident}; - -pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream { - let visitor = format_ident!("{}Visitor", ident); - - // Create match arm streams and get a list with all available values. - let mut match_arms_stream = TokenStream2::new(); - let mut available_values = String::from("one of "); - for variant in data_enum.variants.iter().filter(|variant| { - // Skip deserialization for `#[config(skip)]` fields. - variant.attrs.iter().all(|attr| { - !crate::path_ends_with(&attr.path, "config") || attr.tokens.to_string() != "(skip)" - }) - }) { - let variant_ident = &variant.ident; - let variant_str = variant_ident.to_string(); - available_values = format!("{}`{}`, ", available_values, variant_str); - - let literal = variant_str.to_lowercase(); - - match_arms_stream.extend(quote! { - #literal => Ok(#ident :: #variant_ident), - }); - } - - // Remove trailing `, ` from the last enum variant. - available_values.truncate(available_values.len().saturating_sub(2)); - - // Generate deserialization impl. - let tokens = quote! { - struct #visitor; - impl<'de> serde::de::Visitor<'de> for #visitor { - type Value = #ident; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str(#available_values) - } - - fn visit_str(self, s: &str) -> Result - where - E: serde::de::Error, - { - match s.to_lowercase().as_str() { - #match_arms_stream - _ => Err(E::custom( - &format!("unknown variant `{}`, expected {}", s, #available_values) - )), - } - } - } - - impl<'de> serde::Deserialize<'de> for #ident { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_str(#visitor) - } - } - }; - - tokens.into() -} diff --git a/alacritty_config_derive/src/de_struct.rs b/alacritty_config_derive/src/de_struct.rs deleted file mode 100644 index cf7ea141..00000000 --- a/alacritty_config_derive/src/de_struct.rs +++ /dev/null @@ -1,226 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::parse::{self, Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{Error, Field, GenericParam, Generics, Ident, LitStr, Token, Type, TypeParam}; - -/// Error message when attempting to flatten multiple fields. -const MULTIPLE_FLATTEN_ERROR: &str = "At most one instance of #[config(flatten)] is supported"; -/// Use this crate's name as log target. -const LOG_TARGET: &str = env!("CARGO_PKG_NAME"); - -pub fn derive_deserialize( - ident: Ident, - generics: Generics, - fields: Punctuated, -) -> TokenStream { - // Create all necessary tokens for the implementation. - let GenericsStreams { unconstrained, constrained, phantoms } = - generics_streams(generics.params); - let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields); - let visitor = format_ident!("{}Visitor", ident); - - // Generate deserialization impl. - let tokens = quote! { - #[derive(Default)] - #[allow(non_snake_case)] - struct #visitor < #unconstrained > { - #phantoms - } - - impl<'de, #constrained> serde::de::Visitor<'de> for #visitor < #unconstrained > { - type Value = #ident < #unconstrained >; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a mapping") - } - - fn visit_map(self, mut map: M) -> Result - where - M: serde::de::MapAccess<'de>, - { - let mut config = Self::Value::default(); - - // NOTE: This could be used to print unused keys. - let mut unused = serde_yaml::Mapping::new(); - - while let Some((key, value)) = map.next_entry::()? { - match key.as_str() { - #match_assignments - _ => { - unused.insert(serde_yaml::Value::String(key), value); - }, - } - } - - #flatten - - Ok(config) - } - } - - impl<'de, #constrained> serde::Deserialize<'de> for #ident < #unconstrained > { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_map(#visitor :: default()) - } - } - }; - - tokens.into() -} - -// Token streams created from the fields in the struct. -#[derive(Default)] -struct FieldStreams { - match_assignments: TokenStream2, - flatten: TokenStream2, -} - -/// Create the deserializers for match arms and flattened fields. -fn fields_deserializer(fields: &Punctuated) -> FieldStreams { - let mut field_streams = FieldStreams::default(); - - // Create the deserialization stream for each field. - for field in fields.iter() { - if let Err(err) = field_deserializer(&mut field_streams, field) { - field_streams.flatten = err.to_compile_error(); - return field_streams; - } - } - - field_streams -} - -/// Append a single field deserializer to the stream. -fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result<(), Error> { - let ident = field.ident.as_ref().expect("unreachable tuple struct"); - let literal = ident.to_string(); - let mut literals = vec![literal.clone()]; - - // Create default stream for deserializing fields. - let mut match_assignment_stream = quote! { - match serde::Deserialize::deserialize(value) { - Ok(value) => config.#ident = value, - Err(err) => { - log::error!(target: #LOG_TARGET, "Config error: {}: {}", #literal, err); - }, - } - }; - - // Iterate over all #[config(...)] attributes. - for attr in field.attrs.iter().filter(|attr| crate::path_ends_with(&attr.path, "config")) { - let parsed = match attr.parse_args::() { - Ok(parsed) => parsed, - Err(_) => continue, - }; - - match parsed.ident.as_str() { - // Skip deserialization for `#[config(skip)]` fields. - "skip" => return Ok(()), - "flatten" => { - // NOTE: Currently only a single instance of flatten is supported per struct - // for complexity reasons. - if !field_streams.flatten.is_empty() { - return Err(Error::new(attr.span(), MULTIPLE_FLATTEN_ERROR)); - } - - // Create the tokens to deserialize the flattened struct from the unused fields. - field_streams.flatten.extend(quote! { - let unused = serde_yaml::Value::Mapping(unused); - config.#ident = serde::Deserialize::deserialize(unused).unwrap_or_default(); - }); - }, - "deprecated" | "removed" => { - // Construct deprecation/removal message with optional attribute override. - let mut message = format!("Config warning: {} has been {}", literal, parsed.ident); - if let Some(warning) = parsed.param { - message = format!("{}; {}", message, warning.value()); - } - - // Append stream to log deprecation/removal warning. - match_assignment_stream.extend(quote! { - log::warn!(target: #LOG_TARGET, #message); - }); - }, - // Add aliases to match pattern. - "alias" => { - if let Some(alias) = parsed.param { - literals.push(alias.value()); - } - }, - _ => (), - } - } - - // Create token stream for deserializing "none" string into `Option`. - if let Type::Path(type_path) = &field.ty { - if crate::path_ends_with(&type_path.path, "Option") { - match_assignment_stream = quote! { - if value.as_str().map_or(false, |s| s.eq_ignore_ascii_case("none")) { - config.#ident = None; - continue; - } - #match_assignment_stream - }; - } - } - - // Create the token stream for deserialization and error handling. - field_streams.match_assignments.extend(quote! { - #(#literals)|* => { #match_assignment_stream }, - }); - - Ok(()) -} - -/// Field attribute. -struct Attr { - ident: String, - param: Option, -} - -impl Parse for Attr { - fn parse(input: ParseStream<'_>) -> parse::Result { - let ident = input.parse::()?.to_string(); - let param = input.parse::().and_then(|_| input.parse()).ok(); - Ok(Self { ident, param }) - } -} - -/// Storage for all necessary generics information. -#[derive(Default)] -struct GenericsStreams { - unconstrained: TokenStream2, - constrained: TokenStream2, - phantoms: TokenStream2, -} - -/// Create the necessary generics annotations. -/// -/// This will create three different token streams, which might look like this: -/// - unconstrained: `T` -/// - constrained: `T: Default + Deserialize<'de>` -/// - phantoms: `T: PhantomData,` -fn generics_streams(params: Punctuated) -> GenericsStreams { - let mut generics = GenericsStreams::default(); - - for generic in params { - // NOTE: Lifetimes and const params are not supported. - if let GenericParam::Type(TypeParam { ident, .. }) = generic { - generics.unconstrained.extend(quote!( #ident , )); - generics.constrained.extend(quote! { - #ident : Default + serde::Deserialize<'de> , - }); - generics.phantoms.extend(quote! { - #ident : std::marker::PhantomData < #ident >, - }); - } - } - - generics -} diff --git a/alacritty_config_derive/src/lib.rs b/alacritty_config_derive/src/lib.rs index af8f2e7f..116d4828 100644 --- a/alacritty_config_derive/src/lib.rs +++ b/alacritty_config_derive/src/lib.rs @@ -2,25 +2,27 @@ #![cfg_attr(feature = "cargo-clippy", deny(warnings))] use proc_macro::TokenStream; -use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields, Path}; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{self, Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{GenericParam, Ident, LitStr, Path, Token, TypeParam}; -mod de_enum; -mod de_struct; +mod config_deserialize; +mod serde_replace; -/// Error if the derive was used on an unsupported type. -const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on a struct with fields"; +/// Error message when attempting to flatten multiple fields. +pub(crate) const MULTIPLE_FLATTEN_ERROR: &str = + "At most one instance of #[config(flatten)] is supported"; #[proc_macro_derive(ConfigDeserialize, attributes(config))] pub fn derive_config_deserialize(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - match input.data { - Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => { - de_struct::derive_deserialize(input.ident, input.generics, fields.named) - }, - Data::Enum(data_enum) => de_enum::derive_deserialize(input.ident, data_enum), - _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(), - } + config_deserialize::derive(input) +} + +#[proc_macro_derive(SerdeReplace)] +pub fn derive_serde_replace(input: TokenStream) -> TokenStream { + serde_replace::derive(input) } /// Verify that a token path ends with a specific segment. @@ -28,3 +30,50 @@ pub(crate) fn path_ends_with(path: &Path, segment: &str) -> bool { let segments = path.segments.iter(); segments.last().map_or(false, |s| s.ident == segment) } + +/// Storage for all necessary generics information. +#[derive(Default)] +struct GenericsStreams { + unconstrained: TokenStream2, + constrained: TokenStream2, + phantoms: TokenStream2, +} + +/// Create the necessary generics annotations. +/// +/// This will create three different token streams, which might look like this: +/// - unconstrained: `T` +/// - constrained: `T: Default + Deserialize<'de>` +/// - phantoms: `T: PhantomData,` +pub(crate) fn generics_streams(params: &Punctuated) -> GenericsStreams { + let mut generics = GenericsStreams::default(); + + for generic in params { + // NOTE: Lifetimes and const params are not supported. + if let GenericParam::Type(TypeParam { ident, .. }) = generic { + generics.unconstrained.extend(quote!( #ident , )); + generics.constrained.extend(quote! { + #ident : Default + serde::Deserialize<'de> + alacritty_config::SerdeReplace, + }); + generics.phantoms.extend(quote! { + #ident : std::marker::PhantomData < #ident >, + }); + } + } + + generics +} + +/// Field attribute. +pub(crate) struct Attr { + ident: String, + param: Option, +} + +impl Parse for Attr { + fn parse(input: ParseStream<'_>) -> parse::Result { + let ident = input.parse::()?.to_string(); + let param = input.parse::().and_then(|_| input.parse()).ok(); + Ok(Self { ident, param }) + } +} diff --git a/alacritty_config_derive/src/serde_replace.rs b/alacritty_config_derive/src/serde_replace.rs new file mode 100644 index 00000000..4a0a6a99 --- /dev/null +++ b/alacritty_config_derive/src/serde_replace.rs @@ -0,0 +1,113 @@ +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, key: &str, value: serde_yaml::Value) -> Result<(), Box> { + if !key.is_empty() { + let error = format!("Fields \"{}\" do not exist", key); + return Err(error.into()); + } + *self = serde::Deserialize::deserialize(value)?; + + Ok(()) + } + } + } +} + +pub fn derive_recursive( + ident: Ident, + generics: Generics, + fields: Punctuated, +) -> TokenStream2 { + let GenericsStreams { unconstrained, constrained, .. } = + crate::generics_streams(&generics.params); + let replace_arms = match_arms(&fields); + + quote! { + #[allow(clippy::extra_unused_lifetimes)] + impl <'de, #constrained> alacritty_config::SerdeReplace for #ident <#unconstrained> { + fn replace(&mut self, key: &str, value: serde_yaml::Value) -> Result<(), Box> { + if key.is_empty() { + *self = serde::Deserialize::deserialize(value)?; + return Ok(()); + } + + let (field, next_key) = key.split_once('.').unwrap_or((key, "")); + match field { + #replace_arms + _ => { + let error = format!("Field \"{}\" does not exist", field); + return Err(error.into()); + }, + } + + Ok(()) + } + } + } +} + +/// Create SerdeReplace recursive match arms. +fn match_arms(fields: &Punctuated) -> TokenStream2 { + 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_map(|attr| attr.parse_args::().ok()) + .any(|parsed| parsed.ident.as_str() == "flatten"); + + if flatten && flattened_arm.is_some() { + return Error::new(ident.span(), MULTIPLE_FLATTEN_ERROR).to_compile_error(); + } else if flatten { + flattened_arm = Some(quote! { + _ => alacritty_config::SerdeReplace::replace(&mut self.#ident, key, value)?, + }); + } else { + stream.extend(quote! { + #literal => alacritty_config::SerdeReplace::replace(&mut self.#ident, next_key, value)?, + }); + } + } + + // Add the flattened catch-all as last match arm. + if let Some(flattened_arm) = flattened_arm.take() { + stream.extend(flattened_arm); + } + + stream +} diff --git a/alacritty_config_derive/tests/config.rs b/alacritty_config_derive/tests/config.rs index 4828b822..bd449ff8 100644 --- a/alacritty_config_derive/tests/config.rs +++ b/alacritty_config_derive/tests/config.rs @@ -1,8 +1,10 @@ use std::sync::{Arc, Mutex}; use log::{Level, Log, Metadata, Record}; +use serde::Deserialize; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config::SerdeReplace as _; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; #[derive(ConfigDeserialize, Debug, PartialEq, Eq)] enum TestEnum { @@ -63,6 +65,7 @@ struct Test2 { field3: usize, #[config(alias = "aliased")] field4: u8, + newtype: NewType, } #[derive(ConfigDeserialize, Default)] @@ -70,6 +73,9 @@ struct Test3 { flatty: usize, } +#[derive(SerdeReplace, Deserialize, Default, PartialEq, Eq, Debug)] +struct NewType(usize); + #[test] fn config_deserialize() { let logger = unsafe { @@ -159,3 +165,33 @@ impl Log for Logger { fn flush(&self) {} } + +#[test] +fn field_replacement() { + let mut test = Test::default(); + + let value = serde_yaml::to_value(13).unwrap(); + test.replace("nesting.field2", value).unwrap(); + + assert_eq!(test.nesting.field2, Some(13)); +} + +#[test] +fn replace_derive() { + let mut test = Test::default(); + + let value = serde_yaml::to_value(9).unwrap(); + test.replace("nesting.newtype", value).unwrap(); + + assert_eq!(test.nesting.newtype, NewType(9)); +} + +#[test] +fn replace_flatten() { + let mut test = Test::default(); + + let value = serde_yaml::to_value(7).unwrap(); + test.replace("flatty", value).unwrap(); + + assert_eq!(test.flatten.flatty, 7); +} -- cgit v1.2.3-54-g00ecf