diff options
author | Matthias Beyer <matthias.beyer@ifm.com> | 2022-05-12 10:22:38 +0200 |
---|---|---|
committer | Matthias Beyer <matthias.beyer@ifm.com> | 2022-05-12 10:24:24 +0200 |
commit | 50749af654c4687843cc48a69f6d31ea0799658d (patch) | |
tree | f4c838b141aed6975b17aa9944ddc479afcb1eb9 /crates | |
parent | 9e5162fcfd2dfd0ed090f18c698e91b6b0e29908 (diff) | |
parent | 85abb91f4fa91c0f3b0991f5d59d2c5ecc68b8d8 (diff) |
Merge remote-tracking branch 'gitlab-marcel/feature/add_tedge_api_only' into feature/add_tedge_api/integrate-api
Diffstat (limited to 'crates')
-rw-r--r-- | crates/core/tedge_api/Cargo.toml | 8 | ||||
-rw-r--r-- | crates/core/tedge_api/examples/print_config.rs | 149 | ||||
-rw-r--r-- | crates/core/tedge_api/src/config.rs | 379 | ||||
-rw-r--r-- | crates/core/tedge_api/src/lib.rs | 7 | ||||
-rw-r--r-- | crates/core/tedge_api/src/message.rs | 7 | ||||
-rw-r--r-- | crates/core/tedge_api/src/plugin.rs | 13 | ||||
-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 | 382 | ||||
-rw-r--r-- | crates/core/tedge_api/tests/derive_config.rs | 76 |
9 files changed, 1033 insertions, 3 deletions
diff --git a/crates/core/tedge_api/Cargo.toml b/crates/core/tedge_api/Cargo.toml index afcd9800..619e769e 100644 --- a/crates/core/tedge_api/Cargo.toml +++ b/crates/core/tedge_api/Cargo.toml @@ -14,8 +14,14 @@ thiserror = "1.0.30" tokio = { version = "1.16.1", features = ["sync", "time"] } tokio-util = "0.7.0" toml = "0.5.8" +serde = { version = "1.0.136", features = ["derive"] } +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] -serde = { version = "1.0.136", features = ["derive"] } +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 new file mode 100644 index 00000000..ba6e7a6b --- /dev/null +++ b/crates/core/tedge_api/examples/print_config.rs @@ -0,0 +1,149 @@ +use std::collections::HashMap; + +use nu_ansi_term::Color; +use pretty::Arena; +use tedge_api::{ + config::{AsConfig, ConfigDescription, ConfigKind}, + Config, +}; +struct Port(u64); + +impl AsConfig for Port { + fn as_config() -> ConfigDescription { + ConfigDescription::new( + String::from("Integer"), + ConfigKind::Integer, + Some("A TCP port number is an integer between 0 and 65535"), + ) + } +} + +struct VHost; + +impl AsConfig for VHost { + fn as_config() -> ConfigDescription { + ConfigDescription::new( + String::from("VHost"), + ConfigKind::Struct(vec![("name", None, String::as_config())]), + Some("A virtual host definition"), + ) + } +} + +fn main() { + let arena = Arena::new(); + + let doc = Vec::<String>::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 {}", + std::any::type_name::<Vec<String>>() + ); + println!("{}", output); + + let arena = Arena::new(); + + let doc = ConfigDescription::new( + String::from("ServerConfig"), + ConfigKind::Struct(vec![ + ("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*."), + ); + let rendered_doc = doc.as_terminal_doc(&arena); + + let mut output = String::new(); + + rendered_doc.render_fmt(80, &mut output).unwrap(); + + 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 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, + }, + UndocumentedComplex { + num: u16, + foo: f32, + }, + } + + #[derive(Config)] + #[config(untagged)] + enum DebugLevel { + /// Enables debug output + /// + /// And info of course + Debug, + /// Only pertinent information will be logged + Info, + /// A custom debug level + Custom(String), + } + + #[derive(Config)] + struct NginxConfig { + vhosts: Vec<NginxVHost>, + debug_level: DebugLevel, + 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/config.rs b/crates/core/tedge_api/src/config.rs new file mode 100644 index 00000000..90bee595 --- /dev/null +++ b/crates/core/tedge_api/src/config.rs @@ -0,0 +1,379 @@ +use std::collections::HashMap; + +use nu_ansi_term::Color; +use pretty::{Arena, Doc, DocAllocator, Pretty, RefDoc}; +use serde::Serialize; +use termimad::MadSkin; + +/// Generic config that represents what kind of config a plugin wishes to accept +#[derive(Debug, Serialize, PartialEq)] +pub struct ConfigDescription { + name: String, + kind: ConfigKind, + doc: Option<&'static str>, +} + +impl ConfigDescription { + /// Construct a new generic config explanation + #[must_use] + pub fn new(name: String, kind: ConfigKind, doc: Option<&'static str>) -> Self { + Self { name, kind, doc } + } + + /// Get a reference to the config's documentation. + #[must_use] + pub fn doc(&self) -> Option<&'static str> { + self.doc + } + + /// Get a reference to the config's kind. + #[must_use] + pub fn kind(&self) -> &ConfigKind { + &self.kind + } + + /// Set or replace the documentation of this [`Config`] + #[must_use] + pub fn with_doc(mut self, doc: Option<&'static str>) -> Self { + self.doc = doc; + self + } + + /// Get the config's name. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } +} + +/// How an enum is represented +#[derive(Debug, Serialize, PartialEq)] +pub enum EnumVariantRepresentation { + /// The enum is represented by a string + /// + /// This is the case with unit variants for example + String(&'static str), + /// The enum is represented by the value presented here + Wrapped(Box<ConfigDescription>), +} + +/// The kind of enum tagging used by the [`ConfigKind`] +#[derive(Debug, Serialize, PartialEq)] +pub enum ConfigEnumKind { + /// An internal tag with the given tag name + Tagged(&'static str), + /// An untagged enum variant + Untagged, +} + +/// The specific kind a [`Config`] represents +#[derive(Debug, Serialize, PartialEq)] +pub enum ConfigKind { + /// Config represents a boolean `true`/`false` + Bool, + + /// Config represents an integer `1, 10, 200, 10_000, ...` + /// + /// # Note + /// + /// The maximum value that can be represented is between [`i64::MIN`] and [`i64::MAX`] + Integer, + + /// Config represents a floating point value `1.0, 20.235, 3.1419` + /// + /// # Note + /// Integers are also accepted and converted to their floating point variant + /// + /// The maximum value that can be represented is between [`f64::MIN`] and [`f64::MAX`] + Float, + + /// Config represents a string + String, + + /// Wrap another config + /// + /// This is particularly useful if you want to restrict another kind. The common example is a + /// `Port` config object which is represented as a `u16` but with an explanation of what it is + /// meant to represent. + Wrapped(Box<ConfigDescription>), + + /// Config represents an array of values of the given [`ConfigKind`] + Array(Box<ConfigDescription>), + + /// Config represents a hashmap of named configurations of the same type + /// + /// # Note + /// + /// The key is always a [`String`] so this only holds the value config + HashMap(Box<ConfigDescription>), + + /// Config represents a map of different configurations + /// + /// The tuple represent `(field_name, documentation, config_description)` + Struct(Vec<(&'static str, Option<&'static str>, ConfigDescription)>), + + /// Config represents multiple choice of configurations + Enum( + ConfigEnumKind, + Vec<( + &'static str, + Option<&'static str>, + EnumVariantRepresentation, + )>, + ), +} + +/// Turn a plugin configuration into a [`Config`] object +/// +/// Plugin authors are expected to implement this for their configurations to give users +pub trait AsConfig { + /// Get a [`Config`] object from the type + fn as_config() -> ConfigDescription; +} + +impl<T: AsConfig> AsConfig for Vec<T> { + fn as_config() -> ConfigDescription { + ConfigDescription::new( + format!("Array of '{}'s", T::as_config().name()), + ConfigKind::Array(Box::new(T::as_config())), + None, + ) + } +} + +impl<V: AsConfig> AsConfig for HashMap<String, V> { + fn as_config() -> ConfigDescription { + ConfigDescription::new( + format!("Table of '{}'s", V::as_config().name()), + ConfigKind::HashMap(Box::new(V::as_config())), + None, + ) + } +} + +macro_rules! impl_config_kind { + ($kind:expr; $name:expr; $doc:expr => $($typ:ty),+) => { + $( + impl AsConfig for $typ { + fn as_config() -> ConfigDescription { + ConfigDescription::new({$name}.into(), $kind, Some($doc)) + } + } + )+ + }; +} + +impl_config_kind!(ConfigKind::Integer; "Integer"; "A signed integer with 64 bits" => i64); +impl_config_kind!(ConfigKind::Integer; "Integer"; "An unsigned integer with 64 bits" => u64); + +impl_config_kind!(ConfigKind::Integer; "Integer"; "A signed integer with 32 bits" => i32); +impl_config_kind!(ConfigKind::Integer; "Integer"; "An unsigned integer with 32 bits" => u32); + +impl_config_kind!(ConfigKind::Integer; "Integer"; "A signed integer with 16 bits" => i16); +impl_config_kind!(ConfigKind::Integer; "Integer"; "An unsigned integer with 16 bits" => u16); + +impl_config_kind!(ConfigKind::Integer; "Integer"; "A signed integer with 8 bits" => i8); +impl_config_kind!(ConfigKind::Integer; "Integer"; "An unsigned integer with 8 bits" => u8); + +impl_config_kind!(ConfigKind::Float; "Float"; "A floating point value with 64 bits" => f64); +impl_config_kind!(ConfigKind::Float; "Float"; "A floating point value with 32 bits" => f32); + +impl_config_kind!(ConfigKind::Bool; "Boolean"; "A boolean" => bool); +impl_config_kind!(ConfigKind::String; "String"; "An UTF-8 string" => String); + +/******Pretty Printing of Configs******/ + +impl ConfigDescription { + /// Get a [`RcDoc`](pretty::RcDoc) which can be used to write the documentation of this + pub fn as_terminal_doc<'a>(&'a self, arena: &'a Arena<'a>) -> RefDoc<'a> { + let mut doc = arena.nil(); + + if !matches!(self.kind(), ConfigKind::Wrapped(_)) && self.doc().is_none() { + doc = doc + .append(Color::LightBlue.bold().paint(self.name()).to_string()) + .append(arena.space()) + .append(match self.kind() { + ConfigKind::Bool + | ConfigKind::Integer + | ConfigKind::Float + | ConfigKind::String + | ConfigKind::Wrapped(_) + | ConfigKind::Array(_) + | ConfigKind::HashMap(_) => arena.nil(), + ConfigKind::Struct(_) => { + arena.text(Color::Blue.dimmed().paint("[Table]").to_string()) + } + ConfigKind::Enum(_, _) => { + arena.text(Color::Green.dimmed().paint("[Enum]").to_string()) + } + }) + .append(arena.hardline()); + } + + let skin = MadSkin::default_dark(); + let render_markdown = |text: &str| { + let rendered = skin.text(text, None).to_string(); + arena.intersperse( + rendered.split("\n").map(|t| { + arena.intersperse( + t.split(char::is_whitespace).map(|t| t.to_string()), + arena.softline(), + ) + }), + arena.hardline(), + ) + }; + + if let Some(conf_doc) = self.doc() { + doc = doc.append(render_markdown(&conf_doc)); + } + + match self.kind() { + ConfigKind::Bool | ConfigKind::Integer | ConfigKind::Float | ConfigKind::String => (), + ConfigKind::Struct(stc) => { + doc = doc + .append(arena.hardline()) + .append(Color::Blue.paint("[Members]").to_string()) + .append(arena.hardline()) + .append(arena.intersperse( + stc.iter().map(|(member_name, member_doc, member_conf)| { + let mut doc = arena.nil(); + + if let Some(member_doc) = member_doc { + doc = doc.append(render_markdown(&member_doc)); + } + doc.append( + arena.text(Color::Blue.bold().paint(*member_name).to_string()), + ) + .append(": ") + .append( + Pretty::pretty(member_conf.as_terminal_doc(arena), arena).nest(4), + ) + }), + Doc::hardline(), + )) + } + ConfigKind::Enum(enum_kind, variants) => { + doc = doc + .append(arena.hardline()) + .append(Color::Green.paint("One of:").to_string()) + .append(arena.space()) + .append(match enum_kind { + ConfigEnumKind::Tagged(tag) => arena.text( + Color::White + .dimmed() + .paint(format!( + "[Tagged with {}]", + Color::LightGreen + .italic() + .dimmed() + .paint(format!("'{}'", tag)) + )) + .to_string(), + ), + ConfigEnumKind::Untagged => { + arena.text(Color::White.dimmed().paint("[Untagged]").to_string()) + } + }) + .append(arena.hardline()) + .append( + arena.intersperse( + variants + .iter() + .map(|(member_name, member_doc, member_conf)| { + arena.text("-").append(arena.space()).append({ + let mut doc = arena + .nil() + .append(match member_conf { + EnumVariantRepresentation::String(_) => arena.text( + Color::Green + .bold() + .paint(&format!( + "{:?}", + member_name.to_lowercase() + )) + .to_string(), + ), + EnumVariantRepresentation::Wrapped(_) => arena + .text( + Color::Green + .bold() + .paint(*member_name) + .to_string(), + ), + }) + .append(": "); + + if let Some(member_doc) = member_doc { + doc = doc.append(render_markdown(&member_doc)); + } + + doc.append( + Pretty::pretty( + match member_conf { + EnumVariantRepresentation::String(_) => { + arena.nil().into_doc() + } + + EnumVariantRepresentation::Wrapped( + member_conf, + ) => arena + .text( + Color::LightRed + .paint("Is a: ") + .to_string(), + ) + .append(member_conf.as_terminal_doc(arena)) + .into_doc(), + }, + arena, + ) + .nest(4), + ) + .nest(2) + }) + }), + Doc::hardline(), + ), + ); + } + ConfigKind::Array(conf) => { + doc = doc + .append(Color::LightRed.paint("Many of:").to_string()) + .append(arena.space()) + .append(conf.as_terminal_doc(arena)); + } + ConfigKind::HashMap(conf) | ConfigKind::Wrapped(conf) => { + doc = doc.append(conf.as_terminal_doc(arena)); + } + }; + + doc.into_doc() + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::config::{AsConfig, ConfigDescription, ConfigKind}; + + #[test] + fn verify_correct_config_kinds() { + assert!(matches!( + Vec::<f64>::as_config(), + ConfigDescription { + doc: None, + kind: ConfigKind::Array(x), + .. + } if matches!(x.kind(), ConfigKind::Float) + )); + + let complex_config = HashMap::<String, Vec<HashMap<String, String>>>::as_config(); + println!("Complex config: {:#?}", complex_config); + + assert!( + matches!(complex_config.kind(), ConfigKind::HashMap(map) if matches!(map.kind(), ConfigKind::Array(arr) if matches!(arr.kind(), ConfigKind::HashMap(inner_map) if matches!(inner_map.kind(), ConfigKind::String)))) + ); + } +} diff --git a/crates/core/tedge_api/src/lib.rs b/crates/core/tedge_api/src/lib.rs index c2e793f4..5a9f77bb 100644 --- a/crates/core/tedge_api/src/lib.rs +++ b/crates/core/tedge_api/src/lib.rs @@ -10,6 +10,10 @@ pub mod plugin; pub use plugin::{Message, Plugin, PluginBuilder, PluginConfiguration, PluginDirectory, PluginExt}; +/// Generic representation of a configuration +pub mod config; +pub use config::{AsConfig, ConfigDescription, ConfigKind}; + /// Addresses allow plugins to exchange messages pub mod address; pub use address::Address; @@ -26,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/src/message.rs b/crates/core/tedge_api/src/message.rs index 70361581..388d3cd7 100644 --- a/crates/core/tedge_api/src/message.rs +++ b/crates/core/tedge_api/src/message.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::{address::AnyMessageBox, plugin::Message}; /// A message that can contain any other message @@ -42,15 +44,16 @@ impl AnyMessage { impl Message for AnyMessage {} /// The type of a message as used by `tedge_api` to represent a type -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct MessageType { name: &'static str, kind: MessageKind, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] enum MessageKind { Wildcard, + #[serde(skip)] Typed(std::any::TypeId), } diff --git a/crates/core/tedge_api/src/plugin.rs b/crates/core/tedge_api/src/plugin.rs index a3bcbc2c..92f14c2b 100644 --- a/crates/core/tedge_api/src/plugin.rs +++ b/crates/core/tedge_api/src/plugin.rs @@ -13,6 +13,7 @@ use async_trait::async_trait; use crate::{ address::{InternalMessage, ReceiverBundle, ReplySenderFor}, + config::ConfigDescription, error::{DirectoryError, PluginError}, message::{CoreMessages, MessageType}, Address, @@ -169,6 +170,18 @@ pub trait PluginBuilder<PD: PluginDirectory>: Sync + Send + 'static { where Self: Sized; + /// Get a generic configuration description of what kind of input the + /// plugin expects. + /// + /// See [`Config`] as well as [`AsConfig`] for how to implement and use these types and + /// interfaces. + fn kind_configuration() -> Option<ConfigDescription> + where + Self: Sized, + { + None + } + /// Verify the configuration of the plugin for this plugin kind /// /// This function will be used by the core implementation to verify that a given plugin 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..5c34dbf2 --- /dev/null +++ b/crates/core/tedge_api/tedge_config_derive/src/lib.rs @@ -0,0 +1,382 @@ +use proc_macro::TokenStream as TS; +use proc_macro2::TokenStream; +use proc_macro_error::{abort, emit_error, proc_macro_error, OptionExt, 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> { + String(&'q Ident), + 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::EnumVariantRepresentation::Wrapped( + std::boxed::Box::new(::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, |