diplomat_core/ast/
docs.rs

1use super::Path;
2use core::fmt;
3use quote::ToTokens;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use syn::parse::{self, Parse, ParseStream};
7use syn::{Attribute, Ident, Meta, Token};
8
9#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Default)]
10pub struct Docs(String, Vec<RustLink>);
11
12#[non_exhaustive]
13pub enum TypeReferenceSyntax {
14    SquareBrackets,
15    AtLink,
16}
17
18impl Docs {
19    pub fn from_attrs(attrs: &[Attribute]) -> Self {
20        Self(Self::get_doc_lines(attrs), Self::get_rust_link(attrs))
21    }
22
23    fn get_doc_lines(attrs: &[Attribute]) -> String {
24        let mut lines: String = String::new();
25
26        attrs.iter().for_each(|attr| {
27            if let Meta::NameValue(ref nv) = attr.meta {
28                if nv.path.is_ident("doc") {
29                    let node: syn::LitStr = syn::parse2(nv.value.to_token_stream()).unwrap();
30                    let line = node.value().trim().to_string();
31
32                    if !lines.is_empty() {
33                        lines.push('\n');
34                    }
35
36                    lines.push_str(&line);
37                }
38            }
39        });
40
41        lines
42    }
43
44    fn get_rust_link(attrs: &[Attribute]) -> Vec<RustLink> {
45        attrs
46            .iter()
47            .filter(|i| i.path().to_token_stream().to_string() == "diplomat :: rust_link")
48            .map(|i| i.parse_args().expect("Malformed attribute"))
49            .collect()
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.0.is_empty() && self.1.is_empty()
54    }
55
56    /// Convert to markdown
57    pub fn to_markdown(
58        &self,
59        ref_syntax: TypeReferenceSyntax,
60        docs_url_gen: &DocsUrlGenerator,
61    ) -> String {
62        use std::fmt::Write;
63        let mut lines = match ref_syntax {
64            TypeReferenceSyntax::SquareBrackets => self.0.replace("[`", "[").replace("`]", "]"),
65            TypeReferenceSyntax::AtLink => self.0.replace("[`", "{@link ").replace("`]", "}"),
66        };
67
68        let mut has_compact = false;
69        for rust_link in &self.1 {
70            if rust_link.display == RustLinkDisplay::Compact {
71                has_compact = true;
72            } else if rust_link.display == RustLinkDisplay::Normal {
73                if !lines.is_empty() {
74                    write!(lines, "\n\n").unwrap();
75                }
76                write!(
77                    lines,
78                    "See the [Rust documentation for `{name}`]({link}) for more information.",
79                    name = rust_link.path.elements.last().unwrap(),
80                    link = docs_url_gen.gen_for_rust_link(rust_link)
81                )
82                .unwrap();
83            }
84        }
85        if has_compact {
86            if !lines.is_empty() {
87                write!(lines, "\n\n").unwrap();
88            }
89            write!(lines, "Additional information: ").unwrap();
90            for (i, rust_link) in self
91                .1
92                .iter()
93                .filter(|r| r.display == RustLinkDisplay::Compact)
94                .enumerate()
95            {
96                if i != 0 {
97                    write!(lines, ", ").unwrap();
98                }
99                write!(
100                    lines,
101                    "[{}]({})",
102                    i + 1,
103                    docs_url_gen.gen_for_rust_link(rust_link)
104                )
105                .unwrap();
106            }
107        }
108        lines
109    }
110
111    pub fn rust_links(&self) -> &[RustLink] {
112        &self.1
113    }
114}
115
116#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
117#[non_exhaustive]
118pub enum RustLinkDisplay {
119    /// A nice expanded representation that includes the type name
120    ///
121    /// e.g. "See the \[link to Rust documentation\] for more details"
122    Normal,
123    /// A compact representation that will fit multiple rust_link entries in one line
124    ///
125    /// E.g. "For further information, see: 1, 2, 3, 4" (all links)
126    Compact,
127    /// Hidden. Useful for programmatically annotating an API as related without showing a link to the user
128    Hidden,
129}
130
131#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
132#[non_exhaustive]
133pub struct RustLink {
134    pub path: Path,
135    pub typ: DocType,
136    pub display: RustLinkDisplay,
137}
138
139impl Parse for RustLink {
140    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
141        let path = input.parse()?;
142        let path = Path::from_syn(&path);
143        let _comma: Token![,] = input.parse()?;
144        let ty_ident: Ident = input.parse()?;
145        let typ = match &*ty_ident.to_string() {
146            "Struct" => DocType::Struct,
147            "StructField" => DocType::StructField,
148            "Enum" => DocType::Enum,
149            "EnumVariant" => DocType::EnumVariant,
150            "EnumVariantField" => DocType::EnumVariantField,
151            "Trait" => DocType::Trait,
152            "FnInStruct" => DocType::FnInStruct,
153            "FnInTypedef" => DocType::FnInTypedef,
154            "FnInEnum" => DocType::FnInEnum,
155            "FnInTrait" => DocType::FnInTrait,
156            "DefaultFnInTrait" => DocType::DefaultFnInTrait,
157            "Fn" => DocType::Fn,
158            "Mod" => DocType::Mod,
159            "Constant" => DocType::Constant,
160            "AssociatedConstantInEnum" => DocType::AssociatedConstantInEnum,
161            "AssociatedConstantInTrait" => DocType::AssociatedConstantInTrait,
162            "AssociatedConstantInStruct" => DocType::AssociatedConstantInStruct,
163            "Macro" => DocType::Macro,
164            "AssociatedTypeInEnum" => DocType::AssociatedTypeInEnum,
165            "AssociatedTypeInTrait" => DocType::AssociatedTypeInTrait,
166            "AssociatedTypeInStruct" => DocType::AssociatedTypeInStruct,
167            "Typedef" => DocType::Typedef,
168            t => {
169                return Err(parse::Error::new(
170                    ty_ident.span(),
171                    format!("Unknown rust_link doc type {t:?}"),
172                ))
173            }
174        };
175        let lookahead = input.lookahead1();
176        let display = if lookahead.peek(Token![,]) {
177            let _comma: Token![,] = input.parse()?;
178            let display_ident: Ident = input.parse()?;
179            match &*display_ident.to_string() {
180                "normal" => RustLinkDisplay::Normal,
181                "compact" => RustLinkDisplay::Compact,
182                "hidden" => RustLinkDisplay::Hidden,
183                _ => return Err(parse::Error::new(display_ident.span(), "Unknown rust_link display style: Must be must be `normal`, `compact`, or `hidden`.")),
184            }
185        } else {
186            RustLinkDisplay::Normal
187        };
188        Ok(RustLink { path, typ, display })
189    }
190}
191impl fmt::Display for RustLink {
192    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
193        write!(f, "{}#{:?}", self.path, self.typ)
194    }
195}
196
197#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
198#[non_exhaustive]
199pub enum DocType {
200    Struct,
201    StructField,
202    Enum,
203    EnumVariant,
204    EnumVariantField,
205    Trait,
206    FnInStruct,
207    FnInTypedef,
208    FnInEnum,
209    FnInTrait,
210    DefaultFnInTrait,
211    Fn,
212    Mod,
213    Constant,
214    AssociatedConstantInEnum,
215    AssociatedConstantInTrait,
216    AssociatedConstantInStruct,
217    Macro,
218    AssociatedTypeInEnum,
219    AssociatedTypeInTrait,
220    AssociatedTypeInStruct,
221    Typedef,
222}
223
224#[derive(Default)]
225pub struct DocsUrlGenerator {
226    default_url: Option<String>,
227    base_urls: HashMap<String, String>,
228}
229
230impl DocsUrlGenerator {
231    pub fn with_base_urls(default_url: Option<String>, base_urls: HashMap<String, String>) -> Self {
232        Self {
233            default_url,
234            base_urls,
235        }
236    }
237
238    fn gen_for_rust_link(&self, rust_link: &RustLink) -> String {
239        use DocType::*;
240
241        let mut r = String::new();
242
243        let base = self
244            .base_urls
245            .get(rust_link.path.elements[0].as_str())
246            .map(String::as_str)
247            .or(self.default_url.as_deref())
248            .unwrap_or("https://docs.rs/");
249
250        r.push_str(base);
251        if !base.ends_with('/') {
252            r.push('/');
253        }
254        if r == "https://docs.rs/" {
255            r.push_str(rust_link.path.elements[0].as_str());
256            r.push_str("/latest/");
257        }
258
259        let mut elements = rust_link.path.elements.iter().peekable();
260
261        let module_depth = rust_link.path.elements.len()
262            - match rust_link.typ {
263                Mod => 0,
264                Struct | Enum | Trait | Fn | Macro | Constant | Typedef => 1,
265                FnInEnum
266                | FnInStruct
267                | FnInTypedef
268                | FnInTrait
269                | DefaultFnInTrait
270                | EnumVariant
271                | StructField
272                | AssociatedTypeInEnum
273                | AssociatedTypeInStruct
274                | AssociatedTypeInTrait
275                | AssociatedConstantInEnum
276                | AssociatedConstantInStruct
277                | AssociatedConstantInTrait => 2,
278                EnumVariantField => 3,
279            };
280
281        for _ in 0..module_depth {
282            r.push_str(elements.next().unwrap().as_str());
283            r.push('/');
284        }
285
286        if elements.peek().is_none() {
287            r.push_str("index.html");
288            return r;
289        }
290
291        r.push_str(match rust_link.typ {
292            Typedef | FnInTypedef => "type.",
293            Struct
294            | StructField
295            | FnInStruct
296            | AssociatedTypeInStruct
297            | AssociatedConstantInStruct => "struct.",
298            Enum
299            | EnumVariant
300            | EnumVariantField
301            | FnInEnum
302            | AssociatedTypeInEnum
303            | AssociatedConstantInEnum => "enum.",
304            Trait
305            | FnInTrait
306            | DefaultFnInTrait
307            | AssociatedTypeInTrait
308            | AssociatedConstantInTrait => "trait.",
309            Fn => "fn.",
310            Constant => "constant.",
311            Macro => "macro.",
312            Mod => unreachable!(),
313        });
314
315        r.push_str(elements.next().unwrap().as_str());
316
317        r.push_str(".html");
318
319        match rust_link.typ {
320            FnInStruct | FnInEnum | DefaultFnInTrait | FnInTypedef => {
321                r.push_str("#method.");
322                r.push_str(elements.next().unwrap().as_str());
323            }
324            AssociatedTypeInStruct | AssociatedTypeInEnum | AssociatedTypeInTrait => {
325                r.push_str("#associatedtype.");
326                r.push_str(elements.next().unwrap().as_str());
327            }
328            AssociatedConstantInStruct | AssociatedConstantInEnum | AssociatedConstantInTrait => {
329                r.push_str("#associatedconstant.");
330                r.push_str(elements.next().unwrap().as_str());
331            }
332            FnInTrait => {
333                r.push_str("#tymethod.");
334                r.push_str(elements.next().unwrap().as_str());
335            }
336            EnumVariant => {
337                r.push_str("#variant.");
338                r.push_str(elements.next().unwrap().as_str());
339            }
340            StructField => {
341                r.push_str("#structfield.");
342                r.push_str(elements.next().unwrap().as_str());
343            }
344            EnumVariantField => {
345                r.push_str("#variant.");
346                r.push_str(elements.next().unwrap().as_str());
347                r.push_str(".field.");
348                r.push_str(elements.next().unwrap().as_str());
349            }
350            Struct | Enum | Trait | Fn | Mod | Constant | Macro | Typedef => {}
351        }
352        r
353    }
354}
355
356#[test]
357fn test_docs_url_generator() {
358    let test_cases = [
359        (
360            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Struct)] },
361            "https://docs.rs/std/latest/std/foo/bar/struct.batz.html",
362        ),
363        (
364            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, StructField)] },
365            "https://docs.rs/std/latest/std/foo/struct.bar.html#structfield.batz",
366        ),
367        (
368            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Enum)] },
369            "https://docs.rs/std/latest/std/foo/bar/enum.batz.html",
370        ),
371        (
372            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariant)] },
373            "https://docs.rs/std/latest/std/foo/enum.bar.html#variant.batz",
374        ),
375        (
376            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariantField)] },
377            "https://docs.rs/std/latest/std/enum.foo.html#variant.bar.field.batz",
378        ),
379        (
380            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Trait)] },
381            "https://docs.rs/std/latest/std/foo/bar/trait.batz.html",
382        ),
383        (
384            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInStruct)] },
385            "https://docs.rs/std/latest/std/foo/struct.bar.html#method.batz",
386        ),
387        (
388            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInEnum)] },
389            "https://docs.rs/std/latest/std/foo/enum.bar.html#method.batz",
390        ),
391        (
392            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInTrait)] },
393            "https://docs.rs/std/latest/std/foo/trait.bar.html#tymethod.batz",
394        ),
395        (
396            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, DefaultFnInTrait)] },
397            "https://docs.rs/std/latest/std/foo/trait.bar.html#method.batz",
398        ),
399        (
400            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Fn)] },
401            "https://docs.rs/std/latest/std/foo/bar/fn.batz.html",
402        ),
403        (
404            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Mod)] },
405            "https://docs.rs/std/latest/std/foo/bar/batz/index.html",
406        ),
407        (
408            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Constant)] },
409            "https://docs.rs/std/latest/std/foo/bar/constant.batz.html",
410        ),
411        (
412            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Macro)] },
413            "https://docs.rs/std/latest/std/foo/bar/macro.batz.html",
414        ),
415    ];
416
417    for (attr, expected) in test_cases.clone() {
418        assert_eq!(
419            DocsUrlGenerator::default().gen_for_rust_link(&Docs::from_attrs(&[attr]).1[0]),
420            expected
421        );
422    }
423
424    assert_eq!(
425        DocsUrlGenerator::with_base_urls(
426            None,
427            [("std".to_string(), "http://std-docs.biz/".to_string())]
428                .into_iter()
429                .collect()
430        )
431        .gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
432        "http://std-docs.biz/std/foo/bar/struct.batz.html"
433    );
434
435    assert_eq!(
436        DocsUrlGenerator::with_base_urls(Some("http://std-docs.biz/".to_string()), HashMap::new())
437            .gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
438        "http://std-docs.biz/std/foo/bar/struct.batz.html"
439    );
440}