summaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorMatthias Beyer <matthias.beyer@ifm.com>2022-05-12 10:22:38 +0200
committerMatthias Beyer <matthias.beyer@ifm.com>2022-05-12 10:24:24 +0200
commit50749af654c4687843cc48a69f6d31ea0799658d (patch)
treef4c838b141aed6975b17aa9944ddc479afcb1eb9 /crates
parent9e5162fcfd2dfd0ed090f18c698e91b6b0e29908 (diff)
parent85abb91f4fa91c0f3b0991f5d59d2c5ecc68b8d8 (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.toml8
-rw-r--r--crates/core/tedge_api/examples/print_config.rs149
-rw-r--r--crates/core/tedge_api/src/config.rs379
-rw-r--r--crates/core/tedge_api/src/lib.rs7
-rw-r--r--crates/core/tedge_api/src/message.rs7
-rw-r--r--crates/core/tedge_api/src/plugin.rs13
-rw-r--r--crates/core/tedge_api/tedge_config_derive/Cargo.toml15
-rw-r--r--crates/core/tedge_api/tedge_config_derive/src/lib.rs382
-rw-r--r--crates/core/tedge_api/tests/derive_config.rs76
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,