api-macro: type-key support for derived enums

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2024-08-05 13:47:11 +02:00
parent 4a154b3cb5
commit 839f508f55
3 changed files with 147 additions and 13 deletions

View File

@ -68,3 +68,45 @@ impl UpdaterFieldAttributes {
}
}
}
#[derive(Default)]
pub struct EnumFieldAttributes {
/// Change the "type-key" for this entry type..
type_key: Option<syn::LitStr>,
}
impl EnumFieldAttributes {
pub fn from_attributes(input: &mut Vec<syn::Attribute>) -> 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()
}
}

View File

@ -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<TokenStream, Error> {
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
})

View File

@ -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);
}