doc_macro/
generated_doc.rs

1use proc_macro::TokenStream;
2use quote::ToTokens;
3use syn::{
4    AngleBracketedGenericArguments, Attribute, Expr, GenericArgument, ItemFn, Path, PathArguments,
5    ReturnType, Type, TypePath,
6};
7
8pub fn generated_doc_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
9    let mut stream = TokenStream::new();
10    stream.extend(
11        format!("#[cfg_attr(all(doc, not(doctest)), generated_doc_inner({attr}))]")
12            .parse::<TokenStream>()
13            .unwrap(),
14    );
15    stream.extend(item);
16    stream
17}
18
19pub fn generated_doc_inner_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
20    let mut item = syn::parse_macro_input!(item as ItemFn);
21
22    let storage;
23    let (arg, generate_docs_for) = if attr.is_empty() {
24        process_return_type(&item)
25    } else {
26        storage = syn::parse_macro_input!(attr as Type);
27        (&storage, GenerateDocsFor::JsonAndTs)
28    };
29    let windows_safe_name = arg
30        .to_token_stream()
31        .to_string()
32        .replace('<', "(")
33        .replace('>', ")");
34    let expr: Expr = syn::parse_str(&windows_safe_name).unwrap_or_else(|err| {
35        panic!("Failed to parse windows safe name {windows_safe_name}: {err}")
36    });
37    let attr: Attribute = match generate_docs_for {
38        GenerateDocsFor::Ts => syn::parse_quote!(#[doc = generated_docs!(#expr, ts)]),
39        GenerateDocsFor::JsonAndTs => {
40            syn::parse_quote!(#[doc = generated_docs!(#expr)])
41        }
42    };
43
44    item.attrs.push(attr);
45
46    item.into_token_stream().into()
47}
48
49#[derive(Clone, Copy)]
50enum GenerateDocsFor {
51    Ts,
52    JsonAndTs,
53}
54
55fn process_return_type(item: &ItemFn) -> (&Type, GenerateDocsFor) {
56    // should have a path return type
57    if let ReturnType::Type(_, ty) = &item.sig.output
58        && let Type::Path(TypePath {
59            path: Path { segments, .. },
60            ..
61        }) = ty.as_ref()
62    {
63        // this is probably ControllerResult<web::Json<T>>
64        let segment = segments
65            .last()
66            .expect("return type path shouldn't be empty");
67        // this will probably contain web::Json<T> or Bytes, both the type and the inner segments for convenience
68        let mut inner = (ty.as_ref(), segments);
69
70        // extract inner segments from non-Json types, use as is otherwise
71        if segment.ident != "Json"
72            && let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
73                &segment.arguments
74        {
75            // extract inner generic (e.g. T from some::path::Result<T, E>)
76            let arg = args
77                .first()
78                .expect("return type generic list shouldn't be empty");
79            if let GenericArgument::Type(
80                ty @ Type::Path(TypePath {
81                    path: Path { segments, .. },
82                    ..
83                }),
84            ) = arg
85            {
86                inner = (ty, segments);
87            }
88        };
89
90        // inner type
91        if let Some(segments) = inner.1.last() {
92            if segments.ident == "Bytes" {
93                return (inner.0, GenerateDocsFor::Ts);
94            }
95            // extract generics e.g. T from Json<T>
96            if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
97                &segments.arguments
98                && let Some(GenericArgument::Type(t)) = args.first()
99            {
100                return (t, GenerateDocsFor::JsonAndTs);
101            }
102        }
103    }
104    panic!(
105        "return type was expected to be `Json<_>` or `SomeGenericType<Json<_>>`, but it was `{:#?}`",
106        item.sig.output
107    )
108}