diff --git a/Cargo.toml b/Cargo.toml index 3494fa9..9cf4dd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["ImJeremyHe"] edition = "2018" name = "gents" -version = "1.1.0" +version = "1.2.0" license = "MIT" description = "generate Typescript interfaces from Rust code" repository = "https://github.com/ImJeremyHe/gents" diff --git a/derives/Cargo.toml b/derives/Cargo.toml index 5ebc190..3c908e7 100644 --- a/derives/Cargo.toml +++ b/derives/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gents_derives" -version = "1.1.0" +version = "1.2.0" description = "provides some macros for gents" authors = ["ImJeremyHe"] license = "MIT" diff --git a/derives/src/ts_interface.rs b/derives/src/ts_interface.rs index a66bd5a..d8b0542 100644 --- a/derives/src/ts_interface.rs +++ b/derives/src/ts_interface.rs @@ -21,12 +21,14 @@ pub fn ts_interface(attr: TokenStream, item: TokenStream) -> TokenStream { struct TsInterfaceArgs { file_name: String, ident: Option, + async_func: bool, } impl Parse for TsInterfaceArgs { fn parse(input: ParseStream) -> Result { let mut file_name: Option = None; let mut ident_val: Option = None; + let mut async_func: bool = false; while !input.is_empty() { let ident: Ident = input.parse()?; @@ -37,10 +39,12 @@ impl Parse for TsInterfaceArgs { file_name = Some(lit.value()); } else if ident == "ident" { ident_val = Some(lit.value()); + } else if ident == "async" { + async_func = lit.value() == "true"; } else { return Err(Error::new_spanned( ident, - "expected `file_name = \"...\"` or `ident = \"...\"`", + "expected `file_name = \"...\"` or `ident = \"...\"` or `async = \"true|false\"`", )); } @@ -56,6 +60,7 @@ impl Parse for TsInterfaceArgs { Ok(Self { file_name, ident: ident_val, + async_func, }) } } @@ -78,16 +83,10 @@ fn expand_ts_interface( // prefer user-specified ident if provided, otherwise fall back to Rust type name let type_name = args.ident.unwrap_or_else(|| self_ty_ident.to_string()); let file_name = args.file_name; + let async_func = args.async_func; - // Collect impl-level doc comments - let mut impl_comments = Vec::new(); - for attr in &impl_block.attrs { - if attr.path().is_ident("doc") { - if let Ok(lit) = attr.parse_args::() { - impl_comments.push(lit.value()); - } - } - } + let impl_comments = Vec::::new(); + // TODO: collect impl-level doc comments // Collect public methods let mut method_tokens = Vec::new(); @@ -113,6 +112,7 @@ fn expand_ts_interface( file_name: #file_name.to_string(), methods: vec![ #(#method_tokens),* ], comment: vec![ #( #impl_comments.to_string() ),* ], + async_func: #async_func, } } } @@ -136,16 +136,33 @@ fn extract_self_type_ident(ty: &Type) -> Result<&Ident> { } } +fn extract_doc(attr: &syn::Attribute) -> Option { + if !attr.path().is_ident("doc") { + return None; + } + + match &attr.meta { + syn::Meta::NameValue(nv) => { + if let syn::Expr::Lit(expr_lit) = &nv.value { + if let syn::Lit::Str(lit_str) = &expr_lit.lit { + return Some(lit_str.value()); + } + } + None + } + _ => None, + } +} + fn expand_method(func: &ImplItemFn) -> Result { let name = convert_camel_from_snake(func.sig.ident.to_string()); // Collect method-level doc comments let mut comments = Vec::new(); for attr in &func.attrs { - if attr.path().is_ident("doc") { - if let Ok(lit) = attr.parse_args::() { - comments.push(lit.value()); - } + if let Some(doc) = extract_doc(attr) { + let d = doc.trim_start().to_string(); + comments.push(d); } } diff --git a/src/descriptor.rs b/src/descriptor.rs index 041fe41..7be17c5 100644 --- a/src/descriptor.rs +++ b/src/descriptor.rs @@ -40,6 +40,7 @@ pub struct ApiDescriptor { pub file_name: String, pub methods: Vec, pub comment: Vec, + pub async_func: bool, } pub struct MethodDescriptor { @@ -203,6 +204,7 @@ impl DescriptorManager { let (ts_name, file_name) = get_import_deps(&descriptors, idx); fmt.add_import(&ts_name, &file_name); }); + let async_func = api.async_func; // For API files we currently do not emit comments into the generated TS, // so that the output matches the expected test fixtures exactly. @@ -223,7 +225,12 @@ impl DescriptorManager { let desc = descriptors.get(*idx).unwrap(); desc.ts_name().to_string() }); - fmt.add_method(&m.name, params, ret); + fmt.add_comment(&m.comment); + if async_func { + fmt.add_async_method(&m.name, params, ret); + } else { + fmt.add_method(&m.name, params, ret); + } }); fmt.end_interface(); result.push((api.file_name.to_string(), fmt.end_file())); diff --git a/src/ts_formatter.rs b/src/ts_formatter.rs index 9737c63..7451d66 100644 --- a/src/ts_formatter.rs +++ b/src/ts_formatter.rs @@ -73,6 +73,24 @@ impl TsFormatter { self.write_line(&format!("{}({}): {};", name, param_str, ret_str)); } + pub fn add_async_method( + &mut self, + name: &str, + params: Vec<(String, String)>, + ret: Option, + ) { + let param_str = params + .iter() + .map(|(n, t)| format!("{}: {}", n, t)) + .collect::>() + .join(", "); + let ret_str = ret.map_or("void".to_string(), |r| r); + self.write_line(&format!( + "async {}({}): Promise<{}>;", + name, param_str, ret_str + )); + } + pub fn end_interface(&mut self) { if self.indent > 0 { self.indent -= 1; diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 89ec499..e010c67 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -510,7 +510,7 @@ mod test_api { assert_eq!( files.get("v1_api.ts").unwrap(), - &"export interface V1Api {\n f1(): number;\n f2(): string;\n setF1(f1: number): void;\n setF2(f2: string): void;\n}\n" + &"export interface V1Api {\n f1(): number;\n f2(): string;\n // set f1\n setF1(f1: number): void;\n setF2(f2: string): void;\n}\n" ); } }