jsonschema_annotator/annotator/
toml.rs1use toml_edit::{DocumentMut, Item, Table};
2
3use super::{Annotator, AnnotatorConfig};
4use crate::error::{AnnotatorError, AnnotatorErrorKind, Error};
5use crate::schema::{Annotation, AnnotationMap};
6
7pub struct TomlAnnotator {
9 config: AnnotatorConfig,
10}
11
12impl TomlAnnotator {
13 pub fn new(config: AnnotatorConfig) -> Self {
14 Self { config }
15 }
16
17 fn format_comment(&self, annotation: &Annotation) -> Option<String> {
18 let mut lines = Vec::new();
19
20 if self.config.include_title {
21 if let Some(title) = &annotation.title {
22 lines.push(format!("# {}", title));
23 }
24 }
25
26 if self.config.include_description {
27 if let Some(desc) = &annotation.description {
28 let width = self.config.max_line_width.unwrap_or(78);
29 for line in textwrap::wrap(desc, width.saturating_sub(2)) {
30 lines.push(format!("# {}", line));
31 }
32 }
33 }
34
35 if lines.is_empty() {
36 None
37 } else {
38 Some(lines.join("\n") + "\n")
40 }
41 }
42
43 fn annotate_table(
44 &self,
45 table: &mut Table,
46 path: &[String],
47 annotations: &AnnotationMap,
48 ) {
49 let keys: Vec<String> = table.iter().map(|(k, _)| (*k).to_string()).collect();
52
53 for key_string in keys {
54 let mut current_path = path.to_vec();
55 current_path.push(key_string.clone());
56 let path_string = current_path.join(".");
57
58 if let Some((mut key, item)) = table.get_key_value_mut(&key_string) {
60 match item {
62 Item::Table(nested) => {
63 if let Some(ann) = annotations.get(&path_string) {
65 if let Some(comment) = self.format_comment(ann) {
66 let decor = nested.decor_mut();
67 if self.config.preserve_existing {
68 let existing = decor.prefix().map(|s| s.as_str().unwrap_or("")).unwrap_or("");
69 if !existing.is_empty() {
70 decor.set_prefix(format!("{}{}", comment, existing));
71 } else {
72 decor.set_prefix(comment);
73 }
74 } else {
75 decor.set_prefix(comment);
76 }
77 }
78 }
79 self.annotate_table(nested, ¤t_path, annotations);
81 }
82 Item::Value(toml_edit::Value::InlineTable(_)) => {
83 }
85 _ => {
86 if let Some(ann) = annotations.get(&path_string) {
88 if let Some(comment) = self.format_comment(ann) {
89 let decor = key.leaf_decor_mut();
90 if self.config.preserve_existing {
91 let existing = decor.prefix().map(|s| s.as_str().unwrap_or("")).unwrap_or("");
92 if !existing.is_empty() {
93 decor.set_prefix(format!("{}{}", comment, existing));
94 } else {
95 decor.set_prefix(comment);
96 }
97 } else {
98 decor.set_prefix(comment);
99 }
100 }
101 }
102 }
103 }
104 }
105 }
106 }
107}
108
109impl Annotator for TomlAnnotator {
110 fn annotate(
111 &self,
112 content: &str,
113 annotations: &AnnotationMap,
114 ) -> Result<String, AnnotatorError> {
115 let mut doc: DocumentMut = content
116 .parse()
117 .map_err(|e| Error::new(AnnotatorErrorKind::Parse).with_source(e))?;
118
119 self.annotate_table(doc.as_table_mut(), &Vec::new(), annotations);
120
121 Ok(doc.to_string())
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::schema::Annotation;
129 use insta::assert_snapshot;
130
131 fn make_annotations(items: &[(&str, Option<&str>, Option<&str>)]) -> AnnotationMap {
132 let mut map = AnnotationMap::new();
133 for (path, title, desc) in items {
134 let mut ann = Annotation::new(*path);
135 if let Some(t) = title {
136 ann = ann.with_title(*t);
137 }
138 if let Some(d) = desc {
139 ann = ann.with_description(*d);
140 }
141 map.insert(ann);
142 }
143 map
144 }
145
146 #[test]
147 fn test_simple_annotation() {
148 let content = "port = 8080\n";
149 let annotations = make_annotations(&[("port", Some("Port"), Some("Server port number"))]);
150
151 let annotator = TomlAnnotator::new(AnnotatorConfig::default());
152 let result = annotator.annotate(content, &annotations).unwrap();
153
154 assert_snapshot!(result);
155 }
156
157 #[test]
158 fn test_nested_table() {
159 let content = r#"[server]
160port = 8080
161host = "localhost"
162"#;
163 let annotations = make_annotations(&[
164 ("server", Some("Server Config"), None),
165 ("server.port", Some("Port"), Some("The port to listen on")),
166 ("server.host", Some("Host"), None),
167 ]);
168
169 let annotator = TomlAnnotator::new(AnnotatorConfig::default());
170 let result = annotator.annotate(content, &annotations).unwrap();
171
172 assert_snapshot!(result);
173 }
174
175 #[test]
176 fn test_title_only() {
177 let content = "name = \"test\"\n";
178 let annotations = make_annotations(&[("name", Some("Name"), Some("Full description"))]);
179
180 let annotator = TomlAnnotator::new(AnnotatorConfig::titles_only());
181 let result = annotator.annotate(content, &annotations).unwrap();
182
183 assert_snapshot!(result);
184 }
185
186 #[test]
187 fn test_description_only() {
188 let content = "name = \"test\"\n";
189 let annotations = make_annotations(&[("name", Some("Name"), Some("Full description"))]);
190
191 let annotator = TomlAnnotator::new(AnnotatorConfig::descriptions_only());
192 let result = annotator.annotate(content, &annotations).unwrap();
193
194 assert_snapshot!(result);
195 }
196
197 #[test]
198 fn test_preserve_existing_comments() {
199 let content = "# Existing comment\nport = 8080\n";
200 let annotations = make_annotations(&[("port", Some("Port"), None)]);
201
202 let annotator = TomlAnnotator::new(AnnotatorConfig::default());
203 let result = annotator.annotate(content, &annotations).unwrap();
204
205 assert_snapshot!(result);
206 }
207
208 #[test]
209 fn test_no_matching_annotations() {
210 let content = "name = \"test\"\nage = 30\n";
211 let annotations = make_annotations(&[("other", Some("Other"), None)]);
212
213 let annotator = TomlAnnotator::new(AnnotatorConfig::default());
214 let result = annotator.annotate(content, &annotations).unwrap();
215
216 assert_snapshot!(result);
217 }
218
219 #[test]
220 fn test_deeply_nested() {
221 let content = r#"[database]
222[database.connection]
223host = "localhost"
224port = 5432
225"#;
226 let annotations = make_annotations(&[
227 ("database", Some("Database"), None),
228 ("database.connection", Some("Connection Settings"), None),
229 ("database.connection.host", Some("Host"), Some("Database server hostname")),
230 ("database.connection.port", Some("Port"), None),
231 ]);
232
233 let annotator = TomlAnnotator::new(AnnotatorConfig::default());
234 let result = annotator.annotate(content, &annotations).unwrap();
235
236 assert_snapshot!(result);
237 }
238
239 #[test]
240 fn test_long_description_wrapping() {
241 let content = "name = \"test\"\n";
242 let long_desc = "This is a very long description that should be wrapped across multiple lines when the max line width is set to a reasonable value";
243 let annotations = make_annotations(&[("name", None, Some(long_desc))]);
244
245 let mut config = AnnotatorConfig::default();
246 config.max_line_width = Some(40);
247 let annotator = TomlAnnotator::new(config);
248 let result = annotator.annotate(content, &annotations).unwrap();
249
250 assert_snapshot!(result);
251 }
252}