diff --git a/bomboni_request/src/error.rs b/bomboni_request/src/error.rs index 1c6d062..ea36097 100644 --- a/bomboni_request/src/error.rs +++ b/bomboni_request/src/error.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::query::error::QueryError; @@ -51,7 +52,8 @@ pub enum PathErrorStep { Key(String), } -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] pub enum CommonError { #[error("requested entity was not found")] ResourceNotFound, diff --git a/bomboni_request/src/parse/mod.rs b/bomboni_request/src/parse/mod.rs index 91cb75d..54132ff 100644 --- a/bomboni_request/src/parse/mod.rs +++ b/bomboni_request/src/parse/mod.rs @@ -91,6 +91,7 @@ mod tests { Int32Value, Int64Value, StringValue, Timestamp, UInt32Value, }; use bomboni_request_derive::{impl_parse_into_map, parse_resource_name, Parse}; + use serde::{Deserialize, Serialize}; use super::*; @@ -1924,4 +1925,25 @@ mod tests { } ); } + + #[test] + fn serde_as() { + #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] + struct Item { + value: i32, + } + + #[derive(Debug, Clone, PartialEq, Parse)] + #[parse(source = Item, write, serde_as)] + struct ParsedItem { + value: i32, + } + + let js = serde_json::to_string_pretty(&Item { value: 42 }).unwrap(); + assert_eq!( + js, + serde_json::to_string_pretty(&ParsedItem { value: 42 }).unwrap(), + ); + assert_eq!(serde_json::from_str::(&js).unwrap().value, 42); + } } diff --git a/bomboni_request_derive/src/parse/message/parse.rs b/bomboni_request_derive/src/parse/message/parse.rs index f47a0d4..123e93c 100644 --- a/bomboni_request_derive/src/parse/message/parse.rs +++ b/bomboni_request_derive/src/parse/message/parse.rs @@ -133,6 +133,7 @@ pub fn expand(options: &ParseOptions, fields: &[ParseField]) -> syn::Result( @@ -145,6 +146,7 @@ pub fn expand(options: &ParseOptions, fields: &[ParseField]) -> syn::Result( @@ -157,6 +159,7 @@ pub fn expand(options: &ParseOptions, fields: &[ParseField]) -> syn::Result for #ident #type_generics #where_clause { #[allow(clippy::ignored_unit_patterns)] fn parse(source: #source) -> RequestResult { diff --git a/bomboni_request_derive/src/parse/message/write.rs b/bomboni_request_derive/src/parse/message/write.rs index 1805c32..c468352 100644 --- a/bomboni_request_derive/src/parse/message/write.rs +++ b/bomboni_request_derive/src/parse/message/write.rs @@ -48,6 +48,7 @@ pub fn expand(options: &ParseOptions, fields: &[ParseField]) -> TokenStream { let (impl_generics, type_generics, where_clause) = options.generics.split_for_impl(); quote! { + #[automatically_derived] impl #impl_generics From<#ident #type_generics> for #source #where_clause { #[allow(clippy::needless_update)] fn from(value: #ident #type_generics) -> Self { diff --git a/bomboni_request_derive/src/parse/mod.rs b/bomboni_request_derive/src/parse/mod.rs index 1b22a75..9351361 100644 --- a/bomboni_request_derive/src/parse/mod.rs +++ b/bomboni_request_derive/src/parse/mod.rs @@ -10,6 +10,7 @@ mod message; mod oneof; pub mod parse_into_map; pub mod parse_resource_name; +mod serde; #[derive(Debug, FromDeriveInput)] #[darling(attributes(parse))] @@ -22,6 +23,18 @@ pub struct ParseOptions { /// Set to true to implement `From` trait for converting parsed type back into source proto type. #[darling(default)] pub write: bool, + /// Implement `serde::Serialize` from source type. + #[darling(default)] + pub serialize_as: bool, + /// Implement `serde::Deserialize` from source type. + #[darling(default)] + pub deserialize_as: bool, + /// Implement `serde::Serialize` and `serde::Deserialize` from source type. + #[darling(default)] + pub serde_as: bool, + /// Custom serde crate. + #[darling(default)] + pub serde_crate: Option, /// Used to create tagged unions. #[darling(default)] pub tagged_union: Option, @@ -208,10 +221,14 @@ pub fn expand(input: DeriveInput) -> syn::Result { } }; - match &options.data { - ast::Data::Struct(fields) => message::expand(&options, &fields.fields), - ast::Data::Enum(variants) => oneof::expand(&options, variants), - } + let mut result = match &options.data { + ast::Data::Struct(fields) => message::expand(&options, &fields.fields)?, + ast::Data::Enum(variants) => oneof::expand(&options, variants)?, + }; + + result.extend(serde::expand(&options)?); + + Ok(result) } pub fn parse_default_expr(meta: &Meta) -> darling::Result { diff --git a/bomboni_request_derive/src/parse/serde.rs b/bomboni_request_derive/src/parse/serde.rs new file mode 100644 index 0000000..b802172 --- /dev/null +++ b/bomboni_request_derive/src/parse/serde.rs @@ -0,0 +1,73 @@ +use crate::parse::ParseOptions; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn expand(options: &ParseOptions) -> syn::Result { + if !options.serde_as && !options.serialize_as && !options.deserialize_as { + return Ok(quote!()); + } + + let mut result = if let Some(path) = options.serde_crate.as_ref() { + quote! { + use #path as _serde; + } + } else { + quote! { + #[allow(unused_extern_crates, clippy::useless_attribute)] + extern crate serde as _serde; + } + }; + + if options.serde_as || options.serialize_as { + if !options.write { + return Err(syn::Error::new_spanned( + &options.ident, + "Cannot use `serde_as` or `serialize_as` without `write`", + )); + } + + let source = &options.source; + let ident = &options.ident; + let (impl_generics, type_generics, where_clause) = options.generics.split_for_impl(); + + result.extend(quote! { + #[automatically_derived] + impl #impl_generics _serde::Serialize for #ident #type_generics #where_clause { + fn serialize<__S>(&self, serializer: __S) -> _serde::__private::Result<__S::Ok, __S::Error> + where + __S: _serde::Serializer, + { + #source::serialize(&self.clone().into(), serializer) + } + } + }); + } + + if options.serde_as || options.deserialize_as { + let source = &options.source; + let ident = &options.ident; + let (impl_generics, type_generics, where_clause) = options.generics.split_for_impl(); + + result.extend(quote! { + #[automatically_derived] + impl<'de> #impl_generics _serde::Deserialize<'de> for #ident #type_generics #where_clause { + fn deserialize<__D>(deserializer: __D) -> _serde::__private::Result + where + __D: _serde::Deserializer<'de>, + { + #source::deserialize(deserializer)? + .parse_into() + .map_err(_serde::de::Error::custom) + } + } + }); + } + + Ok(quote! { + #[doc(hidden)] + #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] + const _ : () = { + #result + }; + }) +} diff --git a/bomboni_wasm/src/lib.rs b/bomboni_wasm/src/lib.rs index 7dcb3c1..094d761 100644 --- a/bomboni_wasm/src/lib.rs +++ b/bomboni_wasm/src/lib.rs @@ -6,8 +6,6 @@ pub mod utility; pub trait Wasm { type JsType: JsCast; - // const DECL: &'static str; - fn to_js(&self) -> Result where Self: serde::Serialize, @@ -90,13 +88,6 @@ mod tests { value: i32, } - // #[derive(Serialize, Deserialize, Wasm)] - // #[repr(i32)] - // pub enum CStyle { - // A = 1, - // B = 2, - // } - assert_eq!( ExternalTag::DECL, "export type ExternalTag = {\n String: string;\n Number?: null;\n} | {\n Number: number;\n String?: null;\n};" @@ -110,7 +101,5 @@ mod tests { InternalTag::DECL, "export type InternalTag = {\n kind: \"String\";\n value: string;\n} | ({\n kind: \"Item\";\n} & InternalItem);" ); - - // println!("{}", serde_json::to_string_pretty(&CStyle::A).unwrap()); } } diff --git a/bomboni_wasm_derive/src/wasm.rs b/bomboni_wasm_derive/src/wasm.rs index e9941f3..592c2ee 100644 --- a/bomboni_wasm_derive/src/wasm.rs +++ b/bomboni_wasm_derive/src/wasm.rs @@ -174,7 +174,7 @@ fn derive_proxy(proxy: &ProxyWasm, options: &WasmOptions) -> TokenStream { #[inline] fn from(value: #ident #type_generics) -> Self { let proxy: #proxy_ident = #proxy_into(value); - proxy.to_js().unwrap().into() + proxy.to_js().unwrap_throw().into() } } });