Skip to main content

ntex_prost_build/
ast.rs

1use once_cell::sync::Lazy;
2use prost_types::source_code_info::Location;
3use regex::Regex;
4
5/// Comments on a Protobuf item.
6#[derive(Debug)]
7pub struct Comments {
8    /// Leading detached blocks of comments.
9    pub leading_detached: Vec<Vec<String>>,
10
11    /// Leading comments.
12    pub leading: Vec<String>,
13
14    /// Trailing comments.
15    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    /// Appends the comments to a buffer with indentation.
48    ///
49    /// Each level of indentation corresponds to four space (' ') characters.
50    pub fn append_with_indent(&self, indent_level: u8, buf: &mut String) {
51        // Append blocks of detached comments.
52        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        // Append leading comments.
65        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        // Append an empty comment line if there are leading and trailing comments.
75        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        // Append trailing comments.
83        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    /// Sanitizes the line for rustdoc by performing the following operations:
94    ///     - escape urls as <http://foo.com>
95    ///     - escape `[` & `]`
96    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/// A service descriptor.
110#[derive(Debug)]
111pub struct Service {
112    /// The service name in Rust style.
113    pub name: String,
114    /// The service name as it appears in the .proto file.
115    pub proto_name: String,
116    /// The package name as it appears in the .proto file.
117    pub package: String,
118    /// The service comments.
119    pub comments: Comments,
120    /// The service methods.
121    pub methods: Vec<Method>,
122    /// The service options.
123    pub options: prost_types::ServiceOptions,
124}
125
126/// A service method descriptor.
127#[derive(Debug)]
128pub struct Method {
129    /// The name of the method in Rust style.
130    pub name: String,
131    /// The name of the method as it appears in the .proto file.
132    pub proto_name: String,
133    /// The method comments.
134    pub comments: Comments,
135    /// The input Rust type.
136    pub input_type: String,
137    /// The output Rust type.
138    pub output_type: String,
139    /// The input Protobuf type.
140    pub input_proto_type: String,
141    /// The output Protobuf type.
142    pub output_proto_type: String,
143    /// The method options.
144    pub options: prost_types::MethodOptions,
145    /// Identifies if client streams multiple client messages.
146    pub client_streaming: bool,
147    /// Identifies if server streams multiple server messages.
148    pub server_streaming: bool,
149    /// Identifies if input type is external type
150    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}