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