diff --git a/proxmox-api-macro/src/api/attributes.rs b/proxmox-api-macro/src/api/attributes.rs index b4ba13c5..1d18aab1 100644 --- a/proxmox-api-macro/src/api/attributes.rs +++ b/proxmox-api-macro/src/api/attributes.rs @@ -68,3 +68,45 @@ impl UpdaterFieldAttributes { } } } + +#[derive(Default)] +pub struct EnumFieldAttributes { + /// Change the "type-key" for this entry type.. + type_key: Option, +} + +impl EnumFieldAttributes { + pub fn from_attributes(input: &mut Vec) -> Self { + let mut this = Self::default(); + + for attr in std::mem::take(input) { + if attr.style != syn::AttrStyle::Outer || !attr.path().is_ident("api") { + input.push(attr); + continue; + } + match attr.parse_nested_meta(|meta| this.parse(meta)) { + Ok(()) => (), + Err(err) => crate::add_error(err), + } + } + + this + } + + fn parse(&mut self, meta: ParseNestedMeta<'_>) -> Result<(), syn::Error> { + let path = &meta.path; + + if path.is_ident("type_key") { + util::duplicate(&self.type_key, path); + self.type_key = Some(meta.value()?.parse()?); + } else { + return Err(meta.error(format!("invalid api attribute: {path:?}"))); + } + + Ok(()) + } + + pub fn type_key(&self) -> Option<&syn::LitStr> { + self.type_key.as_ref() + } +} diff --git a/proxmox-api-macro/src/api/enums.rs b/proxmox-api-macro/src/api/enums.rs index e42c886c..cb72ec84 100644 --- a/proxmox-api-macro/src/api/enums.rs +++ b/proxmox-api-macro/src/api/enums.rs @@ -6,6 +6,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote_spanned; use syn::spanned::Spanned; +use super::attributes::EnumFieldAttributes; use super::Schema; use crate::serde; use crate::util::{self, FieldName, JSONObject, JSONValue, Maybe}; @@ -158,7 +159,7 @@ fn handle_string_enum( fn handle_section_config_enum( mut attribs: JSONObject, - enum_ty: syn::ItemEnum, + mut enum_ty: syn::ItemEnum, ) -> Result { let name = &enum_ty.ident; @@ -186,6 +187,13 @@ fn handle_section_config_enum( Some(name) => name.try_into()?, None => bail!(name => "missing 'id-property' property for SectionConfig style enum"), }; + let with_type_key: TokenStream = match attribs.remove("type-key") { + Some(value) => { + let value: syn::LitStr = value.try_into()?; + quote_spanned!(value.span() => .with_type_key(#value)) + } + None => TokenStream::new(), + }; let container_attrs = serde::ContainerAttrib::try_from(&enum_ty.attrs[..])?; let Some(tag) = container_attrs.tag.as_ref() else { @@ -195,7 +203,7 @@ fn handle_section_config_enum( let mut variants = TokenStream::new(); let mut register_sections = TokenStream::new(); let mut to_type = TokenStream::new(); - for variant in &enum_ty.variants { + for variant in &mut enum_ty.variants { let field = match &variant.fields { syn::Fields::Unnamed(field) if field.unnamed.len() == 1 => &field.unnamed[0], _ => bail!(variant => "SectionConfig style enum can only have newtype variants"), @@ -212,6 +220,13 @@ fn handle_section_config_enum( syn::LitStr::new(&name.to_string(), name.span()) }; + let field_attrs = EnumFieldAttributes::from_attributes(&mut variant.attrs); + let with_type_key = if let Some(key) = field_attrs.type_key() { + quote_spanned!(key.span() => .with_type_key(#key)) + } else { + TokenStream::new() + }; + let variant_ident = &variant.ident; let ty = &field.ty; variants.extend(quote_spanned! { variant.ident.span() => @@ -221,17 +236,20 @@ fn handle_section_config_enum( ), }); register_sections.extend(quote_spanned! { variant.ident.span() => - this.register_plugin(::proxmox_section_config::SectionConfigPlugin::new( - #variant_string.to_string(), - Some(#id_property.to_string()), - const { - match &<#ty as ::proxmox_schema::ApiType>::API_SCHEMA { - ::proxmox_schema::Schema::Object(schema) => schema, - ::proxmox_schema::Schema::OneOf(schema) => schema, - _ => panic!("enum requires an object schema"), + this.register_plugin( + ::proxmox_section_config::SectionConfigPlugin::new( + #variant_string.to_string(), + Some(#id_property.to_string()), + const { + match &<#ty as ::proxmox_schema::ApiType>::API_SCHEMA { + ::proxmox_schema::Schema::Object(schema) => schema, + ::proxmox_schema::Schema::OneOf(schema) => schema, + _ => panic!("enum requires an object schema"), + } } - } - )); + ) + #with_type_key + ); }); to_type.extend(quote_spanned! { variant.ident.span() => Self::#variant_ident(_) => #variant_string, @@ -265,7 +283,8 @@ fn handle_section_config_enum( .type_property_entry .2 }; - let mut this = ::proxmox_section_config::SectionConfig::new(id_schema); + let mut this = ::proxmox_section_config::SectionConfig::new(id_schema) + #with_type_key; #register_sections this }) diff --git a/proxmox-api-macro/tests/section-config.rs b/proxmox-api-macro/tests/section-config.rs index 3f5b3d1e..97f89dbc 100644 --- a/proxmox-api-macro/tests/section-config.rs +++ b/proxmox-api-macro/tests/section-config.rs @@ -23,6 +23,10 @@ pub struct TypeB { /// An age. age: u64, + + /// The internally tagged type. + #[serde(rename = "the-type")] + ty: String, } #[api( @@ -37,6 +41,7 @@ pub struct TypeB { #[serde(tag = "type")] pub enum Config { A(TypeA), + #[api(type_key = "the-type")] B(TypeB), } @@ -66,6 +71,7 @@ fn test_config() { Config::B(TypeB { id: "the-b".to_string(), age: 42, + ty: "B".to_string(), }) ); @@ -73,3 +79,70 @@ fn test_config() { .expect("failed to write out test section config"); assert_eq!(raw, content); } + +#[api] +/// Type A2. +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] +pub struct TypeA2 { + /// The id. + id: String, + + /// Some name. + name: String, + + /// The internally tagged type. + ty: String, +} + +#[api( + "id-property": "id", + "id-schema": { + type: String, + description: "A config ID", + max_length: 16, + }, + "type-key": "ty", +)] +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum ConfigTypeKey { + A2(TypeA2), + #[api(type_key = "the-type")] + B(TypeB), +} + +#[test] +fn test_global_type_key() { + let content = "\ + a2: the-a\n\ + \tname The Name\n\ + \n\ + b: the-b\n\ + \tage 42\n\ + "; + + let data = ConfigTypeKey::parse_section_config("a_test_file.cfg", content) + .expect("failed to parse test section config"); + + assert_eq!(data.len(), 2); + assert_eq!( + data["the-a"], + ConfigTypeKey::A2(TypeA2 { + id: "the-a".to_string(), + name: "The Name".to_string(), + ty: "a2".to_string(), + }) + ); + assert_eq!( + data["the-b"], + ConfigTypeKey::B(TypeB { + id: "the-b".to_string(), + age: 42, + ty: "b".to_string(), + }) + ); + + let raw = ConfigTypeKey::write_section_config("a_test_output_file.cfg", &data) + .expect("failed to write out test section config"); + assert_eq!(raw, content); +}