Skip to content

Commit

Permalink
Merge pull request #4 from syyyr/derive-try-from
Browse files Browse the repository at this point in the history
Add libshvproto-macros
  • Loading branch information
fvacek authored Jul 4, 2024
2 parents a01c83c + 2dda4d4 commit b5e4b9b
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
workspace = { members = ["libshvproto-macros"] }

[package]
name = "shvproto"
version = "3.0.2"
edition = "2021"

[dependencies]
libshvproto-macros = { path = "./libshvproto-macros" }
simple_logger = { git = "https://github.com/fvacek/rust-simple_logger.git", branch = "main", features = ["stderr"] }

log = "0.4.21"
Expand Down
15 changes: 15 additions & 0 deletions libshvproto-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "libshvproto-macros"
version = "0.1.0"
edition = "2021"

[lib]
name = "libshvproto_macros"
path = "src/lib.rs"
proc-macro = true

[dependencies]
convert_case = "0.6.0"
proc-macro2 = "1.0.86"
quote = "1.0.36"
syn = "2.0.68"
85 changes: 85 additions & 0 deletions libshvproto-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use convert_case::{Case, Casing};

use core::panic;

use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(TryFromRpcValue, attributes(field_name))]
pub fn derive_from_rpcvalue(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::DeriveInput);
let struct_identifier = &input.ident;

match &input.data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let mut struct_initializers = quote!{};
for field in fields {
let identifier = field.ident.as_ref().unwrap();
let field_name = field
.attrs.first()
.and_then(|x| x.meta.require_name_value().ok())
.filter(|x| x.path.is_ident("field_name"))
.map(|x| if let syn::Expr::Lit(expr) = &x.value { expr } else { panic!("Expected a string literal for 'field_name'") })
.map(|x| if let syn::Lit::Str(expr) = &x.lit { expr.value() } else { panic!("Expected a string literal for 'field_name'") })
.unwrap_or_else(|| identifier.to_string().to_case(Case::Camel));

struct_initializers.extend(quote!{
#identifier: get_key(#field_name).and_then(|x| x.try_into())?,
});

}
quote!{
impl TryFrom<shvproto::RpcValue> for #struct_identifier {
type Error = String;
fn try_from(value: shvproto::RpcValue) -> Result<Self, Self::Error> {
if let shvproto::Value::Map(value) = value.value() {
value.try_into()
} else {
Err("Value is not a map".into())
}
}
}

impl TryFrom<&shvproto::RpcValue> for #struct_identifier {
type Error = String;
fn try_from(value: &shvproto::RpcValue) -> Result<Self, Self::Error> {
if let shvproto::Value::Map(value) = value.value() {
value.as_ref().try_into()
} else {
Err("Value is not a map".into())
}
}
}

impl TryFrom<&Box<shvproto::Map>> for #struct_identifier {
type Error = String;
fn try_from(value: &Box<shvproto::Map>) -> Result<Self, Self::Error> {
value.as_ref().try_into()
}
}

impl TryFrom<&shvproto::Map> for #struct_identifier {
type Error = String;
fn try_from(value: &shvproto::Map) -> Result<Self, Self::Error> {
let get_key = |key_name| value.get(key_name).ok_or_else(|| "Missing ".to_string() + key_name + " key");
Ok(Self {
#struct_initializers
})
}
}

impl TryFrom<shvproto::Map> for #struct_identifier {
type Error = String;
fn try_from(value: shvproto::Map) -> Result<Self, Self::Error> {
let get_key = |key_name| value.get(key_name).ok_or_else(|| "Missing ".to_string() + key_name + " key");
Ok(Self {
#struct_initializers
})
}
}

}
}
_ => panic!("This macro can only be used on a struct.")
}.into()
}
55 changes: 55 additions & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#[cfg(test)]
mod test {
use libshvproto_macros::TryFromRpcValue;
use shvproto::RpcValue;
use shvproto::rpcvalue;

#[derive(Debug,PartialEq,TryFromRpcValue)]
struct EmptyStruct {
}

#[derive(Debug,PartialEq,TryFromRpcValue)]
struct OneFieldStruct {
x: i32
}

#[derive(Debug,PartialEq,TryFromRpcValue)]
struct TestStruct {
int_field: i32,
#[field_name = "my_custom_field_name"] int_field_with_custom_field_name: i32,
string_field: String,
map_field: shvproto::Map,
empty_struct_field: EmptyStruct,
one_field_struct: OneFieldStruct
}

#[test]
fn derive_struct() {
let x: TestStruct = shvproto::make_map!(
"intField" => 123,
"my_custom_field_name" => 1234,
"stringField" => "some_string",
"mapField" => shvproto::make_map!(
"some_key" => 123
),
"emptyStructField" => shvproto::make_map!(),
"oneFieldStruct" => shvproto::make_map!("x" => 4565)
).try_into().expect("Failed to parse");
assert_eq!(x, TestStruct {
int_field: 123,
string_field: "some_string".to_owned(),
int_field_with_custom_field_name: 1234,
map_field: shvproto::make_map!("some_key" => 123),
one_field_struct: OneFieldStruct {x: 4565},
empty_struct_field: EmptyStruct{}
})
}

#[test]
#[should_panic]
fn missing_fields() {
let _x: TestStruct = shvproto::make_map!(
"my_custom_field_name" => 1234
).try_into().expect("Asd");
}
}

0 comments on commit b5e4b9b

Please sign in to comment.