diff options
author | Marcel Müller <m.mueller@ifm.com> | 2022-05-11 09:05:53 +0200 |
---|---|---|
committer | Marcel Müller <m.mueller@ifm.com> | 2022-05-12 08:51:18 +0200 |
commit | 296e0bc82c532acd77a4ceeb83490bc93628442d (patch) | |
tree | 5d5ab8b56678f2c5bc65a32cc6fd4ca609de886b /crates/core/tedge_api | |
parent | 28cecb8697b4a5eec80503fe2b13a2d1b4917bbe (diff) |
Add Config derive macro
Signed-off-by: Marcel Müller <m.mueller@ifm.com>
Diffstat (limited to 'crates/core/tedge_api')
-rw-r--r-- | crates/core/tedge_api/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/core/tedge_api/examples/print_config.rs | 63 | ||||
-rw-r--r-- | crates/core/tedge_api/src/lib.rs | 3 | ||||
-rw-r--r-- | crates/core/tedge_api/tedge_config_derive/Cargo.toml | 15 | ||||
-rw-r--r-- | crates/core/tedge_api/tedge_config_derive/src/lib.rs | 359 | ||||
-rw-r--r-- | crates/core/tedge_api/tests/derive_config.rs | 66 |
6 files changed, 502 insertions, 6 deletions
diff --git a/crates/core/tedge_api/Cargo.toml b/crates/core/tedge_api/Cargo.toml index 7fdf5508..619e769e 100644 --- a/crates/core/tedge_api/Cargo.toml +++ b/crates/core/tedge_api/Cargo.toml @@ -19,7 +19,9 @@ pretty = { version = "0.11.3", features = ["termcolor"] } termcolor = "1.1.3" termimad = "0.20.1" nu-ansi-term = "0.45.1" +tedge_config_derive = { version = "0.1.0", path = "tedge_config_derive" } [dev-dependencies] +pretty_assertions = "1.2.1" static_assertions = "1.1.0" tokio = { version = "1.16.1", features = ["full"] } diff --git a/crates/core/tedge_api/examples/print_config.rs b/crates/core/tedge_api/examples/print_config.rs index 9cefa5a1..498a79b7 100644 --- a/crates/core/tedge_api/examples/print_config.rs +++ b/crates/core/tedge_api/examples/print_config.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; use nu_ansi_term::Color; use pretty::Arena; -use tedge_api::config::{AsConfig, ConfigDescription, ConfigKind}; +use tedge_api::{ + config::{AsConfig, ConfigDescription, ConfigKind}, + Config, +}; struct Port(u64); impl AsConfig for Port { @@ -21,7 +24,7 @@ impl AsConfig for VHost { fn as_config() -> ConfigDescription { ConfigDescription::new( String::from("VHost"), - ConfigKind::Struct(vec![("name", String::as_config())]), + ConfigKind::Struct(vec![("name", None, String::as_config())]), Some("A virtual host definition"), ) } @@ -48,10 +51,10 @@ fn main() { let doc = ConfigDescription::new( String::from("ServerConfig"), ConfigKind::Struct(vec![ - ("port", Port::as_config()), - ("interface", String::as_config()), - ("virtual_hosts", Vec::<VHost>::as_config()), - ("headers", HashMap::<String, String>::as_config()), + ("port", None, Port::as_config()), + ("interface", None, String::as_config()), + ("virtual_hosts", None, Vec::<VHost>::as_config()), + ("headers", None, HashMap::<String, String>::as_config()), ]), Some("Specify how the server should be started\n\n## Note\n\nThis is a reallly really loooooooooooooooooong loooooooooooooooooooong new *line*."), ); @@ -76,5 +79,53 @@ fn main() { ); println!("------- Output for ServerConfig"); println!("{}", output); + let arena = Arena::new(); + + #[derive(Config)] + #[config(tag = "type")] + /// An Nginx virtual host + /// + /// # Note + /// + /// This is an example and as such is nonsense + enum NginxVHost { + /// A simple host consisting of a string + Simple(String), + /// A more complex host that can also specify its port + Complex { + /// the name of the VHost + name: String, + port: Port, + }, + } + + #[derive(Config)] + struct NginxConfig { + vhosts: Vec<NginxVHost>, + allow_priv_ports: bool, + } + + let doc = NginxConfig::as_config(); + let rendered_doc = doc.as_terminal_doc(&arena); + + let mut output = String::new(); + + rendered_doc.render_fmt(80, &mut output).unwrap(); + + println!("------- Output for NginxConfig"); + println!( + "Configuration for {} plugin kinds", + Color::White.bold().paint(doc.name()) + ); + println!( + "{}", + Color::White.dimmed().bold().paint(format!( + "=================={}=============", + std::iter::repeat('=') + .take(doc.name().len()) + .collect::<String>() + )) + ); + println!("{}", output); println!("-------"); } diff --git a/crates/core/tedge_api/src/lib.rs b/crates/core/tedge_api/src/lib.rs index a6e6b665..5a9f77bb 100644 --- a/crates/core/tedge_api/src/lib.rs +++ b/crates/core/tedge_api/src/lib.rs @@ -30,6 +30,9 @@ pub use message::CoreMessages; /// pub use tokio_util::sync::CancellationToken; +/// Derive macro for self-describing configurations +pub use tedge_config_derive::Config; + #[doc(hidden)] pub mod _internal { pub use futures::future::BoxFuture; diff --git a/crates/core/tedge_api/tedge_config_derive/Cargo.toml b/crates/core/tedge_api/tedge_config_derive/Cargo.toml new file mode 100644 index 00000000..975daf1b --- /dev/null +++ b/crates/core/tedge_api/tedge_config_derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tedge_config_derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.38" +quote = "1.0.18" +syn = { version = "1.0.93", features = ["extra-traits"] } +proc-macro-error = "1.0.4" diff --git a/crates/core/tedge_api/tedge_config_derive/src/lib.rs b/crates/core/tedge_api/tedge_config_derive/src/lib.rs new file mode 100644 index 00000000..a7e87321 --- /dev/null +++ b/crates/core/tedge_api/tedge_config_derive/src/lib.rs @@ -0,0 +1,359 @@ +use proc_macro::TokenStream as TS; +use proc_macro2::TokenStream; +use proc_macro_error::{abort, proc_macro_error, ResultExt}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse_macro_input, Attribute, DeriveInput, Ident, Lit, LitStr, Meta, MetaNameValue, NestedMeta, + Type, +}; + +#[derive(Debug)] +struct ConfigField<'q> { + ident: &'q Ident, + ty: &'q Type, + docs: Option<Vec<LitStr>>, +} + +#[derive(Debug)] +enum ConfigVariantKind<'q> { + Wrapped(&'q Ident, ConfigField<'q>), + Struct(&'q Ident, Vec<ConfigField<'q>>), +} + +#[derive(Debug)] +struct ConfigVariant<'q> { + kind: ConfigVariantKind<'q>, + docs: Option<Vec<LitStr>>, +} + +#[derive(Debug)] +enum ConfigEnumKind { + Tagged(LitStr), + Untagged, +} + +#[derive(Debug)] +enum ConfigQuoteKind<'q> { + Wrapped(&'q Type), + Struct(Vec<ConfigField<'q>>), + Enum(ConfigEnumKind, Vec<ConfigVariant<'q>>), +} + +#[derive(Debug)] +struct ConfigQuote<'q> { + ident: &'q Ident, + docs: Option<Vec<LitStr>>, + kind: ConfigQuoteKind<'q>, +} + +fn lit_strings_to_string_quoted(docs: &Option<Vec<LitStr>>) -> TokenStream { + if let Some(docs) = docs { + let docs = docs + .iter() + .map(|litstr| litstr.value().trim().to_string()) + .collect::<Vec<_>>() + .join("\n"); + quote!(Some(#docs)) + } else { + quote!(None) + } +} + +fn extract_docs_from_attributes<'a>( + attrs: impl Iterator<Item = &'a Attribute>, +) -> Option<Vec<LitStr>> { + let attrs = attrs + .filter_map(|attr| { + if let Ok(Meta::NameValue(meta)) = attr.parse_meta() { + if meta.path.is_ident("doc") { + if let Lit::Str(litstr) = meta.lit { + return Some(litstr); + } + } + } + None + }) + .collect::<Vec<_>>(); + + if attrs.is_empty() { + None + } else { + Some(attrs) + } +} + +impl<'q> ToTokens for ConfigQuote<'q> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident_name = self.ident.to_string(); + let outer_docs = lit_strings_to_string_quoted(&self.docs); + + tokens.append_all(match &self.kind { + ConfigQuoteKind::Wrapped(ty) => { + quote! { + ::tedge_api::config::ConfigDescription::new( + ::std::string::String::from(#ident_name), + ::tedge_api::config::ConfigKind::Wrapped( + ::std::boxed::Box::new(<#ty as ::tedge_api::AsConfig>::as_config()) + ), + #outer_docs + ) + } + } + ConfigQuoteKind::Struct(fields) => { + let ident = fields.iter().map(|f| f.ident.to_string()); + let ty = fields.iter().map(|f| f.ty); + let docs = fields.iter().map(|f| lit_strings_to_string_quoted(&f.docs)); + + quote! { + ::tedge_api::config::ConfigDescription::new( + ::std::string::String::from(#ident_name), + ::tedge_api::config::ConfigKind::Struct( + vec![ + #( + (#ident, #docs, <#ty as ::tedge_api::AsConfig>::as_config()) + ),* + ] + ), + #outer_docs + ) + } + } + ConfigQuoteKind::Enum(kind, variants) => { + let kind = match kind { + ConfigEnumKind::Tagged(tag) => { + quote! { + ::tedge_api::config::ConfigEnumKind::Tagged(#tag) + } + } + ConfigEnumKind::Untagged => { + quote! { + ::tedge_api::config::ConfigEnumKind::Untagged + } + } + }; + + let variants = variants.iter().map(|var| { + let docs = lit_strings_to_string_quoted(&var.docs); + match &var.kind { + ConfigVariantKind::Wrapped(ident, ConfigField { ty, .. }) => { + // we ignore the above docs since the outer docs ar ethe important ones + // TODO: Emit an error if an inner type in a enum is annotated + let ident = ident.to_string(); + quote!{ + ( + #ident, + #docs, + ::tedge_api::config::ConfigDescription::new( + ::std::string::String::from(#ident), + ::tedge_api::config::ConfigKind::Wrapped( + std::boxed::Box::new(<#ty as ::tedge_api::AsConfig>::as_config()) + ), + None, + ) + ) + } + } + ConfigVariantKind::Struct(ident, fields) => { + let ident = ident.to_string(); + let idents = fields.iter().map(|f| f.ident.to_string()); + let field_docs = fields.iter().map(|f| lit_strings_to_string_quoted(&f.docs)); + let tys = fields.iter().map(|f| f.ty); + + quote! { + ( + #ident, + #docs, + ::tedge_api::config::ConfigDescription::new( + ::std::string::String::from(#ident), + ::tedge_api::config::ConfigKind::Struct( + vec![ + #( + (#idents, #field_docs, <#tys as ::tedge_api::AsConfig>::as_config()) + ),* + ] + ), + None + ) + ) + } + } + } + }); + + quote! { + ::tedge_api::config::ConfigDescription::new( + ::std::string::String::from(#ident_name), + ::tedge_api::config::ConfigKind::Enum( + #kind, + vec![#(#variants),*] + ), + #outer_docs + ) + } + } + }); + } +} + +#[proc_macro_derive(Config, attributes(config))] +#[proc_macro_error] +pub fn derive_config(input: TS) -> TS { + let input = parse_macro_input!(input as DeriveInput); + + let ident = &input.ident; + + let config_desc_kind: ConfigQuoteKind = match &input.data { + syn::Data::Struct(data) => match &data.fields { + syn::Fields::Named(fields) => ConfigQuoteKind::Struct( + fields + .named + .iter() + .map(|f| ConfigField { + ident: &f.ident.as_ref().unwrap(), + ty: &f.ty, + docs: extract_docs_from_attributes(f.attrs.iter()), + }) + .collect(), + ), + syn::Fields::Unnamed(fields) => { + if fields.unnamed.len() != 1 { + abort!( + fields, + "Tuple structs should only contain a single variant." + ) + } + ConfigQuoteKind::Wrapped(&fields.unnamed.first().unwrap().ty) + } + syn::Fields::Unit => abort!( + ident, + "Unit structs are not supported as they cannot be represented" + ), + }, + syn::Data::Enum(data) => { + let enum_kind: ConfigEnumKind = { + let potential_kind = input + .attrs + .iter() + .find(|attr| attr.path.is_ident("config")) + .unwrap_or_else(|| { + abort!(ident, "Enums need to specify what kind of tagging they use"; + help = "Use #[config(untagged)] for untagged enums, and #[config(tag = \"type\")] for internally tagged variants. Other kinds are not supported.") + }); + + macro_rules! abort_parse_enum_kind { + ($kind:expr) => { + abort!($kind, "Could not parse enum tag kind."; + help = "Accepted kinds are 'untagged' and 'tag = \"type\'") + } + } + + match potential_kind + .parse_meta() + .expect_or_abort("Could not parse #[config] meta attribute.") + { + syn::Meta::Path(kind) => { + if kind.is_ident("untagged") { + ConfigEnumKind::Untagged + } else { + abort_parse_enum_kind!(kind) + } + } + syn::Meta::List(kind) => { + if kind.nested.len() != 1 { + abort_parse_enum_kind!(kind) + } + + if let Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(lit_str), + .. + }))) = kind.nested.first() + { + if path.is_ident("tag") { + ConfigEnumKind::Tagged(lit_str.clone()) + } else { + abort_parse_enum_kind!(kind) + } + } else { + abort_parse_enum_kind!(kind) + } + } + syn::Meta::NameValue(kind) => abort!( + kind, + "The #[config] attribute cannot be used as a name-value attribute."; + help = "Maybe you meant #[config(tag = \"type\")] to describe that this enum has an internal tag?" + ), + } + }; + + let variants = data + .variants + .iter() + .map(|var| { + let kind = match &var.fields { + syn::Fields::Named(fields) => ConfigVariantKind::Struct( + &var.ident, + fields + .named + .iter() + .map(|f| ConfigField { + ident: &f.ident.as_ref().unwrap(), + ty: &f.ty, + docs: extract_docs_from_attributes(f.attrs.iter()), + }) + .collect(), + ), + syn::Fields::Unnamed(fields) => { + if fields.unnamed.len() != 1 { + abort!( + fields, + "Tuple structs should only contain a single variant." + ) + } + ConfigVariantKind::Wrapped( + &var.ident, + ConfigField { + ident: &var.ident, + ty: &fields.unnamed.first().unwrap().ty, + docs: extract_docs_from_attributes(var.attrs.iter()), + }, + ) + } + syn::Fields::Unit => abort!( + ident, + "Unit structs are not supported as they cannot be represented" + ), + }; + let docs = extract_docs_from_attributes(var.attrs.iter()); + ConfigVariant { kind, docs } + }) + .collect(); + + ConfigQuoteKind::Enum(enum_kind, variants) + } + syn::Data::Union(_) => { + abort!( + ident, + "Untagged unions are not supported. Consider using an enum instead." + ); + } + }; + + let docs = extract_docs_from_attributes(input.attrs.iter()); + + let config_desc = ConfigQuote { + kind: config_desc_kind, + docs, + ident, + }; + + let expanded = quote! { + impl ::tedge_api::config::AsConfig for #ident { + fn as_config() -> ::tedge_api::config::ConfigDescription { + #config_desc + } + } + }; + + TS::from(expanded) +} diff --git a/crates/core/tedge_api/tests/derive_config.rs b/crates/core/tedge_api/tests/derive_config.rs new file mode 100644 index 00000000..dfe57ef3 --- /dev/null +++ b/crates/core/tedge_api/tests/derive_config.rs @@ -0,0 +1,66 @@ +#![allow(unused, dead_code)] + +use pretty_assertions::assert_eq; +use tedge_api::{AsConfig, Config, ConfigDescription, ConfigKind}; + +/// Some Config +#[derive(Debug, Config)] +struct SimpleConfig { + /// The port to connect to + port: Port, + name: String, + /// A nested configuration + /// + /// # This also includes markdown + /// + /// And can go over several _lines_ + nested: NestedConfig, +} + +#[derive(Debug, Config)] +/// Nested configuration can have its own documentation +struct NestedConfig { + num: EnumConfig, +} + +#[derive(Debug, Config)] +struct Port(u16); + +#[derive(Debug, Config)] +#[config(tag = "type")] +/// An enum configuration +enum EnumConfig { + String(String), + Num(u64), + /// Some docs on the complex type + Complex { + /// The port of the inner complex type + port: Port, + other: String, + }, +} + +#[test] +fn check_derive_config() { + let conf = SimpleConfig::as_config(); + + println!("{:#?}", conf); + + assert!(matches!(conf.kind(), ConfigKind::Struct(_))); + assert_eq!(conf.doc().unwrap(), "Some Config"); + + assert_eq!(Port::as_config().doc(), None); + + if let ConfigKind::Enum(_, variants) = EnumConfig::as_config().kind() { + if let ConfigKind::Struct(fields) = variants[2].2.kind() { + assert_eq!( + fields[0].1, + Some("The port of the inner complex type") + ) + } else { + panic!() + } + } else { + panic!(); + } +} |