--- /dev/null
+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()
+ //}
+}
.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;
+ }
})
}
use crate::util::{FieldName, JSONObject, JSONValue, Maybe};
+mod attributes;
mod enums;
mod method;
mod structs;
.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> {
.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();
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;
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(
}
});
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()
}
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 },
));
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()
- });
}
}
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)
+}
-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();
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"))
- }
}
}
}
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
- })
- }
}
})
}
}
}
-#[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
/// An element in a json style map.
struct JSONMapEntry {
pub key: FieldName,
- pub colon_token: Token![:],
+ _colon_token: Token![:],
pub value: JSONValue,
}
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
key: input.parse()?,
- colon_token: input.parse()?,
+ _colon_token: input.parse()?,
value: input.parse()?,
})
}
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)
+}
+*/
-#[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.
},
)]
/// 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,
},
)]
/// 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,
}
/// 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")]
($($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"))
- }
}
)*
};
{
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