change updater derivation
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
017b81712e
commit
34020ea3d6
52
proxmox-api-macro/src/api/attributes.rs
Normal file
52
proxmox-api-macro/src/api/attributes.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use syn::{Meta, NestedMeta};
|
||||
|
||||
use crate::util::{self, default_false, set_bool};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UpdaterFieldAttributes {
|
||||
/// Skip this field in the updater.
|
||||
skip: Option<syn::LitBool>,
|
||||
// /// Change the type for the updater.
|
||||
// ty: Option<syn::Type>,
|
||||
}
|
||||
|
||||
impl UpdaterFieldAttributes {
|
||||
pub fn from_attributes(input: &mut Vec<syn::Attribute>) -> Self {
|
||||
let mut this = Self::default();
|
||||
|
||||
util::extract_attributes(input, "updater", |meta| this.parse(meta));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn parse(&mut self, input: NestedMeta) -> Result<(), syn::Error> {
|
||||
match input {
|
||||
NestedMeta::Lit(lit) => bail!(lit => "unexpected literal"),
|
||||
NestedMeta::Meta(meta) => self.parse_meta(meta),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_meta(&mut self, meta: Meta) -> Result<(), syn::Error> {
|
||||
match meta {
|
||||
Meta::Path(ref path) if path.is_ident("skip") => {
|
||||
set_bool(&mut self.skip, path, true);
|
||||
}
|
||||
// Meta::NameValue(ref nv) if nv.path.is_ident("type") => {
|
||||
// parse_str_value_to_option(&mut self.ty, nv)
|
||||
// }
|
||||
Meta::NameValue(m) => bail!(&m => "invalid updater attribute: {:?}", m.path),
|
||||
Meta::List(m) => bail!(&m => "invalid updater attribute: {:?}", m.path),
|
||||
Meta::Path(m) => bail!(&m => "invalid updater attribute: {:?}", m),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn skip(&self) -> bool {
|
||||
default_false(self.skip.as_ref())
|
||||
}
|
||||
|
||||
//pub fn ty(&self) -> Option<&syn::Type> {
|
||||
// self.ty.as_ref()
|
||||
//}
|
||||
}
|
@ -82,5 +82,11 @@ pub fn handle_enum(
|
||||
.format(&::proxmox::api::schema::ApiStringFormat::Enum(&[#variants]))
|
||||
.schema();
|
||||
}
|
||||
|
||||
#[automatically_derived]
|
||||
impl ::proxmox::api::schema::Updatable for #name {
|
||||
type Updater = Option<Self>;
|
||||
const UPDATER_IS_OPTION: bool = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ use syn::{Expr, ExprPath, Ident};
|
||||
|
||||
use crate::util::{FieldName, JSONObject, JSONValue, Maybe};
|
||||
|
||||
mod attributes;
|
||||
mod enums;
|
||||
mod method;
|
||||
mod structs;
|
||||
@ -198,6 +199,12 @@ impl Schema {
|
||||
.and_then(|obj| obj.find_property_by_ident_mut(key))
|
||||
}
|
||||
|
||||
fn remove_obj_property_by_ident(&mut self, key: &str) -> bool {
|
||||
self.as_object_mut()
|
||||
.expect("tried to remove object property on non-object schema")
|
||||
.remove_property_by_ident(key)
|
||||
}
|
||||
|
||||
// FIXME: Should we turn the property list into a map? We used to have no need to find keys in
|
||||
// it, but we do now...
|
||||
fn find_schema_property(&self, key: &str) -> Option<&syn::Expr> {
|
||||
@ -644,6 +651,20 @@ impl SchemaObject {
|
||||
.find(|p| p.name.as_ident_str() == key)
|
||||
}
|
||||
|
||||
fn remove_property_by_ident(&mut self, key: &str) -> bool {
|
||||
match self
|
||||
.properties_
|
||||
.iter()
|
||||
.position(|p| p.name.as_ident_str() == key)
|
||||
{
|
||||
Some(idx) => {
|
||||
self.properties_.remove(idx);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_properties(&mut self, new_fields: Vec<ObjectEntry>) {
|
||||
self.properties_.extend(new_fields);
|
||||
self.sort_properties();
|
||||
|
@ -18,6 +18,7 @@ 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;
|
||||
@ -58,7 +59,16 @@ fn handle_unit_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<Toke
|
||||
|
||||
get_struct_description(&mut schema, &stru)?;
|
||||
|
||||
finish_schema(schema, &stru, &stru.ident)
|
||||
let name = &stru.ident;
|
||||
let mut schema = finish_schema(schema, &stru, name)?;
|
||||
schema.extend(quote_spanned! { name.span() =>
|
||||
impl ::proxmox::api::schema::Updatable for #name {
|
||||
type Updater = Option<Self>;
|
||||
const UPDATER_IS_OPTION: bool = true;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(schema)
|
||||
}
|
||||
|
||||
fn finish_schema(
|
||||
@ -261,7 +271,16 @@ fn handle_regular_struct(
|
||||
}
|
||||
});
|
||||
if derive {
|
||||
derive_updater(stru.clone(), schema.clone(), &mut stru)?
|
||||
let updater = derive_updater(stru.clone(), schema.clone(), &mut stru)?;
|
||||
|
||||
// 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()
|
||||
}
|
||||
@ -381,7 +400,7 @@ fn derive_updater(
|
||||
stru.ident = Ident::new(&format!("{}Updater", stru.ident), stru.ident.span());
|
||||
|
||||
if !util::derived_items(&original_struct.attrs).any(|p| p.is_ident("Default")) {
|
||||
original_struct.attrs.push(util::make_derive_attribute(
|
||||
stru.attrs.push(util::make_derive_attribute(
|
||||
Span::call_site(),
|
||||
quote::quote! { Default },
|
||||
));
|
||||
@ -404,51 +423,20 @@ fn derive_updater(
|
||||
let mut is_empty_impl = TokenStream::new();
|
||||
|
||||
if let syn::Fields::Named(fields) = &mut stru.fields {
|
||||
for field in &mut fields.named {
|
||||
let field_name = field.ident.as_ref().expect("unnamed field in FieldsNamed");
|
||||
let field_name_string = field_name.to_string();
|
||||
|
||||
let field_schema = match schema.find_obj_property_by_ident_mut(&field_name_string) {
|
||||
Some(obj) => obj,
|
||||
None => {
|
||||
error!(
|
||||
field_name.span(),
|
||||
"failed to find schema entry for {:?}", field_name_string,
|
||||
);
|
||||
continue;
|
||||
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,
|
||||
) {
|
||||
Ok(FieldAction::Keep) => fields.named.push(field),
|
||||
Ok(FieldAction::Skip) => (),
|
||||
Err(err) => {
|
||||
crate::add_error(err);
|
||||
fields.named.push(field);
|
||||
}
|
||||
};
|
||||
|
||||
field_schema.optional = field.ty.clone().into();
|
||||
|
||||
let span = Span::call_site();
|
||||
let updater = syn::TypePath {
|
||||
qself: Some(syn::QSelf {
|
||||
lt_token: syn::token::Lt { spans: [span] },
|
||||
ty: Box::new(field.ty.clone()),
|
||||
position: 4, // 'Updater' is the 4th item 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", "api", "schema", "Updatable", "Updater"],
|
||||
),
|
||||
};
|
||||
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::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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,3 +459,75 @@ fn derive_updater(
|
||||
|
||||
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,
|
||||
) -> Result<FieldAction, syn::Error> {
|
||||
let updater_attrs = UpdaterFieldAttributes::from_attributes(&mut field.attrs);
|
||||
|
||||
let field_name = field.ident.as_ref().expect("unnamed field in FieldsNamed");
|
||||
let field_name_string = field_name.to_string();
|
||||
|
||||
if updater_attrs.skip() {
|
||||
if !schema.remove_obj_property_by_ident(&field_name_string) {
|
||||
bail!(
|
||||
field_name.span(),
|
||||
"failed to find schema entry for {:?}",
|
||||
field_name_string,
|
||||
);
|
||||
}
|
||||
return Ok(FieldAction::Skip);
|
||||
}
|
||||
|
||||
let field_schema = 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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
field_schema.optional = field.ty.clone().into();
|
||||
|
||||
let span = Span::call_site();
|
||||
let updater = syn::TypePath {
|
||||
qself: Some(syn::QSelf {
|
||||
lt_token: syn::token::Lt { spans: [span] },
|
||||
ty: Box::new(field.ty.clone()),
|
||||
position: 4, // 'Updater' is the 4th item 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", "api", "schema", "Updatable", "Updater"],
|
||||
),
|
||||
};
|
||||
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::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)
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use crate::serde;
|
||||
use crate::util;
|
||||
|
||||
pub(crate) fn updatable(item: TokenStream) -> Result<TokenStream, syn::Error> {
|
||||
let item: syn::Item = syn::parse2(item)?;
|
||||
let full_span = item.span();
|
||||
@ -64,22 +59,6 @@ fn default_updatable(full_span: Span, ident: Ident) -> TokenStream {
|
||||
impl ::proxmox::api::schema::Updatable for #ident {
|
||||
type Updater = Option<#ident>;
|
||||
const UPDATER_IS_OPTION: bool = true;
|
||||
|
||||
fn update_from<T: AsRef<str>>(
|
||||
&mut self,
|
||||
from: Option<#ident>,
|
||||
_delete: &[T],
|
||||
) -> Result<(), ::anyhow::Error> {
|
||||
if let Some(val) = from {
|
||||
*self = val;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_build_from(from: Option<#ident>) -> Result<Self, ::anyhow::Error> {
|
||||
from.ok_or_else(|| ::anyhow::format_err!("cannot build from None value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,115 +68,21 @@ fn derive_named_struct_updatable(
|
||||
full_span: Span,
|
||||
ident: Ident,
|
||||
generics: syn::Generics,
|
||||
fields: syn::FieldsNamed,
|
||||
_fields: syn::FieldsNamed,
|
||||
) -> Result<TokenStream, syn::Error> {
|
||||
no_generics(generics);
|
||||
|
||||
let serde_container_attrs = serde::ContainerAttrib::try_from(&attrs[..])?;
|
||||
let args = UpdatableArgs::from_attributes(attrs);
|
||||
let updater = match args.updater {
|
||||
Some(updater) => updater,
|
||||
None => return Ok(default_updatable(full_span, ident)),
|
||||
};
|
||||
|
||||
let mut delete = TokenStream::new();
|
||||
let mut apply = TokenStream::new();
|
||||
let mut build = TokenStream::new();
|
||||
|
||||
for field in fields.named {
|
||||
let serde_attrs = serde::SerdeAttrib::try_from(&field.attrs[..])?;
|
||||
let attrs = UpdaterFieldArgs::from_attributes(field.attrs);
|
||||
|
||||
let field_ident = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("unnamed field in named struct?");
|
||||
|
||||
let field_name_string = if let Some(renamed) = serde_attrs.rename {
|
||||
renamed.into_str()
|
||||
} else if let Some(rename_all) = serde_container_attrs.rename_all {
|
||||
let name = rename_all.apply_to_field(&field_ident.to_string());
|
||||
name
|
||||
} else {
|
||||
field_ident.to_string()
|
||||
};
|
||||
|
||||
let build_err = format!(
|
||||
"failed to build value for field '{}': {{}}",
|
||||
field_name_string
|
||||
);
|
||||
if util::is_option_type(&field.ty).is_some() {
|
||||
delete.extend(quote! {
|
||||
#field_name_string => { self.#field_ident = None; }
|
||||
});
|
||||
build.extend(quote! {
|
||||
#field_ident: ::proxmox::api::schema::Updatable::try_build_from(
|
||||
from.#field_ident
|
||||
)
|
||||
.map_err(|err| ::anyhow::format_err!(#build_err, err))?,
|
||||
});
|
||||
} else {
|
||||
build.extend(quote! {
|
||||
#field_ident: ::proxmox::api::schema::Updatable::try_build_from(
|
||||
from.#field_ident
|
||||
)
|
||||
.map_err(|err| ::anyhow::format_err!(#build_err, err))?,
|
||||
});
|
||||
}
|
||||
|
||||
if attrs.fixed {
|
||||
let error = format!(
|
||||
"field '{}' must not be set when updating existing data",
|
||||
field_ident
|
||||
);
|
||||
apply.extend(quote! {
|
||||
if from.#field_ident.is_some() {
|
||||
::anyhow::bail!(#error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
apply.extend(quote! {
|
||||
::proxmox::api::schema::Updatable::update_from(
|
||||
&mut self.#field_ident,
|
||||
from.#field_ident,
|
||||
delete,
|
||||
)?;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !delete.is_empty() {
|
||||
delete = quote! {
|
||||
for delete in delete {
|
||||
match delete.as_ref() {
|
||||
#delete
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl ::proxmox::api::schema::Updatable for #ident {
|
||||
type Updater = #updater;
|
||||
const UPDATER_IS_OPTION: bool = false;
|
||||
|
||||
fn update_from<T: AsRef<str>>(
|
||||
&mut self,
|
||||
from: Self::Updater,
|
||||
delete: &[T],
|
||||
) -> Result<(), ::anyhow::Error> {
|
||||
#delete
|
||||
#apply
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_build_from(from: Self::Updater) -> Result<Self, ::anyhow::Error> {
|
||||
Ok(Self {
|
||||
#build
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -244,31 +129,6 @@ impl UpdatableArgs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct UpdaterFieldArgs {
|
||||
/// A fixed field must not be set in the `Updater` when the data is updated via `update_from`,
|
||||
/// but is still required for the `build()` method.
|
||||
fixed: bool,
|
||||
}
|
||||
|
||||
impl UpdaterFieldArgs {
|
||||
fn from_attributes(attributes: Vec<syn::Attribute>) -> Self {
|
||||
let mut this = Self::default();
|
||||
for_attributes(attributes, "updater", |meta| this.parse_nested(meta));
|
||||
this
|
||||
}
|
||||
|
||||
fn parse_nested(&mut self, meta: syn::NestedMeta) -> Result<(), syn::Error> {
|
||||
match meta {
|
||||
syn::NestedMeta::Meta(syn::Meta::Path(path)) if path.is_ident("fixed") => {
|
||||
self.fixed = true;
|
||||
}
|
||||
other => bail!(other => "invalid updater argument"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-fatally go through all `updater` attributes.
|
||||
fn for_attributes<F>(attributes: Vec<syn::Attribute>, attr_name: &str, mut func: F)
|
||||
where
|
||||
|
@ -376,7 +376,7 @@ impl IntoIterator for JSONObject {
|
||||
/// An element in a json style map.
|
||||
struct JSONMapEntry {
|
||||
pub key: FieldName,
|
||||
pub colon_token: Token![:],
|
||||
_colon_token: Token![:],
|
||||
pub value: JSONValue,
|
||||
}
|
||||
|
||||
@ -384,7 +384,7 @@ impl Parse for JSONMapEntry {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
Ok(Self {
|
||||
key: input.parse()?,
|
||||
colon_token: input.parse()?,
|
||||
_colon_token: input.parse()?,
|
||||
value: input.parse()?,
|
||||
})
|
||||
}
|
||||
@ -816,3 +816,116 @@ pub fn make_derive_attribute(span: Span, content: TokenStream) -> syn::Attribute
|
||||
quote::quote! { (#content) },
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract (remove) an attribute from a list an run a callback on its parameters.
|
||||
pub fn extract_attributes(
|
||||
attributes: &mut Vec<syn::Attribute>,
|
||||
attr_name: &str,
|
||||
mut func_matching: impl FnMut(syn::NestedMeta) -> Result<(), syn::Error>,
|
||||
) {
|
||||
for attr in std::mem::take(attributes) {
|
||||
if attr.style != syn::AttrStyle::Outer {
|
||||
attributes.push(attr);
|
||||
continue;
|
||||
}
|
||||
|
||||
let meta = match attr.parse_meta() {
|
||||
Ok(meta) => meta,
|
||||
Err(err) => {
|
||||
crate::add_error(err);
|
||||
attributes.push(attr);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let list = match meta {
|
||||
syn::Meta::List(list) if list.path.is_ident(attr_name) => list,
|
||||
_ => {
|
||||
attributes.push(attr);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in list.nested {
|
||||
match func_matching(entry) {
|
||||
Ok(()) => (),
|
||||
Err(err) => crate::add_error(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create an error about some duplicate attribute.
|
||||
pub fn duplicate<T>(prev: &Option<T>, attr: &syn::Path) {
|
||||
if prev.is_some() {
|
||||
error!(attr => "duplicate attribute: '{:?}'", attr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a boolean attribute to a value, producing a "duplication" error if it has already been set.
|
||||
pub fn set_bool(b: &mut Option<syn::LitBool>, attr: &syn::Path, value: bool) {
|
||||
duplicate(&*b, attr);
|
||||
*b = Some(syn::LitBool::new(value, attr.span()));
|
||||
}
|
||||
|
||||
pub fn default_false(o: Option<&syn::LitBool>) -> bool {
|
||||
o.as_ref().map(|b| b.value).unwrap_or(false)
|
||||
}
|
||||
|
||||
/*
|
||||
/// Parse the contents of a `LitStr`, preserving its span.
|
||||
pub fn parse_lit_str<T: Parse>(s: &syn::LitStr) -> syn::parse::Result<T> {
|
||||
parse_str(&s.value(), s.span())
|
||||
}
|
||||
|
||||
/// Parse a literal string, giving the entire output the specified span.
|
||||
pub fn parse_str<T: Parse>(s: &str, span: Span) -> syn::parse::Result<T> {
|
||||
syn::parse2(respan_tokens(syn::parse_str(s)?, span))
|
||||
}
|
||||
|
||||
/// Apply a `Span` to an entire `TokenStream`.
|
||||
pub fn respan_tokens(stream: TokenStream, span: Span) -> TokenStream {
|
||||
stream
|
||||
.into_iter()
|
||||
.map(|token| respan(token, span))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Apply a `Span` to a `TokenTree`, recursively if it is a `Group`.
|
||||
pub fn respan(mut token: TokenTree, span: Span) -> TokenTree {
|
||||
use proc_macro2::Group;
|
||||
|
||||
match &mut token {
|
||||
TokenTree::Group(g) => {
|
||||
*g = Group::new(g.delimiter(), respan_tokens(g.stream(), span));
|
||||
}
|
||||
other => other.set_span(span),
|
||||
}
|
||||
|
||||
token
|
||||
}
|
||||
|
||||
/// Parse a string attribute into a value, producing a duplication error if it has already been
|
||||
/// set.
|
||||
pub fn parse_str_value_to_option<T: Parse>(target: &mut Option<T>, nv: &syn::MetaNameValue) {
|
||||
duplicate(&*target, &nv.path);
|
||||
match &nv.lit {
|
||||
syn::Lit::Str(s) => match parse_lit_str(s) {
|
||||
Ok(value) => *target = Some(value),
|
||||
Err(err) => crate::add_error(err),
|
||||
},
|
||||
other => error!(other => "bad value for '{:?}' attribute", nv.path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_str_value<T: Parse>(nv: &syn::MetaNameValue) -> Result<T, syn::Error> {
|
||||
match &nv.lit {
|
||||
syn::Lit::Str(s) => super::parse_lit_str(s),
|
||||
other => bail!(other => "bad value for '{:?}' attribute", nv.path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_true(o: Option<&syn::LitBool>) -> bool {
|
||||
o.as_ref().map(|b| b.value).unwrap_or(true)
|
||||
}
|
||||
*/
|
||||
|
@ -1,14 +1,9 @@
|
||||
#[cfg(not(feature = "noserde"))]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use proxmox::api::api;
|
||||
use proxmox::api::schema::Updater;
|
||||
|
||||
#[api]
|
||||
/// An example of a simple struct type.
|
||||
#[cfg_attr(not(feature = "noserde"), derive(Deserialize, Serialize))]
|
||||
#[derive(Debug, PartialEq, Updater)]
|
||||
#[derive(Updater)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Simple {
|
||||
/// A test string.
|
||||
@ -25,8 +20,7 @@ pub struct Simple {
|
||||
},
|
||||
)]
|
||||
/// A second struct so we can test flattening.
|
||||
#[cfg_attr(not(feature = "noserde"), derive(Deserialize, Serialize))]
|
||||
#[derive(Debug, PartialEq, Updater)]
|
||||
#[derive(Updater)]
|
||||
pub struct Complex {
|
||||
/// An extra field not part of the flattened struct.
|
||||
extra: String,
|
||||
@ -44,288 +38,16 @@ pub struct Complex {
|
||||
},
|
||||
)]
|
||||
/// One of the baaaad cases.
|
||||
#[cfg_attr(not(feature = "noserde"), derive(Deserialize, Serialize))]
|
||||
#[derive(Debug, PartialEq, Updater)]
|
||||
#[derive(Updater)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct SuperComplex {
|
||||
/// An extra field not part of the flattened struct.
|
||||
extra: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Updater::is_empty")]
|
||||
simple: Option<Simple>,
|
||||
}
|
||||
|
||||
#[api(
|
||||
properties: {
|
||||
complex: { type: Complex },
|
||||
},
|
||||
)]
|
||||
/// Something with "fixed" values we cannot update but require for creation.
|
||||
#[cfg_attr(not(feature = "noserde"), derive(Deserialize, Serialize))]
|
||||
#[derive(Debug, PartialEq, Updater)]
|
||||
pub struct Creatable {
|
||||
/// An ID which cannot be changed later.
|
||||
#[updater(fixed)]
|
||||
id: String,
|
||||
|
||||
/// Some parameter we're allowed to change with an updater.
|
||||
name: String,
|
||||
|
||||
/// Optional additional information.
|
||||
#[serde(skip_serializing_if = "Updater::is_empty", default)]
|
||||
info: Option<String>,
|
||||
|
||||
/// Optional additional information 2.
|
||||
#[serde(skip_serializing_if = "Updater::is_empty", default)]
|
||||
info2: Option<String>,
|
||||
|
||||
/// Super complex additional data
|
||||
#[serde(flatten)]
|
||||
complex: Complex,
|
||||
}
|
||||
|
||||
struct RpcEnv;
|
||||
impl proxmox::api::RpcEnvironment for RpcEnv {
|
||||
fn result_attrib_mut(&mut self) -> &mut Value {
|
||||
panic!("result_attrib_mut called");
|
||||
}
|
||||
|
||||
fn result_attrib(&self) -> &Value {
|
||||
panic!("result_attrib called");
|
||||
}
|
||||
|
||||
/// The environment type
|
||||
fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
|
||||
panic!("env_type called");
|
||||
}
|
||||
|
||||
/// Set authentication id
|
||||
fn set_auth_id(&mut self, user: Option<String>) {
|
||||
let _ = user;
|
||||
panic!("set_auth_id called");
|
||||
}
|
||||
|
||||
/// Get authentication id
|
||||
fn get_auth_id(&self) -> Option<String> {
|
||||
panic!("get_auth_id called");
|
||||
}
|
||||
}
|
||||
|
||||
mod test_creatable {
|
||||
use anyhow::{bail, Error};
|
||||
use serde_json::json;
|
||||
|
||||
use proxmox::api::schema::Updatable;
|
||||
use proxmox_api_macro::api;
|
||||
|
||||
use super::*;
|
||||
|
||||
static mut TEST_OBJECT: Option<Creatable> = None;
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
thing: { flatten: true, type: CreatableUpdater },
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Test method to create an object.
|
||||
///
|
||||
/// Returns: the object's ID.
|
||||
pub fn create_thing(thing: CreatableUpdater) -> Result<String, Error> {
|
||||
if unsafe { TEST_OBJECT.is_some() } {
|
||||
bail!("object exists");
|
||||
}
|
||||
|
||||
let obj = Creatable::try_build_from(thing)?;
|
||||
let id = obj.id.clone();
|
||||
|
||||
unsafe {
|
||||
TEST_OBJECT = Some(obj);
|
||||
}
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
thing: { flatten: true, type: CreatableUpdater },
|
||||
delete: {
|
||||
optional: true,
|
||||
description: "list of properties to delete",
|
||||
type: Array,
|
||||
items: {
|
||||
description: "field name to delete",
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Test method to update an object.
|
||||
pub fn update_thing(thing: CreatableUpdater, delete: Option<Vec<String>>) -> Result<(), Error> {
|
||||
let delete = delete.unwrap_or_default();
|
||||
match unsafe { &mut TEST_OBJECT } {
|
||||
Some(obj) => obj.update_from(thing, &delete),
|
||||
None => bail!("object has not been created yet"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let _ = api_function_create_thing(
|
||||
json!({ "name": "The Name" }),
|
||||
&API_METHOD_CREATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect_err("create_thing should fail without an ID");
|
||||
|
||||
let _ = api_function_create_thing(
|
||||
json!({ "id": "Id1" }),
|
||||
&API_METHOD_CREATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect_err("create_thing should fail without a name");
|
||||
|
||||
let value = api_function_create_thing(
|
||||
json!({
|
||||
"id": "Id1",
|
||||
"name": "The Name",
|
||||
"extra": "Extra Info",
|
||||
"one-field": "Part of Simple",
|
||||
"info2": "More Info 2",
|
||||
}),
|
||||
&API_METHOD_CREATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect("create_thing should work");
|
||||
assert_eq!(value, "Id1");
|
||||
assert_eq!(
|
||||
unsafe { &TEST_OBJECT },
|
||||
&Some(Creatable {
|
||||
id: "Id1".to_string(),
|
||||
name: "The Name".to_string(),
|
||||
info: None,
|
||||
info2: Some("More Info 2".to_string()),
|
||||
complex: Complex {
|
||||
extra: "Extra Info".to_string(),
|
||||
simple: Simple {
|
||||
one_field: "Part of Simple".to_string(),
|
||||
opt: None,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let _ = api_function_update_thing(
|
||||
json!({
|
||||
"id": "Poop",
|
||||
}),
|
||||
&API_METHOD_UPDATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect_err("shouldn't be allowed to update the ID");
|
||||
|
||||
let _ = api_function_update_thing(
|
||||
json!({
|
||||
"info": "Updated Info",
|
||||
"delete": ["info2"],
|
||||
}),
|
||||
&API_METHOD_UPDATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect("should be allowed to update the optional field");
|
||||
|
||||
assert_eq!(
|
||||
unsafe { &TEST_OBJECT },
|
||||
&Some(Creatable {
|
||||
id: "Id1".to_string(),
|
||||
name: "The Name".to_string(),
|
||||
info: Some("Updated Info".to_string()),
|
||||
info2: None,
|
||||
complex: Complex {
|
||||
extra: "Extra Info".to_string(),
|
||||
simple: Simple {
|
||||
one_field: "Part of Simple".to_string(),
|
||||
opt: None,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let _ = api_function_update_thing(
|
||||
json!({
|
||||
"extra": "Partial flatten update",
|
||||
}),
|
||||
&API_METHOD_UPDATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect("should be allowed to update the parts of a flattened field");
|
||||
assert_eq!(
|
||||
unsafe { &TEST_OBJECT },
|
||||
&Some(Creatable {
|
||||
id: "Id1".to_string(),
|
||||
name: "The Name".to_string(),
|
||||
info: Some("Updated Info".to_string()),
|
||||
info2: None,
|
||||
complex: Complex {
|
||||
extra: "Partial flatten update".to_string(),
|
||||
simple: Simple {
|
||||
one_field: "Part of Simple".to_string(),
|
||||
opt: None,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let _ = api_function_update_thing(
|
||||
json!({
|
||||
"opt": "Deeply nested optional update.",
|
||||
}),
|
||||
&API_METHOD_UPDATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect("should be allowed to update the parts of a deeply nested struct");
|
||||
assert_eq!(
|
||||
unsafe { &TEST_OBJECT },
|
||||
&Some(Creatable {
|
||||
id: "Id1".to_string(),
|
||||
name: "The Name".to_string(),
|
||||
info: Some("Updated Info".to_string()),
|
||||
info2: None,
|
||||
complex: Complex {
|
||||
extra: "Partial flatten update".to_string(),
|
||||
simple: Simple {
|
||||
one_field: "Part of Simple".to_string(),
|
||||
opt: Some("Deeply nested optional update.".to_string()),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let _ = api_function_update_thing(
|
||||
json!({
|
||||
"delete": ["opt"],
|
||||
}),
|
||||
&API_METHOD_UPDATE_THING,
|
||||
&mut RpcEnv,
|
||||
)
|
||||
.expect("should be allowed to remove parts of a deeply nested struct");
|
||||
assert_eq!(
|
||||
unsafe { &TEST_OBJECT },
|
||||
&Some(Creatable {
|
||||
id: "Id1".to_string(),
|
||||
name: "The Name".to_string(),
|
||||
info: Some("Updated Info".to_string()),
|
||||
info2: None,
|
||||
complex: Complex {
|
||||
extra: "Partial flatten update".to_string(),
|
||||
simple: Simple {
|
||||
one_field: "Part of Simple".to_string(),
|
||||
opt: None,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// A field which should not appear in the updater.
|
||||
#[updater(skip)]
|
||||
not_in_updater: String,
|
||||
}
|
||||
|
@ -1531,11 +1531,6 @@ pub trait Updatable: Sized {
|
||||
/// This should always be true for the "default" updaters which are just `Option<T>` types.
|
||||
/// Types which are not wrapped in `Option` must set this to `false`.
|
||||
const UPDATER_IS_OPTION: bool;
|
||||
|
||||
fn update_from<T>(&mut self, from: Self::Updater, delete: &[T]) -> Result<(), Error>
|
||||
where
|
||||
T: AsRef<str>;
|
||||
fn try_build_from(from: Self::Updater) -> Result<Self, Error>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "api-macro")]
|
||||
@ -1549,23 +1544,8 @@ macro_rules! basic_updatable {
|
||||
($($ty:ty)*) => {
|
||||
$(
|
||||
impl Updatable for $ty {
|
||||
type Updater = Option<$ty>;
|
||||
type Updater = Option<Self>;
|
||||
const UPDATER_IS_OPTION: bool = true;
|
||||
|
||||
fn update_from<T: AsRef<str>>(
|
||||
&mut self,
|
||||
from: Option<$ty>,
|
||||
_delete: &[T],
|
||||
) -> Result<(), Error> {
|
||||
if let Some(val) = from {
|
||||
*self = val;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_build_from(from: Option<$ty>) -> Result<Self, Error> {
|
||||
from.ok_or_else(|| format_err!("cannot build from None value"))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
@ -1578,24 +1558,6 @@ where
|
||||
{
|
||||
type Updater = T::Updater;
|
||||
const UPDATER_IS_OPTION: bool = true;
|
||||
|
||||
fn update_from<S: AsRef<str>>(&mut self, from: T::Updater, delete: &[S]) -> Result<(), Error> {
|
||||
match self {
|
||||
Some(val) => val.update_from(from, delete),
|
||||
None => {
|
||||
*self = Self::try_build_from(from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_build_from(from: T::Updater) -> Result<Self, Error> {
|
||||
if from.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
T::try_build_from(from).map(Some)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper type for "Updater" structs. This trait is *not* implemented for an api "base" type
|
||||
|
Loading…
Reference in New Issue
Block a user