From c60911fd402ea871f59ada18b4943a93ebdbab7d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 20 Nov 2023 14:39:56 +0800 Subject: [PATCH] feat: add a attribute proc macro for convenient sake (#10) --- Cargo.toml | 5 +- derives/Cargo.toml | 4 +- derives/src/container.rs | 134 ++++++++++++++++++++++++--------------- derives/src/lib.rs | 20 ++++++ src/descriptor.rs | 18 ++++++ tests/test.rs | 29 +++++++++ 6 files changed, 155 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index de60cc6..1f9ed60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,12 @@ authors = ["ImJeremyHe"] edition = "2018" name = "gents" -version = "0.6.0" +version = "0.7.0" license = "MIT" description = "generate Typescript interfaces from Rust code" repository = "https://github.com/ImJeremyHe/gents" keywords = ["Typescript", "interface", "ts-rs", "Rust", "wasm"] [dev-dependencies] -gents_derives = {version = "0.6.0", path = "./derives"} +gents_derives = {version = "0.7.0", path = "./derives"} +serde = {version = "1.0", features = ["derive"]} diff --git a/derives/Cargo.toml b/derives/Cargo.toml index 2e1dae6..87e5713 100644 --- a/derives/Cargo.toml +++ b/derives/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gents_derives" -version = "0.6.0" +version = "0.7.0" description = "provides some macros for gents" authors = ["ImJeremyHe"] license = "MIT" @@ -10,7 +10,7 @@ edition = "2018" proc-macro = true [dependencies] -syn = {version = "1.0.86", features = ["full"]} +syn = {version = "2.0.28", features = ["full"]} quote = "1.0.15" paste = "1.0.5" proc-macro2 = "1.0.36" \ No newline at end of file diff --git a/derives/src/container.rs b/derives/src/container.rs index fd3c370..34df5be 100644 --- a/derives/src/container.rs +++ b/derives/src/container.rs @@ -1,8 +1,9 @@ use proc_macro2::Ident; +use syn::parse::Parse; +use syn::punctuated::Punctuated; +use syn::token::Comma; use syn::Attribute; -use syn::Meta::List; -use syn::Meta::NameValue; -use syn::NestedMeta::Meta; +use syn::MetaNameValue; use syn::Type; use crate::symbol::FILE_NAME; @@ -30,24 +31,22 @@ impl<'a> Container<'a> { .flat_map(|attr| get_ts_meta_items(attr)) .flatten() { - match meta_item { - Meta(NameValue(m)) if m.path == RENAME_ALL => { - let s = get_lit_str(&m.lit).expect("rename_all requires lit str"); - let t = match s.value().as_str() { - "camelCase" => RenameAll::CamelCase, - _ => panic!("unexpected literal for case converting"), - }; - rename_all = Some(t); - } - Meta(NameValue(m)) if m.path == FILE_NAME => { - let s = get_lit_str(&m.lit).expect("file_name requires lit str"); - file_name = Some(s.value()); - } - Meta(NameValue(m)) if m.path == RENAME => { - let s = get_lit_str(&m.lit).expect("rename requires lit str"); - rename = Some(s.value()); - } - _ => panic!("unexpected attr"), + let m = meta_item; + if m.path == RENAME_ALL { + let s = get_lit_str(&m.value).expect("rename_all requires lit str"); + let t = match s.value().as_str() { + "camelCase" => RenameAll::CamelCase, + _ => panic!("unexpected literal for case converting"), + }; + rename_all = Some(t); + } else if m.path == FILE_NAME { + let s = get_lit_str(&m.value).expect("file_name requires lit str"); + file_name = Some(s.value()); + } else if m.path == RENAME { + let s = get_lit_str(&m.value).expect("rename requires lit str"); + rename = Some(s.value()); + } else { + panic!("unexpected attr") } } match &item.data { @@ -138,20 +137,17 @@ fn parse_attrs<'a>(attrs: &'a Vec) -> FieldAttrs { .flat_map(|attr| get_ts_meta_items(attr)) .flatten() { - match meta_item { - Meta(NameValue(m)) if m.path == RENAME => { - if let Ok(s) = get_lit_str(&m.lit) { - rename = Some(s.value()); - } + let m = meta_item; + if m.path == RENAME { + if let Ok(s) = get_lit_str(&m.value) { + rename = Some(s.value()); } - Meta(NameValue(m)) if m.path == SKIP => { - if let Ok(s) = get_lit_bool(&m.lit) { - skip = s; - } else { - panic!("expected bool value in skip attr") - } + } else if m.path == SKIP { + if let Ok(s) = get_lit_bool(&m.value) { + skip = s; + } else { + panic!("expected bool value in skip attr") } - _ => {} } } FieldAttrs { skip, rename } @@ -166,41 +162,42 @@ pub enum RenameAll { CamelCase, } -fn get_ts_meta_items(attr: &syn::Attribute) -> Result, ()> { - if attr.path != TS { +fn get_ts_meta_items(attr: &syn::Attribute) -> Result, ()> { + if attr.path() != TS { return Ok(Vec::new()); } - match attr.parse_meta() { - Ok(List(meta)) => Ok(meta.nested.into_iter().collect()), - Ok(_) => Err(()), + match attr.parse_args_with(Punctuated::::parse_terminated) { + Ok(name_values) => Ok(name_values.into_iter().collect()), Err(_) => Err(()), } } -fn get_lit_str<'a>(lit: &'a syn::Lit) -> Result<&'a syn::LitStr, ()> { - if let syn::Lit::Str(lit) = lit { - Ok(lit) - } else { - Err(()) +fn get_lit_str<'a>(lit: &'a syn::Expr) -> Result<&'a syn::LitStr, ()> { + if let syn::Expr::Lit(lit) = lit { + if let syn::Lit::Str(l) = &lit.lit { + return Ok(&l); + } } + Err(()) } -fn get_lit_bool<'a>(lit: &'a syn::Lit) -> Result { - if let syn::Lit::Bool(b) = lit { - Ok(b.value) - } else { - Err(()) +fn get_lit_bool<'a>(lit: &'a syn::Expr) -> Result { + if let syn::Expr::Lit(lit) = lit { + if let syn::Lit::Bool(b) = &lit.lit { + return Ok(b.value); + } } + Err(()) } fn parse_comments(attrs: &[Attribute]) -> Vec { let mut result = Vec::new(); attrs.iter().for_each(|attr| { - if attr.path.is_ident("doc") { - if let Ok(NameValue(nv)) = &attr.parse_meta() { - if let Ok(s) = get_lit_str(&nv.lit) { + if attr.path().is_ident("doc") { + if let Ok(nv) = attr.meta.require_name_value() { + if let Ok(s) = get_lit_str(&nv.value) { let comment = s.value(); result.push(comment.trim().to_string()); } @@ -209,3 +206,38 @@ fn parse_comments(attrs: &[Attribute]) -> Vec { }); result } + +pub(crate) struct GentsWasmAttrs { + file_name: String, +} + +impl GentsWasmAttrs { + pub fn get_file_name(&self) -> &str { + &self.file_name + } +} + +impl Parse for GentsWasmAttrs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let name_values = Punctuated::::parse_terminated(input)?; + let mut file_name = String::new(); + name_values.into_iter().for_each(|name_value| { + let path = name_value.path; + let attr = path + .get_ident() + .expect("unvalid attr, should be an ident") + .to_string(); + let value = get_lit_str(&name_value.value) + .expect("should be a str") + .value(); + match attr.as_str() { + "file_name" => file_name = value, + _ => panic!("invalid attr: {}", attr), + } + }); + if file_name.is_empty() { + panic!("file_name unset") + } + Ok(GentsWasmAttrs { file_name }) + } +} diff --git a/derives/src/lib.rs b/derives/src/lib.rs index 9c644e4..15e899b 100644 --- a/derives/src/lib.rs +++ b/derives/src/lib.rs @@ -8,6 +8,26 @@ use container::{Container, RenameAll}; use proc_macro::TokenStream; use quote::quote; +use crate::container::GentsWasmAttrs; + +#[proc_macro_attribute] +pub fn gents_header(attr: TokenStream, item: TokenStream) -> TokenStream { + let item: proc_macro2::TokenStream = item.into(); + let attrs = syn::parse2::(attr.into()).expect("parse error, please check"); + let file_name = attrs.get_file_name(); + quote! { + #[derive(::serde::Serialize, ::serde::Deserialize)] + #[cfg_attr(any(test, feature = "gents"), derive(::gents_derives::TS))] + #[cfg_attr( + any(test, feature = "gents"), + ts(file_name = #file_name, rename_all = "camelCase") + )] + #[serde(rename_all = "camelCase")] + #item + } + .into() +} + #[proc_macro_derive(TS, attributes(ts))] pub fn derive_ts(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/src/descriptor.rs b/src/descriptor.rs index ee3e0b2..3d74cd0 100644 --- a/src/descriptor.rs +++ b/src/descriptor.rs @@ -286,6 +286,24 @@ impl TS for Option { } } +impl TS for Result { + fn _register(manager: &mut DescriptorManager) -> usize { + let t_idx = T::_register(manager); + let e_idx = E::_register(manager); + let type_id = TypeId::of::(); + let descriptor = GenericDescriptor { + dependencies: vec![t_idx, e_idx], + ts_name: Self::_ts_name(), + optional: false, + }; + manager.registry(type_id, Descriptor::Generics(descriptor)) + } + + fn _ts_name() -> String { + format!("{} | {}", T::_ts_name(), E::_ts_name()) + } +} + impl TS for (K, V) where K: TS + 'static, diff --git a/tests/test.rs b/tests/test.rs index 8c78bff..8655440 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -47,6 +47,7 @@ pub struct TestSkip { mod tests { use super::*; use gents::*; + use gents_derives::gents_header; #[test] fn gen_skip_test() { @@ -178,4 +179,32 @@ export interface StructWithComments { }"# ); } + + #[test] + fn test_result() { + #[gents_header(file_name = "test_struct.ts")] + pub struct TestStruct { + pub f1: u8, + pub f2: Result, + } + let mut manager = DescriptorManager::default(); + TestStruct::_register(&mut manager); + let (_, content) = manager.gen_data().into_iter().next().unwrap(); + assert_eq!( + content.trim(), + r#"export interface TestStruct { + f1: number + f2: string | number +}"# + ); + } + + #[test] + fn test_gents_for_wasm() { + #[gents_header(file_name = "test_struct.ts")] + pub struct TestStruct { + pub f1: u8, + pub f2: String, + } + } }