forked from Proxmox/proxmox
api-macro: type-key support for derived enums
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
4a154b3cb5
commit
839f508f55
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user