summaryrefslogtreecommitdiffstats
path: root/crates/core/tedge_api
diff options
context:
space:
mode:
authorMarcel Müller <m.mueller@ifm.com>2022-05-11 09:05:53 +0200
committerMarcel Müller <m.mueller@ifm.com>2022-05-12 08:51:18 +0200
commit296e0bc82c532acd77a4ceeb83490bc93628442d (patch)
tree5d5ab8b56678f2c5bc65a32cc6fd4ca609de886b /crates/core/tedge_api
parent28cecb8697b4a5eec80503fe2b13a2d1b4917bbe (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.toml2
-rw-r--r--crates/core/tedge_api/examples/print_config.rs63
-rw-r--r--crates/core/tedge_api/src/lib.rs3
-rw-r--r--crates/core/tedge_api/tedge_config_derive/Cargo.toml15
-rw-r--r--crates/core/tedge_api/tedge_config_derive/src/lib.rs359
-rw-r--r--crates/core/tedge_api/tests/derive_config.rs66
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!();
+ }
+}