1use once_cell::sync::Lazy;
2use prost_types::source_code_info::Location;
3use regex::Regex;
4
5#[derive(Debug)]
7pub struct Comments {
8 pub leading_detached: Vec<Vec<String>>,
10
11 pub leading: Vec<String>,
13
14 pub trailing: Vec<String>,
16}
17
18impl Comments {
19 pub(crate) fn from_location(location: &Location) -> Comments {
20 fn get_lines<S>(comments: S) -> Vec<String>
21 where
22 S: AsRef<str>,
23 {
24 comments.as_ref().lines().map(str::to_owned).collect()
25 }
26
27 let leading_detached = location
28 .leading_detached_comments
29 .iter()
30 .map(get_lines)
31 .collect();
32 let leading = location
33 .leading_comments
34 .as_ref()
35 .map_or(Vec::new(), get_lines);
36 let trailing = location
37 .trailing_comments
38 .as_ref()
39 .map_or(Vec::new(), get_lines);
40 Comments {
41 leading_detached,
42 leading,
43 trailing,
44 }
45 }
46
47 pub fn append_with_indent(&self, indent_level: u8, buf: &mut String) {
51 for detached_block in &self.leading_detached {
53 for line in detached_block {
54 for _ in 0..indent_level {
55 buf.push_str(" ");
56 }
57 buf.push_str("//");
58 buf.push_str(&Self::sanitize_line(line));
59 buf.push('\n');
60 }
61 buf.push('\n');
62 }
63
64 for line in &self.leading {
66 for _ in 0..indent_level {
67 buf.push_str(" ");
68 }
69 buf.push_str("///");
70 buf.push_str(&Self::sanitize_line(line));
71 buf.push('\n');
72 }
73
74 if !self.leading.is_empty() && !self.trailing.is_empty() {
76 for _ in 0..indent_level {
77 buf.push_str(" ");
78 }
79 buf.push_str("///\n");
80 }
81
82 for line in &self.trailing {
84 for _ in 0..indent_level {
85 buf.push_str(" ");
86 }
87 buf.push_str("///");
88 buf.push_str(&Self::sanitize_line(line));
89 buf.push('\n');
90 }
91 }
92
93 fn sanitize_line(line: &str) -> String {
97 static RULE_URL: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://[^\s)]+").unwrap());
98 static RULE_BRACKETS: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\[)(\S+)(])").unwrap());
99
100 let mut s = RULE_URL.replace_all(line, r"<$0>").to_string();
101 s = RULE_BRACKETS.replace_all(&s, r"\$1$2\$3").to_string();
102 if !s.is_empty() {
103 s.insert(0, ' ');
104 }
105 s
106 }
107}
108
109#[derive(Debug)]
111pub struct Service {
112 pub name: String,
114 pub proto_name: String,
116 pub package: String,
118 pub comments: Comments,
120 pub methods: Vec<Method>,
122 pub options: prost_types::ServiceOptions,
124}
125
126#[derive(Debug)]
128pub struct Method {
129 pub name: String,
131 pub proto_name: String,
133 pub comments: Comments,
135 pub input_type: String,
137 pub output_type: String,
139 pub input_proto_type: String,
141 pub output_proto_type: String,
143 pub options: prost_types::MethodOptions,
145 pub client_streaming: bool,
147 pub server_streaming: bool,
149 pub input_type_extern: bool,
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_comment_append_with_indent_sanitizes_comment_doc_url() {
159 struct TestCases {
160 name: &'static str,
161 input: String,
162 expected: String,
163 }
164
165 let tests = vec![
166 TestCases {
167 name: "valid_http",
168 input: "See https://www.rust-lang.org/".to_string(),
169 expected: "/// See <https://www.rust-lang.org/>\n".to_string(),
170 },
171 TestCases {
172 name: "valid_https",
173 input: "See https://www.rust-lang.org/".to_string(),
174 expected: "/// See <https://www.rust-lang.org/>\n".to_string(),
175 },
176 TestCases {
177 name: "valid_https_parenthesis",
178 input: "See (https://www.rust-lang.org/)".to_string(),
179 expected: "/// See (<https://www.rust-lang.org/>)\n".to_string(),
180 },
181 TestCases {
182 name: "invalid",
183 input: "See note://abc".to_string(),
184 expected: "/// See note://abc\n".to_string(),
185 },
186 ];
187 for t in tests {
188 let input = Comments {
189 leading_detached: vec![],
190 leading: vec![],
191 trailing: vec![t.input],
192 };
193
194 let mut actual = "".to_string();
195 input.append_with_indent(0, &mut actual);
196
197 assert_eq!(t.expected, actual, "failed {}", t.name);
198 }
199 }
200
201 #[test]
202 fn test_comment_append_with_indent_sanitizes_square_brackets() {
203 struct TestCases {
204 name: &'static str,
205 input: String,
206 expected: String,
207 }
208
209 let tests = vec![
210 TestCases {
211 name: "valid_brackets",
212 input: "foo [bar] baz".to_string(),
213 expected: "/// foo \\[bar\\] baz\n".to_string(),
214 },
215 TestCases {
216 name: "invalid_start_bracket",
217 input: "foo [= baz".to_string(),
218 expected: "/// foo [= baz\n".to_string(),
219 },
220 TestCases {
221 name: "invalid_end_bracket",
222 input: "foo =] baz".to_string(),
223 expected: "/// foo =] baz\n".to_string(),
224 },
225 TestCases {
226 name: "invalid_bracket_combination",
227 input: "[0, 9)".to_string(),
228 expected: "/// [0, 9)\n".to_string(),
229 },
230 ];
231 for t in tests {
232 let input = Comments {
233 leading_detached: vec![],
234 leading: vec![],
235 trailing: vec![t.input],
236 };
237
238 let mut actual = "".to_string();
239 input.append_with_indent(0, &mut actual);
240
241 assert_eq!(t.expected, actual, "failed {}", t.name);
242 }
243 }
244
245 #[test]
246 fn test_codeblocks() {
247 struct TestCase {
248 name: &'static str,
249 input: &'static str,
250 #[allow(unused)]
251 cleanedup_expected: Vec<&'static str>,
252 }
253
254 let tests = vec![
255 TestCase {
256 name: "unlabelled_block",
257 input: " thingy\n",
258 cleanedup_expected: vec!["", "```text", "thingy", "```"],
259 },
260 TestCase {
261 name: "rust_block",
262 input: "```rust\nfoo.bar()\n```\n",
263 cleanedup_expected: vec!["", "```compile_fail", "foo.bar()", "```"],
264 },
265 TestCase {
266 name: "js_block",
267 input: "```javascript\nfoo.bar()\n```\n",
268 cleanedup_expected: vec!["", "```text,javascript", "foo.bar()", "```"],
269 },
270 ];
271
272 for t in tests {
273 let loc = Location {
274 path: vec![],
275 span: vec![],
276 leading_comments: Some(t.input.into()),
277 trailing_comments: None,
278 leading_detached_comments: vec![],
279 };
280 let comments = Comments::from_location(&loc);
281 #[cfg(feature = "cleanup-markdown")]
282 let expected = t.cleanedup_expected;
283 #[cfg(not(feature = "cleanup-markdown"))]
284 let expected: Vec<&str> = t.input.lines().collect();
285 assert_eq!(expected, comments.leading, "failed {}", t.name);
286 }
287 }
288}