diff --git a/rust/candid/tests/types.rs b/rust/candid/tests/types.rs index 847dbedaf..356c7b176 100644 --- a/rust/candid/tests/types.rs +++ b/rust/candid/tests/types.rs @@ -350,6 +350,7 @@ pub struct List { #[test] fn test_func() { + /// Doc comment for 🐂 method #[candid_method(query, rename = "🐂")] fn test(a: String, b: i32) -> (String, i32) { (a, b) @@ -382,24 +383,29 @@ fn test_func() { } } use internal::A; + /// Doc comment for id_variant method #[candid::candid_method] fn id_variant(_: &[internal::A]) -> Result<((A,), A), String> { unreachable!() } + /// Doc comment for oneway method #[candid_method(oneway)] fn oneway(_: &str) { unreachable!() } + /// Doc comment for id_struct query method #[candid_method(query)] fn id_struct(_: (List,)) -> Result, candid::Empty> { unreachable!() } + /// Doc comment for id_struct_composite composite_query method #[candid_method(composite_query)] fn id_struct_composite(_: (List,)) -> Result, candid::Empty> { unreachable!() } + /// Doc comment for id_tuple_destructure method #[candid_method] fn id_tuple_destructure((a, b): (u8, u8)) -> (u8, u8) { (a, b) @@ -437,13 +443,19 @@ type Result = variant { Ok : List; Err : empty }; type Result_1 = variant { Ok : record { record { A }; A }; Err : text }; type Wrap = record { head : int8; tail : opt Box }; service : (List_2) -> { + /// Doc comment for id_struct query method id_struct : (record { List }) -> (Result) query; + /// Doc comment for id_struct_composite composite_query method id_struct_composite : (record { List }) -> (Result) composite_query; id_struct_destructure : (NamedStruct) -> (nat16, int32); + /// Doc comment for id_tuple_destructure method id_tuple_destructure : (record { nat8; nat8 }) -> (nat8, nat8); id_unused_arg : (nat8) -> (Result); + /// Doc comment for id_variant method id_variant : (vec A) -> (Result_1); + /// Doc comment for oneway method "oneway" : (text) -> () oneway; + /// Doc comment for 🐂 method "🐂" : (a : text, b : int32) -> (text, int32) query; }"#; assert_eq!(expected, __export_service()); @@ -458,14 +470,17 @@ fn test_counter() { fn init() -> Self { Service { counter: 0 } } + /// Doc comment for inc method #[candid_method] fn inc(&mut self) { self.counter += 1; } + /// Doc comment for read method #[candid_method(query)] fn read(&self) -> usize { self.counter } + /// Doc comment for set method #[candid_method] fn set(&mut self, value: usize) { self.counter = value; @@ -473,8 +488,11 @@ fn test_counter() { } candid::export_service!(); let expected = r#"service : { + /// Doc comment for inc method inc : () -> (); + /// Doc comment for read method read : () -> (nat64) query; + /// Doc comment for set method set : (value : nat64) -> (); }"#; assert_eq!(expected, __export_service()); diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index 2fff7b72b..7cfb7d984 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -1,3 +1,5 @@ +use crate::get_doc_comment_lines; + use super::{candid_path, get_lit_str}; use lazy_static::lazy_static; use proc_macro2::TokenStream; @@ -10,11 +12,13 @@ type RawArgs = Vec<(Option, String)>; type RawRets = Vec; type ParsedArgs = Vec<(Option, Type)>; type ParsedRets = Vec; +type CommentLines = Vec; struct Method { args: RawArgs, rets: RawRets, modes: String, + doc_comment: CommentLines, } // There is no official way to communicate information across proc macro invocations. @@ -54,6 +58,7 @@ pub(crate) fn candid_method(attrs: Vec, fun: ItemFn) -> Result { @@ -69,7 +74,15 @@ pub(crate) fn candid_method(attrs: Vec, fun: ItemFn) -> Result) -> TokenStream { } else { unreachable!(); }; - let gen_tys = meths.iter().map(|(name, Method { args, rets, modes })| { - let args = args - .iter() - .map(|t| generate_arg(quote! { args }, t)) - .collect::>(); - let rets = rets - .iter() - .map(|t| generate_ret(quote! { rets }, t)) - .collect::>(); - let modes = match modes.as_ref() { - "query" => quote! { vec![#candid::types::FuncMode::Query] }, - "composite_query" => quote! { vec![#candid::types::FuncMode::CompositeQuery] }, - "oneway" => quote! { vec![#candid::types::FuncMode::Oneway] }, - "update" => quote! { vec![] }, - _ => unreachable!(), - }; - quote! { - { - let mut args: Vec = Vec::new(); - #(#args)* - let mut rets: Vec = Vec::new(); - #(#rets)* - let func = Function { args, rets, modes: #modes }; - service.push((#name.to_string(), TypeInner::Func(func).into())); + let gen_tys = meths.iter().map( + |( + name, + Method { + args, + rets, + modes, + doc_comment, + }, + )| { + let args = args + .iter() + .map(|t| generate_arg(quote! { args }, t)) + .collect::>(); + let rets = rets + .iter() + .map(|t| generate_ret(quote! { rets }, t)) + .collect::>(); + let modes = match modes.as_ref() { + "query" => quote! { vec![#candid::types::FuncMode::Query] }, + "composite_query" => quote! { vec![#candid::types::FuncMode::CompositeQuery] }, + "oneway" => quote! { vec![#candid::types::FuncMode::Oneway] }, + "update" => quote! { vec![] }, + _ => unreachable!(), + }; + let doc_comment = generate_doc_comment(doc_comment.as_slice()); + quote! { + { + let mut args: Vec = Vec::new(); + #(#args)* + let mut rets: Vec = Vec::new(); + #(#rets)* + let func = Function { args, rets, modes: #modes }; + service.push((#name.to_string(), TypeInner::Func(func).into())); + let function_doc_comment = #doc_comment; + if !function_doc_comment.is_empty() { + doc_comments.insert(#name.to_string(), function_doc_comment); + } + } } - } - }); + }, + ); let service = quote! { use #candid::types::{CandidType, Function, Type, ArgType, TypeInner}; use #candid::types::syntax::{Binding, IDLMergedProg, IDLActorType}; let mut service = Vec::<(String, Type)>::new(); + let mut doc_comments: std::collections::HashMap> = std::collections::HashMap::new(); let mut env = #candid::types::internal::TypeContainer::new(); #(#gen_tys)* service.sort_unstable_by_key(|(name, _)| name.clone()); @@ -145,13 +174,14 @@ pub(crate) fn export_service(path: Option) -> TokenStream { let bindings = env.env.0.iter().map(|(id, t)| Binding { id: id.clone(), typ: env.as_idl_type(t), - doc_comment: None, + doc_comment: doc_comments.get(id).cloned(), }).collect::>(); let mut idl_merged_prog = IDLMergedProg::from(bindings); idl_merged_prog.set_actor(Some(IDLActorType { typ: env.as_idl_type(&actor), doc_comment: None, })); + idl_merged_prog.set_comments_in_actor(&doc_comments); let result = #candid::pretty::candid::compile(&idl_merged_prog); format!("{}", result) @@ -183,6 +213,15 @@ fn generate_ret(name: TokenStream, ty: &str) -> TokenStream { } } +fn generate_doc_comment(comment_lines: &[String]) -> TokenStream { + let comment_strings: Vec = comment_lines + .iter() + .map(|s| quote::quote! { #s.to_string() }) + .collect(); + + quote::quote! { vec![#(#comment_strings),*] } +} + fn get_args(sig: &Signature) -> Result<(ParsedArgs, ParsedRets)> { let mut args = Vec::new(); for arg in &sig.inputs { diff --git a/rust/candid_derive/src/lib.rs b/rust/candid_derive/src/lib.rs index eb26e4263..78b6d3031 100644 --- a/rust/candid_derive/src/lib.rs +++ b/rust/candid_derive/src/lib.rs @@ -61,6 +61,22 @@ pub(crate) fn get_lit_str(expr: &syn::Expr) -> std::result::Result Vec { + attrs + .iter() + .filter_map(|attr| match &attr.meta { + syn::Meta::NameValue(m) if m.path.is_ident("doc") => { + if let Ok(lit) = get_lit_str(&m.value) { + Some(lit.value().trim().to_string()) + } else { + None + } + } + _ => None, + }) + .collect() +} + fn get_custom_candid_path(input: &syn::DeriveInput) -> Result> { let candid_path_helper_attribute_option = input .attrs