proxmox/proxmox-api-macro/src/api/structs.rs
Wolfgang Bumiller 51d78fdd2b api-macro: handle renames in updater derive
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2024-08-06 14:14:42 +02:00

570 lines
19 KiB
Rust

//! `#[api]` macro for `struct` types.
//!
//! This module implements struct handling.
//!
//! We distinguish between 3 types at the moment:
//! 1) Unit structs (`struct Foo;`),which don't do much really and aren't very useful for the API
//! currently)
//! 2) Newtypes (`struct Foo(T)`), a 1-tuple, which is supposed to be a wrapper for a type `T` and
//! therefore should implicitly deserialize/serialize to `T`. Currently we only support simple
//! types for which we "know" the schema type used in the API.
//! 3) Object structs (`struct Foo { ... }`), which declare an `ObjectSchema`.
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use anyhow::Error;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote_spanned;
use super::attributes::UpdaterFieldAttributes;
use super::Schema;
use crate::api::{self, ObjectEntry, SchemaItem};
use crate::serde;
use crate::util::{self, FieldName, JSONObject, Maybe};
pub fn handle_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<TokenStream, Error> {
match &stru.fields {
// unit structs, not sure about these?
syn::Fields::Unit => handle_unit_struct(attribs, stru),
syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
handle_newtype_struct(attribs, stru)
}
syn::Fields::Unnamed(fields) => bail!(
fields.paren_token.span.open(),
"api macro does not support tuple structs"
),
syn::Fields::Named(_) => handle_regular_struct(attribs, stru),
}
}
fn get_struct_description(schema: &mut Schema, stru: &syn::ItemStruct) -> Result<(), Error> {
if schema.description.is_none() {
let (doc_comment, doc_span) = util::get_doc_comments(&stru.attrs)?;
util::derive_descriptions(schema, None, &doc_comment, doc_span)?;
}
Ok(())
}
fn handle_unit_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<TokenStream, Error> {
// unit structs, not sure about these?
let mut schema: Schema = if attribs.is_empty() {
Schema::empty_object(Span::call_site())
} else {
attribs.try_into()?
};
get_struct_description(&mut schema, &stru)?;
let name = &stru.ident;
let mut schema = finish_schema(schema, &stru, name)?;
schema.extend(quote_spanned! { name.span() =>
impl ::proxmox_schema::UpdaterType for #name {
type Updater = Option<Self>;
}
});
Ok(schema)
}
fn finish_schema(
schema: Schema,
stru: &syn::ItemStruct,
name: &Ident,
) -> Result<TokenStream, Error> {
let schema = {
let mut ts = TokenStream::new();
schema.to_schema(&mut ts)?;
ts
};
Ok(quote_spanned! { name.span() =>
#stru
impl ::proxmox_schema::ApiType for #name {
const API_SCHEMA: ::proxmox_schema::Schema = #schema;
}
})
}
fn handle_newtype_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<TokenStream, Error> {
// Ideally we could clone the contained item's schema, but this is "hard", so for now we assume
// the contained type is a simple type.
//
// In order to support "specializing" an already existing type, we'd need to be able to
// create "linked" schemas. We cannot do this purely via the macro.
let mut schema: Schema = attribs.try_into()?;
if let SchemaItem::Inferred(_span) = schema.item {
// The schema has no `type` and we failed to guess it. Infer it from the contained field!
let fields = match &stru.fields {
syn::Fields::Unnamed(fields) => &fields.unnamed,
// `handle_struct()` verified this!
_ => panic!("handle_unit_struct on non-unit struct"),
};
// this is also part of `handle_struct()`'s verification!
assert_eq!(
fields.len(),
1,
"handle_unit_struct needs a struct with exactly 1 field"
);
// Now infer the type information:
util::infer_type(&mut schema, &fields[0].ty)?;
}
get_struct_description(&mut schema, &stru)?;
finish_schema(schema, &stru, &stru.ident)
}
fn handle_regular_struct(
attribs: JSONObject,
mut stru: syn::ItemStruct,
) -> Result<TokenStream, Error> {
let mut schema: Schema = if attribs.is_empty() {
Schema::empty_object(Span::call_site())
} else {
attribs.try_into()?
};
get_struct_description(&mut schema, &stru)?;
// sanity check, first get us some quick by-name access to our fields:
//
// NOTE: We remove references we're "done with" and in the end fail with a list of extraneous
// fields if there are any.
let mut schema_fields: HashMap<String, &mut ObjectEntry> = HashMap::new();
// We also keep a reference to the SchemaObject around since we derive missing fields
// automatically.
if let SchemaItem::Object(obj) = &mut schema.item {
for field in obj.properties_mut() {
schema_fields.insert(field.name.as_str().to_string(), field);
}
} else {
error!(schema.span, "structs need an object schema");
}
let mut new_fields: Vec<ObjectEntry> = Vec::new();
let container_attrs = serde::ContainerAttrib::try_from(&stru.attrs[..])?;
let mut all_of_schemas = TokenStream::new();
let mut to_remove = Vec::new();
if let syn::Fields::Named(ref fields) = &stru.fields {
for field in &fields.named {
let attrs = serde::FieldAttrib::try_from(&field.attrs[..])?;
let (name, span) = {
let ident: &Ident = field
.ident
.as_ref()
.ok_or_else(|| format_err!(field => "field without name?"))?;
if let Some(renamed) = attrs.rename.clone() {
(renamed.value(), ident.span())
} else if let Some(rename_all) = container_attrs.rename_all {
let name = rename_all.apply_to_field(&ident.to_string());
(name, ident.span())
} else {
(ident.to_string(), ident.span())
}
};
match schema_fields.remove(&name) {
Some(field_def) => {
if attrs.flatten {
to_remove.push(name.clone());
if field_def.schema.description.is_explicit() {
error!(
field_def.name.span(),
"flattened field should not have a description, \
it does not appear in serialized data as a field",
);
}
if field_def.optional.expect_bool() {
// openapi & json schema don't exactly have a proper way to represent
// this, so we simply refuse:
error!(
field_def.name.span(),
"optional flattened fields are not supported (by JSONSchema)"
);
}
}
handle_regular_field(field_def, field, false, &attrs)?;
if attrs.flatten {
all_of_schemas.extend(quote::quote! {&});
field_def.schema.to_schema(&mut all_of_schemas)?;
all_of_schemas.extend(quote::quote! {,});
}
}
None => {
let mut field_def = ObjectEntry::new(
FieldName::new(name.clone(), span),
false,
Schema::blank(span),
);
handle_regular_field(&mut field_def, field, true, &attrs)?;
if attrs.flatten {
all_of_schemas.extend(quote::quote! {&});
field_def.schema.to_schema(&mut all_of_schemas)?;
all_of_schemas.extend(quote::quote! {,});
to_remove.push(name.clone());
} else {
new_fields.push(field_def);
}
}
}
}
} else {
panic!("handle_regular struct without named fields");
};
// now error out about all the fields not found in the struct:
if !schema_fields.is_empty() {
let bad_fields = util::join(", ", schema_fields.keys());
error!(
schema.span,
"struct does not contain the following fields: {}", bad_fields
);
}
{
let obj = schema.item.check_object_mut()?;
// remove flattened fields
for field in to_remove {
//if !obj.remove_property_by_ident(&field)
if let Some(item) = obj.find_property_by_ident_mut(&field) {
item.flatten_in_struct = true;
} else {
error!(
schema.span,
"internal error: failed to remove property {:?} from object schema", field,
);
}
}
// add derived fields
obj.extend_properties(new_fields);
}
let updater = {
let mut derive = false;
util::retain_derived_items(&mut stru.attrs, |path| {
if path.is_ident("Updater") {
derive = true;
true // FIXME: remove retain again?
} else {
true
}
});
if derive {
let updater =
derive_updater(stru.clone(), schema.clone(), &mut stru, &container_attrs)?;
// make sure we don't leave #[updater] attributes on the original struct:
if let syn::Fields::Named(fields) = &mut stru.fields {
for field in &mut fields.named {
let _ = UpdaterFieldAttributes::from_attributes(&mut field.attrs);
}
}
updater
} else {
TokenStream::new()
}
};
let mut output = if all_of_schemas.is_empty() {
finish_schema(schema, &stru, &stru.ident)?
} else {
finish_all_of_struct(schema, &stru, all_of_schemas)?
};
output.extend(updater);
Ok(output)
}
/// If we have flattened fields the struct schema is not the "final" schema, but part of an AllOf
/// schema containing it and all the flattened field schemas.
fn finish_all_of_struct(
mut schema: Schema,
stru: &syn::ItemStruct,
all_of_schemas: TokenStream,
) -> Result<TokenStream, Error> {
let name = &stru.ident;
// take out the inner object schema's description
let description = match schema.description.take().ok() {
Some(description) => description,
None => {
error!(schema.span, "missing description on api type struct");
syn::LitStr::new("<missing description>", schema.span)
}
};
// and replace it with a "dummy"
schema.description = Maybe::Derived(syn::LitStr::new(
&format!("<INNER: {}>", description.value()),
description.span(),
));
// now check if it even has any fields
let has_non_flattened_fields = match &schema.item {
api::SchemaItem::Object(obj) => obj.has_non_flattened_fields(),
_ => panic!("object schema is not an object schema?"),
};
let (inner_schema, inner_schema_ref) = if has_non_flattened_fields {
// if it does, we need to create an "inner" schema to merge into the AllOf schema
let obj_schema = {
let mut ts = TokenStream::new();
schema.to_schema(&mut ts)?;
ts
};
(
quote_spanned!(name.span() =>
const INNER_API_SCHEMA: ::proxmox_schema::Schema = #obj_schema;
),
quote_spanned!(name.span() => &Self::INNER_API_SCHEMA,),
)
} else {
// otherwise it stays empty
(TokenStream::new(), TokenStream::new())
};
Ok(quote_spanned!(name.span() =>
#stru
impl #name {
#inner_schema
}
impl ::proxmox_schema::ApiType for #name {
const API_SCHEMA: ::proxmox_schema::Schema =
::proxmox_schema::AllOfSchema::new(
#description,
&[
#inner_schema_ref
#all_of_schemas
],
)
.schema();
}
))
}
/// Field handling:
///
/// For each field we derive the description from doc-attributes if available.
fn handle_regular_field(
field_def: &mut ObjectEntry,
field: &syn::Field,
derived: bool, // whether this field was missing in the schema
attrs: &serde::FieldAttrib,
) -> Result<(), Error> {
let schema: &mut Schema = &mut field_def.schema;
if schema.description.is_none() {
let (doc_comment, doc_span) = util::get_doc_comments(&field.attrs)?;
util::derive_descriptions(schema, None, &doc_comment, doc_span)?;
}
util::infer_type(schema, &field.ty)?;
if util::is_option_type(&field.ty).is_some() {
if derived {
field_def.optional = true.into();
} else if !field_def.optional.expect_bool() {
error!(&field.ty => "non-optional Option type?");
}
} else {
attrs.check_non_option_type();
}
Ok(())
}
/// To derive an `Updater` we make all fields optional and use the `Updater` derive macro with
/// a `target` parameter.
fn derive_updater(
mut stru: syn::ItemStruct,
mut schema: Schema,
original_struct: &mut syn::ItemStruct,
container_attrs: &serde::ContainerAttrib,
) -> Result<TokenStream, Error> {
let original_name = &original_struct.ident;
stru.ident = Ident::new(&format!("{}Updater", stru.ident), stru.ident.span());
if !util::derives_trait(&original_struct.attrs, "Default") {
stru.attrs.push(util::make_derive_attribute(
Span::call_site(),
quote::quote! { Default },
));
}
let updater_name = &stru.ident;
let mut all_of_schemas = TokenStream::new();
let mut is_empty_impl = TokenStream::new();
if let syn::Fields::Named(fields) = &mut stru.fields {
for mut field in std::mem::take(&mut fields.named) {
match handle_updater_field(
&mut field,
&mut schema,
&mut all_of_schemas,
&mut is_empty_impl,
container_attrs,
) {
Ok(FieldAction::Keep) => fields.named.push(field),
Ok(FieldAction::Skip) => (),
Err(err) => {
crate::add_error(err);
fields.named.push(field);
}
}
}
}
let mut output = if all_of_schemas.is_empty() {
finish_schema(schema, &stru, &stru.ident)?
} else {
finish_all_of_struct(schema, &stru, all_of_schemas)?
};
if !is_empty_impl.is_empty() {
output.extend(quote::quote!(
impl ::proxmox_schema::Updater for #updater_name {
fn is_empty(&self) -> bool {
#is_empty_impl
}
}
));
}
output.extend(quote::quote!(
impl ::proxmox_schema::UpdaterType for #original_name {
type Updater = #updater_name;
}
));
Ok(output)
}
enum FieldAction {
Keep,
Skip,
}
fn handle_updater_field(
field: &mut syn::Field,
schema: &mut Schema,
all_of_schemas: &mut TokenStream,
is_empty_impl: &mut TokenStream,
container_attrs: &serde::ContainerAttrib,
) -> Result<FieldAction, syn::Error> {
let updater_attrs = UpdaterFieldAttributes::from_attributes(&mut field.attrs);
let serde_attrs = serde::FieldAttrib::try_from(&field.attrs[..])?;
let field_name = field.ident.as_ref().expect("unnamed field in FieldsNamed");
let field_name_string = field_name.to_string();
let (name, name_span) = {
let ident: &Ident = field
.ident
.as_ref()
.ok_or_else(|| format_err!(&field => "field without name?"))?;
if let Some(renamed) = serde_attrs.rename.clone() {
(renamed.value(), ident.span())
} else if let Some(rename_all) = container_attrs.rename_all {
let name = rename_all.apply_to_field(&ident.to_string());
(name, ident.span())
} else {
(ident.to_string(), ident.span())
}
};
if updater_attrs.skip() {
if !schema.remove_obj_property_by_ident(&name)
&& !schema.remove_obj_property_by_ident(&field_name_string)
{
bail!(name_span, "failed to find schema entry for {:?}", name);
}
return Ok(FieldAction::Skip);
}
let field_schema = match schema.find_obj_property_by_ident_mut(&name) {
Some(obj) => obj,
None => match schema.find_obj_property_by_ident_mut(&field_name_string) {
Some(obj) => obj,
None => {
bail!(
field_name.span(),
"failed to find schema entry for {:?}",
field_name_string,
);
}
},
};
let span = Span::call_site();
field_schema.optional = field.ty.clone().into();
let updater = match updater_attrs.ty() {
Some(ty) => ty.clone(),
None => {
syn::TypePath {
qself: Some(syn::QSelf {
lt_token: syn::token::Lt { spans: [span] },
ty: Box::new(field.ty.clone()),
position: 2, // 'Updater' is item index 2 in the 'segments' below
as_token: Some(syn::token::As { span }),
gt_token: syn::token::Gt { spans: [span] },
}),
path: util::make_path(span, true, &["proxmox_schema", "UpdaterType", "Updater"]),
}
}
};
updater_attrs.replace_serde_attributes(&mut field.attrs);
// we also need to update the schema to point to the updater's schema for `type: Foo` entries
if let SchemaItem::ExternType(path) = &mut field_schema.schema.item {
*path = syn::ExprPath {
attrs: Vec::new(),
qself: updater.qself.clone(),
path: updater.path.clone(),
};
}
field.ty = syn::Type::Path(updater);
if field_schema.flatten_in_struct {
let updater_ty = &field.ty;
all_of_schemas
.extend(quote::quote! {&<#updater_ty as ::proxmox_schema::ApiType>::API_SCHEMA,});
}
if !is_empty_impl.is_empty() {
is_empty_impl.extend(quote::quote! { && });
}
is_empty_impl.extend(quote::quote! {
self.#field_name.is_empty()
});
Ok(FieldAction::Keep)
}