Skip to content

Commit

Permalink
Merge branch 'master' into very-long-decimals
Browse files Browse the repository at this point in the history
  • Loading branch information
fvacek authored Oct 23, 2024
2 parents 63ee6dd + 340cd06 commit 90872c6
Show file tree
Hide file tree
Showing 4 changed files with 357 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ workspace = { members = ["libshvproto-macros"] }

[package]
name = "shvproto"
version = "3.0.11"
version = "3.0.16"
edition = "2021"

[dependencies]
Expand Down
237 changes: 202 additions & 35 deletions libshvproto-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use convert_case::{Case, Casing};
use syn::punctuated::Punctuated;
use syn::{Meta, Token};

use core::panic;

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;

fn is_option(ty: &syn::Type) -> bool {
Expand Down Expand Up @@ -39,7 +42,84 @@ fn get_type(ty: &syn::Type) -> Option<String> {
}
}

#[proc_macro_derive(TryFromRpcValue, attributes(field_name))]
fn get_field_name(field: &syn::Field) -> String {
field
.attrs.first()
.and_then(|attr| attr.meta.require_name_value().ok())
.filter(|meta_name_value| meta_name_value.path.is_ident("field_name"))
.map(|meta_name_value| if let syn::Expr::Lit(expr) = &meta_name_value.value { expr } else { panic!("Expected a string literal for 'field_name'") })
.map(|literal| if let syn::Lit::Str(expr) = &literal.lit { expr.value() } else { panic!("Expected a string literal for 'field_name'") })
.unwrap_or_else(|| field.ident.as_ref().unwrap().to_string().to_case(Case::Camel))
}

fn field_to_initializers(
field_name: &str,
identifier: &syn::Ident,
is_option: bool,
from_value: Option<TokenStream2>,
context: &str,
) -> (TokenStream2, TokenStream2)
{
let struct_initializer;
let rpcvalue_insert;
let identifier_at_value = if let Some(value) = from_value {
quote! { #value.#identifier }
} else {
quote! { #identifier }
};
if is_option {
struct_initializer = quote!{
#identifier: match get_key(#field_name).ok() {
Some(x) => Some(x.try_into().map_err(|e| format!("{}Cannot parse `{}` field: {e}", #context, #field_name))?),
None => None,
},
};
rpcvalue_insert = quote!{
if let Some(val) = #identifier_at_value {
map.insert(#field_name.into(), val.into());
}
};
} else {
struct_initializer = quote!{
#identifier: get_key(#field_name)
.and_then(|x| x.try_into().map_err(|e| format!("Cannot parse `{}` field: {e}", #field_name)))
.map_err(|e| format!("{}{e}", #context))?,
};
rpcvalue_insert = quote!{
map.insert(#field_name.into(), #identifier_at_value.into());
};
}
(struct_initializer, rpcvalue_insert)
}

#[derive(Default)]
struct StructAttributes {
tag: Option<String>,
}

fn parse_struct_attributes(attrs: &Vec<syn::Attribute>) -> syn::Result<StructAttributes> {
let mut res = StructAttributes::default();
for attr in attrs {
if attr.path().is_ident("rpcvalue") {
let nested = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
for meta in &nested {
match meta {
// #[rpcvalue(tag = "type")]
Meta::NameValue(name_value) if name_value.path.is_ident("tag") => {
let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(tag_lit), .. }) = &name_value.value else {
return Err(syn::Error::new_spanned(meta, "tag value is not a string literal"));
};
res.tag = Some(tag_lit.value());
}
_ => return Err(syn::Error::new_spanned(meta, "unrecognized attributes")),
}
}
}
}
Ok(res)
}

#[proc_macro_derive(TryFromRpcValue, attributes(field_name,rpcvalue))]
pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::DeriveInput);
let struct_identifier = &input.ident;
Expand All @@ -59,32 +139,16 @@ pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
let mut expected_keys = vec![];
let mut rpcvalue_inserts = quote!{};
for field in fields {
let identifier = field.ident.as_ref().unwrap();
let field_name = field
.attrs.first()
.and_then(|attr| attr.meta.require_name_value().ok())
.filter(|meta_name_value| meta_name_value.path.is_ident("field_name"))
.map(|meta_name_value| if let syn::Expr::Lit(expr) = &meta_name_value.value { expr } else { panic!("Expected a string literal for 'field_name'") })
.map(|literal| if let syn::Lit::Str(expr) = &literal.lit { expr.value() } else { panic!("Expected a string literal for 'field_name'") })
.unwrap_or_else(|| identifier.to_string().to_case(Case::Camel));

if is_option(&field.ty) {
struct_initializers.extend(quote!{
#identifier: get_key(#field_name).ok().and_then(|x| x.try_into().ok()),
});
rpcvalue_inserts.extend(quote!{
if let Some(val) = value.#identifier {
map.insert(#field_name.into(), val.into());
}
});
} else {
struct_initializers.extend(quote!{
#identifier: get_key(#field_name).and_then(|x| x.try_into())?,
});
rpcvalue_inserts.extend(quote!{
map.insert(#field_name.into(), value.#identifier.into());
});
}
let field_name = get_field_name(field);
let (struct_initializer, rpcvalue_insert) = field_to_initializers(
&field_name,
field.ident.as_ref().expect("Missing field identifier"),
is_option(&field.ty),
Some(quote! { value }),
"",
);
struct_initializers.extend(struct_initializer);
rpcvalue_inserts.extend(rpcvalue_insert);
expected_keys.push(quote!{#field_name});
}

Expand Down Expand Up @@ -121,7 +185,7 @@ pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
impl #struct_generics_with_bounds TryFrom<&shvproto::Map> for #struct_identifier #struct_generics_without_bounds {
type Error = String;
fn try_from(value: &shvproto::Map) -> Result<Self, <Self as TryFrom<&shvproto::Map>>::Error> {
let get_key = |key_name| value.get(key_name).ok_or_else(|| "Missing ".to_string() + key_name + " key");
let get_key = |key_name| value.get(key_name).ok_or_else(|| "Missing `".to_string() + key_name + "` key");
let unexpected_keys = value
.keys()
.map(String::as_str)
Expand Down Expand Up @@ -158,6 +222,8 @@ pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
let mut match_arms_ser = quote!{};
let mut allowed_types = vec![];
let mut custom_type_matchers = vec![];
let mut match_arms_tags = vec![];
let struct_attributes = parse_struct_attributes(&input.attrs).unwrap();
let mut map_has_been_matched_as_map: Option<(proc_macro2::TokenStream, proc_macro2::TokenStream)> = None;
let mut map_has_been_matched_as_struct: Option<Vec<(proc_macro2::TokenStream, proc_macro2::TokenStream)>> = None;
for variant in variants {
Expand Down Expand Up @@ -203,15 +269,23 @@ pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
.iter()
.map(|(ident, ty)| quote!{ #ident(#ty) })
.collect::<Vec<proc_macro2::TokenStream>>();
panic!("Can't match enum variant {}(Map), because a Map will already be matched as one of: {}", quote!{#variant_ident}, quote!{#(#matched_variants),*});
panic!("Can't match enum variant {}(Map), because a Map will already be matched as one of: {}",
quote!{#variant_ident},
quote!{#(#matched_variants),*}
);
}
add_type_matcher(&mut match_arms_de, quote!{Map(x)}, quote!{Map}, unbox_code.clone());
map_has_been_matched_as_map = Some((quote!(#variant_ident), quote!{#source_variant_type}));
},
"IMap" => add_type_matcher(&mut match_arms_de, quote!{IMap(x)}, quote!{IMap}, unbox_code.clone()),
_ => {
if let Some((matched_variant_ident, matched_variant_type)) = map_has_been_matched_as_map {
panic!("Can't match enum variant {}({}) as a Map, because a Map will already be matched as {}({})", quote!{#variant_ident}, quote!{#source_variant_type}, quote!{#matched_variant_ident}, quote!{#matched_variant_type});
panic!("Can't match enum variant {}({}) as a Map, because a Map will already be matched as {}({})",
quote!{#variant_ident},
quote!{#source_variant_type},
quote!{#matched_variant_ident},
quote!{#matched_variant_type}
);
}
custom_type_matchers.push(quote!{
if let Ok(val) = #source_variant_type::try_from(x.as_ref().clone()) {
Expand All @@ -232,15 +306,108 @@ pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
});
add_type_matcher(&mut match_arms_de, quote!{String(s) if s.as_str() == stringify!(#variant_ident)}, quote!{#variant_ident}, quote!());
},
syn::Fields::Named(_) => ()
syn::Fields::Named(variant_fields) => {
if let Some((matched_variant_ident, matched_variant_type)) = map_has_been_matched_as_map {
panic!("Can't match enum variant {}(...) as a Map, because a Map will already be matched as {}({})",
quote!{#variant_ident},
quote!{#matched_variant_ident},
quote!{#matched_variant_type}
);
}

let mut struct_initializers = quote! {};
let mut rpcvalue_inserts = quote! {};
let mut field_idents = vec![];
let variant_ident_name = variant_ident.to_string().to_case(Case::Camel);

for field in &variant_fields.named {
let field_ident = field.ident.as_ref().expect("Missing field identifier");
let (struct_initializer, rpcvalue_insert) = field_to_initializers(
get_field_name(field).as_str(),
field_ident,
is_option(&field.ty),
None,
&format!("Cannot deserialize into `{}` enum variant: ", variant_ident_name.as_str()),
);
struct_initializers.extend(struct_initializer);
rpcvalue_inserts.extend(rpcvalue_insert);
field_idents.push(field_ident);
}

if let Some(tag_key) = &struct_attributes.tag {
rpcvalue_inserts.extend(quote! {
map.insert(#tag_key.into(), #variant_ident_name.into());
});
} else {
rpcvalue_inserts = quote! {
map.insert(#variant_ident_name.into(), {
let mut map = shvproto::rpcvalue::Map::new();
#rpcvalue_inserts
map.into()
});
};
}

match_arms_ser.extend(quote!{
#struct_identifier::#variant_ident{ #(#field_idents),* } => {
let mut map = shvproto::rpcvalue::Map::new();
#rpcvalue_inserts
map.into()
}
});

match_arms_tags.push(quote! {
#variant_ident_name => Ok(#struct_identifier::#variant_ident {
#struct_initializers
}),
});

map_has_been_matched_as_struct
.get_or_insert(vec![])
.push((quote!(#variant_ident), quote!(#(#field_idents)*)));
},
}
}

if !match_arms_tags.is_empty() {
if let Some(tag_key) = &struct_attributes.tag {
custom_type_matchers.push(quote! {
let tag: String = x.get(#tag_key)
.ok_or_else(|| "Missing `".to_string() + #tag_key + "` key")
.and_then(|val| val
.try_into()
.map_err(|e: String| "Cannot parse `".to_string() + #tag_key + "` field: " + &e)
)
.map_err(|e| "Cannot get tag: ".to_string() + &e)?;
});
} else {
custom_type_matchers.push(quote! {
let (tag, val) = x.iter().nth(0).ok_or_else(|| "Cannot get tag from an empty Map")?;
let x: shvproto::rpcvalue::Map = val.try_into()?;
});
}

custom_type_matchers.push(quote! {
let get_key = |key_name| x
.get(key_name)
.ok_or_else(|| "Missing `".to_string() + key_name + "` key");

match tag.as_str() {
#(#match_arms_tags)*
_ => Err("Couldn't deserialize into `".to_owned() + stringify!(#struct_identifier) + "` enum from a Map. Unknown tag `" + &tag + "`"),
}
});
}

if !custom_type_matchers.is_empty() {
allowed_types.push(quote!{stringify!(Map(x))});
custom_type_matchers.push(quote!{
Err("Couldn't deserialize into '".to_owned() + stringify!(#struct_identifier) + "' enum from a Map.")
});
if match_arms_tags.is_empty() {
// The match in match_arms_tags is the last expression returned from
// custom_type_matchers. If match_arms_tags is empty, just return Err.
custom_type_matchers.push(quote!{
Err("Couldn't deserialize into `".to_owned() + stringify!(#struct_identifier) + "` enum from a Map.")
});
}
match_arms_de.push(quote!{
shvproto::Value::Map(x) => {
#(#custom_type_matchers)*
Expand All @@ -254,7 +421,7 @@ pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
fn try_from(value: shvproto::RpcValue) -> Result<Self, <Self as TryFrom<shvproto::RpcValue>>::Error> {
match value.value() {
#(#match_arms_de),*,
_ => Err("Couldn't deserialize into '".to_owned() + stringify!(#struct_identifier) + "' enum, allowed types: " + [#(#allowed_types),*].join("|").as_ref() + ", got: " + value.type_name())
_ => Err("Couldn't deserialize into `".to_owned() + stringify!(#struct_identifier) + "` enum, allowed types: " + [#(#allowed_types),*].join("|").as_ref() + ", got: " + value.type_name())
}
}
}
Expand Down
30 changes: 29 additions & 1 deletion src/rpcvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ impl From<Vec<u8>> for RpcValue {
}
}

impl<T> From<Option<T>> for RpcValue
where
RpcValue: From<T>,
{
fn from(value: Option<T>) -> Self {
value.map_or_else(RpcValue::null, RpcValue::from)
}
}

#[cfg(feature = "specialization")]
mod with_specialization {
use super::{
Expand Down Expand Up @@ -648,6 +657,23 @@ impl TryFrom<Value> for chrono::NaiveDateTime {
}
}

impl TryFrom<&Value> for chrono::DateTime<chrono::FixedOffset> {
type Error = String;
fn try_from(value: &Value) -> Result<Self, Self::Error> {
match value {
Value::DateTime(val) => Ok(val.to_chrono_datetime()),
_ => Err(format_err_try_from("DateTime", value.type_name()))
}
}
}

impl TryFrom<Value> for chrono::DateTime<chrono::FixedOffset> {
type Error = String;
fn try_from(value: Value) -> Result<Self, Self::Error> {
Self::try_from(&value)
}
}

// Cannot use generic trait implementation here.
// See `rustc --explain E0210`
macro_rules! try_from_rpc_value_ref {
Expand All @@ -674,6 +700,7 @@ macro_rules! try_from_rpc_value {
}

try_from_rpc_value_ref!(());
try_from_rpc_value!(());
try_from_rpc_value_ref!(bool);
try_from_rpc_value!(bool);
try_from_rpc_value_ref!(&'a str);
Expand Down Expand Up @@ -713,7 +740,8 @@ try_from_rpc_value_ref!(datetime::DateTime);
try_from_rpc_value!(datetime::DateTime);
try_from_rpc_value_ref!(chrono::NaiveDateTime);
try_from_rpc_value!(chrono::NaiveDateTime);

try_from_rpc_value_ref!(chrono::DateTime<chrono::FixedOffset>);
try_from_rpc_value!(chrono::DateTime<chrono::FixedOffset>);

impl<T> TryFrom<&RpcValue> for Vec<T>
where
Expand Down
Loading

0 comments on commit 90872c6

Please sign in to comment.