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 | |
parent | 28cecb8697b4a5eec80503fe2b13a2d1b4917bbe (diff) |
Add Config derive macro
Signed-off-by: Marcel Müller <m.mueller@ifm.com>
-rw-r--r-- | Cargo.lock | 148 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-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 |
8 files changed, 583 insertions, 74 deletions
@@ -72,9 +72,9 @@ checksum = "48ad219abc0c06ca788aface2e3a1970587e3413ab70acd20e54b6ec524c1f8f" dependencies = [ "argh_shared", "heck", - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -168,9 +168,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -179,9 +179,9 @@ version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -675,8 +675,8 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" dependencies = [ - "quote 1.0.10", - "syn 1.0.82", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -693,7 +693,7 @@ checksum = "c73af209b6a5dc8ca7cbaba720732304792cddc933cfea3d74509c2b1ef2f436" dependencies = [ "num-bigint 0.4.3", "num-traits", - "syn 1.0.82", + "syn 1.0.93", ] [[package]] @@ -976,9 +976,9 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -1404,9 +1404,9 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45a95a48d0bc28f9af628286e8a4da09f96f34a97744a2e9a5a4db9814ad527d" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -1508,9 +1508,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7e25b214433f669161f414959594216d8e6ba83b6679d3db96899c0b4639033" dependencies = [ "cfg-if 1.0.0", - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -1874,9 +1874,9 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -2018,9 +2018,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.0.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0cfe1b2403f172ba0f234e500906ee0a3e493fb81092dac23ebefe129301cc" +checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563" dependencies = [ "ansi_term", "ctor", @@ -2045,9 +2045,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", "version_check", ] @@ -2057,8 +2057,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", + "proc-macro2 1.0.38", + "quote 1.0.18", "version_check", ] @@ -2073,9 +2073,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" dependencies = [ "unicode-xid 0.2.2", ] @@ -2123,11 +2123,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ - "proc-macro2 1.0.32", + "proc-macro2 1.0.38", ] [[package]] @@ -2580,9 +2580,9 @@ version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -2625,9 +2625,9 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -2747,9 +2747,9 @@ checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -2765,12 +2765,12 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.82" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +checksum = "04066589568b72ec65f42d65a1a52436e954b168773148893c020269563decf2" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", + "proc-macro2 1.0.38", + "quote 1.0.18", "unicode-xid 0.2.2", ] @@ -2861,8 +2861,10 @@ dependencies = [ "miette", "nu-ansi-term", "pretty", + "pretty_assertions", "serde", "static_assertions", + "tedge_config_derive", "termcolor", "termimad", "thiserror", @@ -2903,6 +2905,16 @@ dependencies = [ ] [[package]] +name = "tedge_config_derive" +version = "0.1.0" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", +] + +[[package]] name = "tedge_dummy_plugin" version = "0.5.2" dependencies = [ @@ -3021,9 +3033,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7cad0a06f9a61e94355aa3b3dc92d85ab9c83406722b1ca5e918d4297c12c23" dependencies = [ "cfg-if 1.0.0", - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", "version_check", ] @@ -3072,9 +3084,9 @@ version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -3165,9 +3177,9 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -3291,9 +3303,9 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", ] [[package]] @@ -3589,9 +3601,9 @@ dependencies = [ "bumpalo", "lazy_static", "log", - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", "wasm-bindgen-shared", ] @@ -3613,7 +3625,7 @@ version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ - "quote 1.0.10", + "quote 1.0.18", "wasm-bindgen-macro-support", ] @@ -3623,9 +3635,9 @@ version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ - "proc-macro2 1.0.32", - "quote 1.0.10", - "syn 1.0.82", + "proc-macro2 1.0.38", + "quote 1.0.18", + "syn 1.0.93", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3,6 +3,7 @@ members = [ "crates/common/*", "crates/core/*", + "crates/core/tedge_api/tedge_config_derive", "crates/tests/*", "plugins/tedge_apt_plugin", "plugins/tedge_dummy_plugin", 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, |