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