change updater derivation

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2021-08-10 11:41:41 +02:00
parent 017b81712e
commit 34020ea3d6
8 changed files with 310 additions and 514 deletions

View 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()
//}
}

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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)
}
*/

View File

@ -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,
}

View File

@ -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