summaryrefslogtreecommitdiffstats
path: root/crates/core/tedge_api/tedge_config_derive/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/tedge_api/tedge_config_derive/src/lib.rs')
-rw-r--r--crates/core/tedge_api/tedge_config_derive/src/lib.rs382
1 files changed, 382 insertions, 0 deletions
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..b2a97098
--- /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, 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,
+ ))
+ )
+ )
+ }
+ }
+ 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::EnumVariantRepresentation::Wrapped(
+ std::boxed::Box::new(::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
+ ))
+ )
+ )
+ }
+ }
+ ConfigVariantKind::String(ident) => {
+ let ident = ident.to_string();
+ quote!{
+ (
+ #ident,
+ #docs,
+ ::tedge_api::config::EnumVariantRepresentation::String(
+ #ident
+ )
+ )
+ }
+ }
+ }
+ });
+
+ 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 #[config(untagged)] and #[config(tag = \"type\')].")
+ }
+ }
+
+ match potential_kind
+ .parse_meta()
+ .expect_or_abort("Could not parse #[config] meta attribute.")
+ {
+ syn::Meta::Path(kind) => {
+ abort_parse_enum_kind!(kind)
+ }
+ syn::Meta::List(kind) => {
+ if kind.nested.len() != 1 {
+ abort_parse_enum_kind!(kind)
+ }
+
+ match kind.nested.first() {
+ Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
+ path,
+ lit: Lit::Str(lit_str),
+ ..
+ }))) => {
+ if path.is_ident("tag") {
+ ConfigEnumKind::Tagged(lit_str.clone())
+ } else {
+ abort_parse_enum_kind!(kind)
+ }
+ }
+ Some(NestedMeta::Meta(Meta::Path(path))) => {
+ if path.is_ident("untagged") {
+ ConfigEnumKind::Untagged
+ } else {
+ abort_parse_enum_kind!(path)
+ }
+ }
+ _ => {
+ println!("Oh no!");
+ 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 => ConfigVariantKind::String(&var.ident),
+ };
+ let docs = extract_docs_from_attributes(var.attrs.iter());
+ Some(ConfigVariant { kind, docs })
+ })
+ .collect::<Option<_>>();
+
+ ConfigQuoteKind::Enum(
+ enum_kind,
+ variants.expect_or_abort("Enum contains invalid 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)
+}