From 85088601364cd64d643504b4856a946c37a1615e Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Wed, 21 Sep 2022 17:42:43 -0700 Subject: [PATCH] config: derive some metadata about the config This isn't used by anything yet, but will enable some runtime config editing functions that I have planeed for the future. --- Cargo.lock | 10 ++ config/Cargo.toml | 1 + config/derive/Cargo.toml | 15 ++ config/derive/LICENSE.md | 21 +++ config/derive/src/attr.rs | 279 ++++++++++++++++++++++++++++++++ config/derive/src/bound.rs | 16 ++ config/derive/src/configmeta.rs | 64 ++++++++ config/derive/src/lib.rs | 13 ++ config/src/config.rs | 3 +- config/src/lib.rs | 1 + config/src/meta.rs | 33 ++++ 11 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 config/derive/Cargo.toml create mode 100644 config/derive/LICENSE.md create mode 100644 config/derive/src/attr.rs create mode 100644 config/derive/src/bound.rs create mode 100644 config/derive/src/configmeta.rs create mode 100644 config/derive/src/lib.rs create mode 100644 config/src/meta.rs diff --git a/Cargo.lock b/Cargo.lock index a3b1eebe1..8801e7c02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -723,6 +723,7 @@ dependencies = [ "toml", "umask", "wezterm-bidi", + "wezterm-config-derive", "wezterm-dynamic", "wezterm-input-types", "wezterm-term", @@ -5444,6 +5445,15 @@ dependencies = [ "wezterm-dynamic", ] +[[package]] +name = "wezterm-config-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "wezterm-dynamic" version = "0.1.0" diff --git a/config/Cargo.toml b/config/Cargo.toml index a72255724..1712ebf56 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -39,6 +39,7 @@ smol = "1.2" termwiz = { path = "../termwiz", features=["use_serde"] } toml = "0.5" umask = { path = "../umask" } +wezterm-config-derive = { version="0.1", path="derive" } wezterm-dynamic = { path = "../wezterm-dynamic" } wezterm-bidi = { path = "../bidi" } wezterm-input-types = { path = "../wezterm-input-types" } diff --git a/config/derive/Cargo.toml b/config/derive/Cargo.toml new file mode 100644 index 000000000..b1ae66e78 --- /dev/null +++ b/config/derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "wezterm-config-derive" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/wez/wezterm" +description = "derive config metadata" +license = "MIT" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0.2" +syn = "1.0" diff --git a/config/derive/LICENSE.md b/config/derive/LICENSE.md new file mode 100644 index 000000000..7a93d12fa --- /dev/null +++ b/config/derive/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Wez Furlong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/derive/src/attr.rs b/config/derive/src/attr.rs new file mode 100644 index 000000000..cc08abc78 --- /dev/null +++ b/config/derive/src/attr.rs @@ -0,0 +1,279 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{ + Attribute, Error, Field, GenericArgument, Ident, Lit, Meta, NestedMeta, Path, PathArguments, + Result, Type, +}; + +pub struct ContainerInfo { + pub into: Option, + pub try_from: Option, + pub debug: bool, +} + +pub fn container_info(attrs: &[Attribute]) -> Result { + let mut into = None; + let mut try_from = None; + let mut debug = false; + + for attr in attrs { + if !attr.path.is_ident("dynamic") { + continue; + } + + let list = match attr.parse_meta()? { + Meta::List(list) => list, + other => return Err(Error::new_spanned(other, "unsupported attribute")), + }; + + for meta in &list.nested { + match meta { + NestedMeta::Meta(Meta::Path(path)) => { + if path.is_ident("debug") { + debug = true; + continue; + } + } + NestedMeta::Meta(Meta::NameValue(value)) => { + if value.path.is_ident("into") { + if let Lit::Str(s) = &value.lit { + into = Some(s.parse()?); + continue; + } + } + if value.path.is_ident("try_from") { + if let Lit::Str(s) = &value.lit { + try_from = Some(s.parse()?); + continue; + } + } + } + _ => {} + } + return Err(Error::new_spanned(meta, "unsupported attribute")); + } + } + + Ok(ContainerInfo { + into, + try_from, + debug, + }) +} + +pub enum DefValue { + None, + Default, + Path(Path), +} + +pub struct FieldInfo<'a> { + pub field: &'a Field, + pub type_name: String, + pub name: String, + pub skip: bool, + pub flatten: bool, + pub allow_default: DefValue, + pub into: Option, + pub try_from: Option, + pub deprecated: Option, + pub validate: Option, + pub doc: String, + pub container_type: ContainerType, +} + +#[derive(Debug)] +pub enum ContainerType { + None, + Option, + Vec, + Map, +} + +impl<'a> FieldInfo<'a> { + pub fn to_option(&self) -> TokenStream { + let name = &self.name; + let doc = &self.doc; + let type_name = &self.type_name; + let container_type = Ident::new(&format!("{:?}", self.container_type), Span::call_site()); + let get_default = match self.compute_default() { + Some(def) => quote!(Some(|| #def.to_dynamic())), + None => quote!(None), + }; + quote!( + crate::meta::ConfigOption { + name: #name, + doc: #doc, + tags: &[], + container: crate::meta::ConfigContainer::#container_type, + type_name: #type_name, + default_value: #get_default, + possible_values: &[], + fields: &[], + } + ) + } + + fn compute_default(&self) -> Option { + let ty = &self.field.ty; + match &self.allow_default { + DefValue::Default => Some(quote!( + <#ty>::default() + )), + DefValue::Path(default) => Some(quote!( + #default() + )), + DefValue::None => None, + } + } +} + +pub fn field_info(field: &Field) -> Result { + let mut name = field.ident.as_ref().unwrap().to_string(); + let mut skip = false; + let mut flatten = false; + let mut allow_default = DefValue::None; + let mut try_from = None; + let mut validate = None; + let mut into = None; + let mut deprecated = None; + let mut doc = String::new(); + let mut container_type = ContainerType::None; + + let type_name = match &field.ty { + Type::Path(p) => { + let last_seg = p.path.segments.last().unwrap(); + match &last_seg.arguments { + PathArguments::None => last_seg.ident.to_string(), + PathArguments::AngleBracketed(args) if args.args.len() == 1 => { + let arg = args.args.first().unwrap(); + match arg { + GenericArgument::Type(Type::Path(t)) => { + container_type = match last_seg.ident.to_string().as_str() { + "Option" => ContainerType::Option, + "Vec" => ContainerType::Vec, + _ => panic!("unhandled type for {name}: {:#?}", field.ty), + }; + t.path.segments.last().unwrap().ident.to_string() + } + _ => panic!("unhandled type for {name}: {:#?}", field.ty), + } + } + PathArguments::AngleBracketed(args) if args.args.len() == 2 => { + let arg = args.args.last().unwrap(); + match arg { + GenericArgument::Type(Type::Path(t)) => { + container_type = match last_seg.ident.to_string().as_str() { + "HashMap" => ContainerType::Map, + _ => panic!("unhandled type for {name}: {:#?}", field.ty), + }; + t.path.segments.last().unwrap().ident.to_string() + } + _ => panic!("unhandled type for {name}: {:#?}", field.ty), + } + } + _ => panic!("unhandled type for {name}: {:#?}", field.ty), + } + } + _ => panic!("unhandled type for {name}: {:#?}", field.ty), + }; + + for attr in &field.attrs { + if !attr.path.is_ident("dynamic") && !attr.path.is_ident("doc") { + continue; + } + + let list = match attr.parse_meta()? { + Meta::List(list) => list, + Meta::NameValue(value) if value.path.is_ident("doc") => { + if let Lit::Str(s) = &value.lit { + if !doc.is_empty() { + doc.push('\n'); + } + doc.push_str(&s.value()); + } + continue; + } + other => { + return Err(Error::new_spanned( + other.clone(), + format!("unsupported attribute {other:?}"), + )) + } + }; + + for meta in &list.nested { + match meta { + NestedMeta::Meta(Meta::NameValue(value)) => { + if value.path.is_ident("rename") { + if let Lit::Str(s) = &value.lit { + name = s.value(); + continue; + } + } + if value.path.is_ident("default") { + if let Lit::Str(s) = &value.lit { + allow_default = DefValue::Path(s.parse()?); + continue; + } + } + if value.path.is_ident("deprecated") { + if let Lit::Str(s) = &value.lit { + deprecated.replace(s.value()); + continue; + } + } + if value.path.is_ident("into") { + if let Lit::Str(s) = &value.lit { + into = Some(s.parse()?); + continue; + } + } + if value.path.is_ident("try_from") { + if let Lit::Str(s) = &value.lit { + try_from = Some(s.parse()?); + continue; + } + } + if value.path.is_ident("validate") { + if let Lit::Str(s) = &value.lit { + validate = Some(s.parse()?); + continue; + } + } + } + NestedMeta::Meta(Meta::Path(path)) => { + if path.is_ident("skip") { + skip = true; + continue; + } + if path.is_ident("flatten") { + flatten = true; + continue; + } + if path.is_ident("default") { + allow_default = DefValue::Default; + continue; + } + } + _ => {} + } + return Err(Error::new_spanned(meta, "unsupported attribute")); + } + } + + Ok(FieldInfo { + type_name, + field, + name, + skip, + flatten, + allow_default, + try_from, + into, + deprecated, + validate, + doc, + container_type, + }) +} diff --git a/config/derive/src/bound.rs b/config/derive/src/bound.rs new file mode 100644 index 000000000..17c5640f1 --- /dev/null +++ b/config/derive/src/bound.rs @@ -0,0 +1,16 @@ +use proc_macro2::TokenStream; +use syn::{parse_quote, Generics, WhereClause, WherePredicate}; + +pub fn where_clause_with_bound(generics: &Generics, bound: TokenStream) -> WhereClause { + let new_predicates = generics.type_params().map::(|param| { + let param = ¶m.ident; + parse_quote!(#param : #bound) + }); + + let mut generics = generics.clone(); + generics + .make_where_clause() + .predicates + .extend(new_predicates); + generics.where_clause.unwrap() +} diff --git a/config/derive/src/configmeta.rs b/config/derive/src/configmeta.rs new file mode 100644 index 000000000..05a02ab2f --- /dev/null +++ b/config/derive/src/configmeta.rs @@ -0,0 +1,64 @@ +use crate::{attr, bound}; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{parse_quote, Data, DataStruct, DeriveInput, Error, Fields, FieldsNamed, Ident, Result}; + +pub fn derive(input: DeriveInput) -> Result { + match &input.data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => derive_struct(&input, fields), + Data::Struct(_) => Err(Error::new( + Span::call_site(), + "currently only structs with named fields are supported", + )), + _ => Err(Error::new( + Span::call_site(), + "currently only structs and enums are supported by this derive", + )), + } +} + +fn derive_struct(input: &DeriveInput, fields: &FieldsNamed) -> Result { + let info = attr::container_info(&input.attrs)?; + let ident = &input.ident; + let (impl_generics, ty_generics, _where_clause) = input.generics.split_for_impl(); + let dummy = Ident::new( + &format!("_IMPL_CONFIGMETA_FOR_{}", ident), + Span::call_site(), + ); + + let options = fields + .named + .iter() + .map(attr::field_info) + .collect::>>()?; + + let options = options + .into_iter() + .filter_map(|f| if f.skip { None } else { Some(f.to_option()) }) + .collect::>(); + + let bound = parse_quote!(crate::ConfigMeta); + let bounded_where_clause = bound::where_clause_with_bound(&input.generics, bound); + + let tokens = quote! { + #[allow(non_upper_case_globals)] + const #dummy: () = { + impl #impl_generics crate::meta::ConfigMeta for #ident #ty_generics #bounded_where_clause { + fn get_config_options(&self) -> &'static [crate::meta::ConfigOption] + { + &[ + #( #options, )* + ] + } + } + }; + }; + + if info.debug { + eprintln!("{}", tokens); + } + Ok(tokens) +} diff --git a/config/derive/src/lib.rs b/config/derive/src/lib.rs new file mode 100644 index 000000000..1bfe50114 --- /dev/null +++ b/config/derive/src/lib.rs @@ -0,0 +1,13 @@ +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput}; + +mod attr; +mod bound; +mod configmeta; + +#[proc_macro_derive(ConfigMeta, attributes(config))] +pub fn derive_config(input: TokenStream) -> TokenStream { + configmeta::derive(parse_macro_input!(input as DeriveInput)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/config/src/config.rs b/config/src/config.rs index 9c3674198..ee4703416 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -38,11 +38,12 @@ use std::time::Duration; use termwiz::hyperlink; use termwiz::surface::CursorShape; use wezterm_bidi::ParagraphDirectionHint; +use wezterm_config_derive::ConfigMeta; use wezterm_dynamic::{FromDynamic, ToDynamic}; use wezterm_input_types::{Modifiers, WindowDecorations}; use wezterm_term::TerminalSize; -#[derive(Debug, Clone, FromDynamic, ToDynamic)] +#[derive(Debug, Clone, FromDynamic, ToDynamic, ConfigMeta)] pub struct Config { /// The font size, measured in points #[dynamic(default = "default_font_size")] diff --git a/config/src/lib.rs b/config/src/lib.rs index 5e0615c4a..7752f765b 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -30,6 +30,7 @@ mod frontend; pub mod keyassignment; mod keys; pub mod lua; +pub mod meta; mod scheme_data; mod ssh; mod terminal; diff --git a/config/src/meta.rs b/config/src/meta.rs new file mode 100644 index 000000000..31fa8c25c --- /dev/null +++ b/config/src/meta.rs @@ -0,0 +1,33 @@ +use wezterm_dynamic::Value; + +/// Trait for returning metadata about config options +pub trait ConfigMeta { + fn get_config_options(&self) -> &'static [ConfigOption]; +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ConfigContainer { + None, + Option, + Vec, + Map, +} + +/// Metadata about a config option +pub struct ConfigOption { + /// The field name + pub name: &'static str, + /// Brief documentation + pub doc: &'static str, + /// TODO: tags to categorize the option + pub tags: &'static [&'static str], + pub container: ConfigContainer, + /// The type of the field + pub type_name: &'static str, + /// call this to get the default value + pub default_value: Option Value>, + /// TODO: For enum types, the set of possible values + pub possible_values: &'static [&'static Value], + /// TODO: For struct types, the fields in the child struct + pub fields: &'static [ConfigOption], +}