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        if 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                if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
73                    args, ..
74                }) = &segment.arguments
75                {
76                    // extract inner generic (e.g. T from some::path::Result<T, E>)
77                    let arg = args
78                        .first()
79                        .expect("return type generic list shouldn't be empty");
80                    if let GenericArgument::Type(
81                        ty @ Type::Path(TypePath {
82                            path: Path { segments, .. },
83                            ..
84                        }),
85                    ) = arg
86                    {
87                        inner = (ty, segments);
88                    }
89                }
90            };
91
92            // inner type
93            if let Some(segments) = inner.1.last() {
94                if segments.ident == "Bytes" {
95                    return (inner.0, GenerateDocsFor::Ts);
96                }
97                // extract generics e.g. T from Json<T>
98                if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
99                    args, ..
100                }) = &segments.arguments
101                {
102                    if let Some(GenericArgument::Type(t)) = args.first() {
103                        return (t, GenerateDocsFor::JsonAndTs);
104                    }
105                }
106            }
107        }
108    }
109    panic!(
110        "return type was expected to be `Json<_>` or `SomeGenericType<Json<_>>`, but it was `{:#?}`",
111        item.sig.output
112    )
113}