1use super::config::ToolDefinition;
7use super::processor::RUMDL_BUILTIN_TOOL;
8use std::collections::BTreeMap;
9use std::collections::HashMap;
10use std::sync::LazyLock;
11
12pub struct ToolRegistry {
14 user_tools: BTreeMap<String, ToolDefinition>,
16}
17
18impl ToolRegistry {
19 pub fn new(user_tools: BTreeMap<String, ToolDefinition>) -> Self {
21 Self { user_tools }
22 }
23
24 pub fn get(&self, tool_id: &str) -> Option<&ToolDefinition> {
28 self.user_tools.get(tool_id).or_else(|| BUILTIN_TOOLS.get(tool_id))
29 }
30
31 pub fn contains(&self, tool_id: &str) -> bool {
33 self.user_tools.contains_key(tool_id) || BUILTIN_TOOLS.contains_key(tool_id)
34 }
35
36 pub fn list_tools(&self) -> Vec<&str> {
38 let mut tools: Vec<&str> = self.user_tools.keys().map(std::string::String::as_str).collect();
39 for key in BUILTIN_TOOLS.keys() {
40 if !self.user_tools.contains_key(*key) {
41 tools.push(key);
42 }
43 }
44 tools.sort_unstable();
45 tools
46 }
47}
48
49pub fn builtin_tool_ids() -> Vec<&'static str> {
56 let mut ids: Vec<&'static str> = BUILTIN_TOOLS.keys().copied().collect();
57 ids.sort_unstable();
58 ids
59}
60
61impl Default for ToolRegistry {
62 fn default() -> Self {
63 Self::new(BTreeMap::new())
64 }
65}
66
67static BUILTIN_TOOLS: LazyLock<HashMap<&'static str, ToolDefinition>> = LazyLock::new(|| {
71 let mut m = HashMap::new();
72
73 m.insert(
75 "ruff:check",
76 ToolDefinition {
77 command: vec![
78 "ruff".to_string(),
79 "check".to_string(),
80 "--output-format=concise".to_string(),
81 "--stdin-filename=_.py".to_string(),
82 "-".to_string(),
83 ],
84 stdin: true,
85 stdout: true,
86 lint_args: vec![],
87 format_args: vec![],
88 },
89 );
90
91 m.insert(
92 "ruff:format",
93 ToolDefinition {
94 command: vec![
95 "ruff".to_string(),
96 "format".to_string(),
97 "--stdin-filename=_.py".to_string(),
98 "-".to_string(),
99 ],
100 stdin: true,
101 stdout: true,
102 lint_args: vec![],
103 format_args: vec![],
104 },
105 );
106
107 m.insert(
109 "black",
110 ToolDefinition {
111 command: vec!["black".to_string(), "--quiet".to_string(), "-".to_string()],
112 stdin: true,
113 stdout: true,
114 lint_args: vec!["--check".to_string()],
115 format_args: vec![],
116 },
117 );
118
119 m.insert(
121 "prettier",
122 ToolDefinition {
123 command: vec!["prettier".to_string(), "--stdin-filepath=_.js".to_string()],
124 stdin: true,
125 stdout: true,
126 lint_args: vec!["--check".to_string()],
127 format_args: vec![],
128 },
129 );
130
131 m.insert(
132 "prettier:json",
133 ToolDefinition {
134 command: vec!["prettier".to_string(), "--stdin-filepath=_.json".to_string()],
135 stdin: true,
136 stdout: true,
137 lint_args: vec!["--check".to_string()],
138 format_args: vec![],
139 },
140 );
141
142 m.insert(
143 "prettier:yaml",
144 ToolDefinition {
145 command: vec!["prettier".to_string(), "--stdin-filepath=_.yaml".to_string()],
146 stdin: true,
147 stdout: true,
148 lint_args: vec!["--check".to_string()],
149 format_args: vec![],
150 },
151 );
152
153 m.insert(
154 "prettier:html",
155 ToolDefinition {
156 command: vec!["prettier".to_string(), "--stdin-filepath=_.html".to_string()],
157 stdin: true,
158 stdout: true,
159 lint_args: vec!["--check".to_string()],
160 format_args: vec![],
161 },
162 );
163
164 m.insert(
165 "prettier:css",
166 ToolDefinition {
167 command: vec!["prettier".to_string(), "--stdin-filepath=_.css".to_string()],
168 stdin: true,
169 stdout: true,
170 lint_args: vec!["--check".to_string()],
171 format_args: vec![],
172 },
173 );
174
175 m.insert(
176 "prettier:markdown",
177 ToolDefinition {
178 command: vec!["prettier".to_string(), "--stdin-filepath=_.md".to_string()],
179 stdin: true,
180 stdout: true,
181 lint_args: vec!["--check".to_string()],
182 format_args: vec![],
183 },
184 );
185
186 m.insert(
191 "shellcheck",
192 ToolDefinition {
193 command: vec!["shellcheck".to_string(), "--shell=bash".to_string(), "-".to_string()],
194 stdin: true,
195 stdout: true,
196 lint_args: vec![],
197 format_args: vec![],
198 },
199 );
200
201 m.insert(
203 "shfmt",
204 ToolDefinition {
205 command: vec!["shfmt".to_string()],
206 stdin: true,
207 stdout: true,
208 lint_args: vec!["-d".to_string()], format_args: vec![],
210 },
211 );
212
213 m.insert(
219 "shuck",
220 ToolDefinition {
221 command: vec![
222 "shuck".to_string(),
223 "check".to_string(),
224 "--output-format".to_string(),
225 "concise".to_string(),
226 "-".to_string(),
227 ],
228 stdin: true,
229 stdout: true,
230 lint_args: vec![],
231 format_args: vec![],
232 },
233 );
234
235 m.insert(
237 "rustfmt",
238 ToolDefinition {
239 command: vec!["rustfmt".to_string()],
240 stdin: true,
241 stdout: true,
242 lint_args: vec!["--check".to_string()],
243 format_args: vec![],
244 },
245 );
246
247 m.insert(
249 "gofmt",
250 ToolDefinition {
251 command: vec!["gofmt".to_string()],
252 stdin: true,
253 stdout: true,
254 lint_args: vec!["-d".to_string()], format_args: vec![],
256 },
257 );
258
259 m.insert(
261 "goimports",
262 ToolDefinition {
263 command: vec!["goimports".to_string()],
264 stdin: true,
265 stdout: true,
266 lint_args: vec!["-d".to_string()],
267 format_args: vec![],
268 },
269 );
270
271 m.insert(
273 "clang-format",
274 ToolDefinition {
275 command: vec!["clang-format".to_string()],
276 stdin: true,
277 stdout: true,
278 lint_args: vec!["--dry-run".to_string(), "--Werror".to_string()],
279 format_args: vec![],
280 },
281 );
282
283 m.insert(
286 "sqlfluff:lint",
287 ToolDefinition {
288 command: vec![
289 "sqlfluff".to_string(),
290 "lint".to_string(),
291 "--dialect".to_string(),
292 "ansi".to_string(),
293 "-".to_string(),
294 ],
295 stdin: true,
296 stdout: true,
297 lint_args: vec![],
298 format_args: vec![],
299 },
300 );
301
302 m.insert(
303 "sqlfluff:fix",
304 ToolDefinition {
305 command: vec![
306 "sqlfluff".to_string(),
307 "fix".to_string(),
308 "--dialect".to_string(),
309 "ansi".to_string(),
310 "-".to_string(),
311 ],
312 stdin: true,
313 stdout: true,
314 lint_args: vec![],
315 format_args: vec![],
316 },
317 );
318
319 m.insert(
321 "jq",
322 ToolDefinition {
323 command: vec!["jq".to_string(), ".".to_string()],
324 stdin: true,
325 stdout: true,
326 lint_args: vec![],
327 format_args: vec![],
328 },
329 );
330
331 m.insert(
333 "yamlfmt",
334 ToolDefinition {
335 command: vec!["yamlfmt".to_string()],
336 stdin: true,
337 stdout: true,
338 lint_args: vec!["-lint".to_string(), "-".to_string()],
339 format_args: vec!["-".to_string()],
340 },
341 );
342
343 m.insert(
345 "taplo",
346 ToolDefinition {
347 command: vec!["taplo".to_string(), "fmt".to_string(), "-".to_string()],
348 stdin: true,
349 stdout: true,
350 lint_args: vec!["--check".to_string()],
351 format_args: vec![],
352 },
353 );
354
355 m.insert(
357 "terraform-fmt",
358 ToolDefinition {
359 command: vec!["terraform".to_string(), "fmt".to_string(), "-".to_string()],
360 stdin: true,
361 stdout: true,
362 lint_args: vec!["-check".to_string()],
363 format_args: vec![],
364 },
365 );
366
367 m.insert(
369 "nixfmt",
370 ToolDefinition {
371 command: vec!["nixfmt".to_string(), "-".to_string()],
372 stdin: true,
373 stdout: true,
374 lint_args: vec!["--check".to_string()],
375 format_args: vec![],
376 },
377 );
378
379 m.insert(
381 "stylua",
382 ToolDefinition {
383 command: vec!["stylua".to_string(), "-".to_string()],
384 stdin: true,
385 stdout: true,
386 lint_args: vec!["--check".to_string()],
387 format_args: vec![],
388 },
389 );
390
391 m.insert(
393 "ormolu",
394 ToolDefinition {
395 command: vec!["ormolu".to_string(), "--stdin-input-file=_.hs".to_string()],
396 stdin: true,
397 stdout: true,
398 lint_args: vec!["--check-idempotence".to_string()],
399 format_args: vec![],
400 },
401 );
402
403 m.insert(
405 "elm-format",
406 ToolDefinition {
407 command: vec!["elm-format".to_string(), "--stdin".to_string()],
408 stdin: true,
409 stdout: true,
410 lint_args: vec!["--validate".to_string()],
411 format_args: vec![],
412 },
413 );
414
415 m.insert(
417 "swift-format",
418 ToolDefinition {
419 command: vec!["swift-format".to_string(), "format".to_string(), "-".to_string()],
420 stdin: true,
421 stdout: true,
422 lint_args: vec![],
423 format_args: vec![],
424 },
425 );
426
427 m.insert(
429 "ktfmt",
430 ToolDefinition {
431 command: vec!["ktfmt".to_string(), "-".to_string()],
432 stdin: true,
433 stdout: true,
434 lint_args: vec![],
435 format_args: vec![],
436 },
437 );
438
439 m.insert(
441 "djlint",
442 ToolDefinition {
443 command: vec!["djlint".to_string(), "-".to_string()],
444 stdin: true,
445 stdout: true,
446 lint_args: vec![],
447 format_args: vec!["--reformat".to_string()],
448 },
449 );
450
451 m.insert(
452 "djlint:lint",
453 ToolDefinition {
454 command: vec!["djlint".to_string(), "-".to_string()],
455 stdin: true,
456 stdout: true,
457 lint_args: vec![],
458 format_args: vec![],
459 },
460 );
461
462 m.insert(
463 "djlint:reformat",
464 ToolDefinition {
465 command: vec!["djlint".to_string(), "-".to_string(), "--reformat".to_string()],
466 stdin: true,
467 stdout: true,
468 lint_args: vec![],
469 format_args: vec![],
470 },
471 );
472
473 m.insert(
475 "beautysh",
476 ToolDefinition {
477 command: vec!["beautysh".to_string(), "-".to_string()],
478 stdin: true,
479 stdout: true,
480 lint_args: vec!["--check".to_string()],
481 format_args: vec![],
482 },
483 );
484
485 m.insert(
487 "tombi",
488 ToolDefinition {
489 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
490 stdin: true,
491 stdout: true,
492 lint_args: vec![],
493 format_args: vec![],
494 },
495 );
496
497 m.insert(
498 "tombi:format",
499 ToolDefinition {
500 command: vec!["tombi".to_string(), "format".to_string(), "-".to_string()],
501 stdin: true,
502 stdout: true,
503 lint_args: vec![],
504 format_args: vec![],
505 },
506 );
507
508 m.insert(
509 "tombi:lint",
510 ToolDefinition {
511 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
512 stdin: true,
513 stdout: true,
514 lint_args: vec![],
515 format_args: vec![],
516 },
517 );
518
519 m.insert(
521 "oxfmt",
522 ToolDefinition {
523 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
524 stdin: true,
525 stdout: true,
526 lint_args: vec!["--check".to_string()],
527 format_args: vec![],
528 },
529 );
530
531 m.insert(
532 "oxfmt:js",
533 ToolDefinition {
534 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
535 stdin: true,
536 stdout: true,
537 lint_args: vec!["--check".to_string()],
538 format_args: vec![],
539 },
540 );
541
542 m.insert(
543 "oxfmt:ts",
544 ToolDefinition {
545 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.ts".to_string()],
546 stdin: true,
547 stdout: true,
548 lint_args: vec!["--check".to_string()],
549 format_args: vec![],
550 },
551 );
552
553 m.insert(
554 "oxfmt:jsx",
555 ToolDefinition {
556 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.jsx".to_string()],
557 stdin: true,
558 stdout: true,
559 lint_args: vec!["--check".to_string()],
560 format_args: vec![],
561 },
562 );
563
564 m.insert(
565 "oxfmt:tsx",
566 ToolDefinition {
567 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.tsx".to_string()],
568 stdin: true,
569 stdout: true,
570 lint_args: vec!["--check".to_string()],
571 format_args: vec![],
572 },
573 );
574
575 m.insert(
576 "oxfmt:json",
577 ToolDefinition {
578 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.json".to_string()],
579 stdin: true,
580 stdout: true,
581 lint_args: vec!["--check".to_string()],
582 format_args: vec![],
583 },
584 );
585
586 m.insert(
587 "oxfmt:css",
588 ToolDefinition {
589 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.css".to_string()],
590 stdin: true,
591 stdout: true,
592 lint_args: vec!["--check".to_string()],
593 format_args: vec![],
594 },
595 );
596
597 let deno_fmt = |ext: &str| ToolDefinition {
600 command: vec![
601 "deno".to_string(),
602 "fmt".to_string(),
603 format!("--ext={ext}"),
604 "-".to_string(),
605 ],
606 stdin: true,
607 stdout: true,
608 lint_args: vec!["--check".to_string()],
609 format_args: vec![],
610 };
611 m.insert("deno-fmt", deno_fmt("ts"));
612 m.insert("deno-fmt:ts", deno_fmt("ts"));
613 m.insert("deno-fmt:js", deno_fmt("js"));
614 m.insert("deno-fmt:json", deno_fmt("json"));
615 m.insert("deno-fmt:jsonc", deno_fmt("jsonc"));
616 m.insert("deno-fmt:md", deno_fmt("md"));
617
618 m
619});
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623enum ToolKind {
624 Lint,
625 Format,
626 Both,
627}
628
629impl ToolKind {
630 const fn label(self) -> &'static str {
631 match self {
632 ToolKind::Lint => "Lint",
633 ToolKind::Format => "Format",
634 ToolKind::Both => "Both",
635 }
636 }
637}
638
639struct ToolDocMeta {
653 id: &'static str,
654 language: &'static str,
655 kind: ToolKind,
656 doc_group: &'static str,
658 display_command: Option<&'static str>,
660 runtime: bool,
662}
663
664const BUILTIN_TOOLS_DOCS: &[ToolDocMeta] = &[
666 ToolDocMeta {
667 id: "ruff:check",
668 language: "Python",
669 kind: ToolKind::Lint,
670 doc_group: "ruff:check",
671 display_command: Some("ruff check --output-format=concise -"),
672 runtime: true,
673 },
674 ToolDocMeta {
675 id: "ruff:format",
676 language: "Python",
677 kind: ToolKind::Format,
678 doc_group: "ruff:format",
679 display_command: Some("ruff format -"),
680 runtime: true,
681 },
682 ToolDocMeta {
683 id: "black",
684 language: "Python",
685 kind: ToolKind::Format,
686 doc_group: "black",
687 display_command: None,
688 runtime: true,
689 },
690 ToolDocMeta {
691 id: "prettier",
692 language: "Multi",
693 kind: ToolKind::Format,
694 doc_group: "prettier",
695 display_command: Some("prettier --stdin-filepath=_.EXT"),
696 runtime: true,
697 },
698 ToolDocMeta {
699 id: "prettier:json",
700 language: "Multi",
701 kind: ToolKind::Format,
702 doc_group: "prettier",
703 display_command: Some("prettier --stdin-filepath=_.EXT"),
704 runtime: true,
705 },
706 ToolDocMeta {
707 id: "prettier:yaml",
708 language: "Multi",
709 kind: ToolKind::Format,
710 doc_group: "prettier",
711 display_command: Some("prettier --stdin-filepath=_.EXT"),
712 runtime: true,
713 },
714 ToolDocMeta {
715 id: "prettier:html",
716 language: "Multi",
717 kind: ToolKind::Format,
718 doc_group: "prettier",
719 display_command: Some("prettier --stdin-filepath=_.EXT"),
720 runtime: true,
721 },
722 ToolDocMeta {
723 id: "prettier:css",
724 language: "Multi",
725 kind: ToolKind::Format,
726 doc_group: "prettier",
727 display_command: Some("prettier --stdin-filepath=_.EXT"),
728 runtime: true,
729 },
730 ToolDocMeta {
731 id: "prettier:markdown",
732 language: "Multi",
733 kind: ToolKind::Format,
734 doc_group: "prettier",
735 display_command: Some("prettier --stdin-filepath=_.EXT"),
736 runtime: true,
737 },
738 ToolDocMeta {
739 id: "shellcheck",
740 language: "Shell",
741 kind: ToolKind::Lint,
742 doc_group: "shellcheck",
743 display_command: None,
744 runtime: true,
745 },
746 ToolDocMeta {
747 id: "shfmt",
748 language: "Shell",
749 kind: ToolKind::Format,
750 doc_group: "shfmt",
751 display_command: None,
752 runtime: true,
753 },
754 ToolDocMeta {
755 id: "shuck",
756 language: "Shell",
757 kind: ToolKind::Lint,
758 doc_group: "shuck",
759 display_command: None,
760 runtime: true,
761 },
762 ToolDocMeta {
763 id: "rustfmt",
764 language: "Rust",
765 kind: ToolKind::Format,
766 doc_group: "rustfmt",
767 display_command: None,
768 runtime: true,
769 },
770 ToolDocMeta {
771 id: "gofmt",
772 language: "Go",
773 kind: ToolKind::Format,
774 doc_group: "gofmt",
775 display_command: None,
776 runtime: true,
777 },
778 ToolDocMeta {
779 id: "goimports",
780 language: "Go",
781 kind: ToolKind::Format,
782 doc_group: "goimports",
783 display_command: None,
784 runtime: true,
785 },
786 ToolDocMeta {
787 id: "clang-format",
788 language: "C/C++",
789 kind: ToolKind::Format,
790 doc_group: "clang-format",
791 display_command: None,
792 runtime: true,
793 },
794 ToolDocMeta {
795 id: "sqlfluff:lint",
796 language: "SQL",
797 kind: ToolKind::Lint,
798 doc_group: "sqlfluff:lint",
799 display_command: None,
800 runtime: true,
801 },
802 ToolDocMeta {
803 id: "sqlfluff:fix",
804 language: "SQL",
805 kind: ToolKind::Format,
806 doc_group: "sqlfluff:fix",
807 display_command: None,
808 runtime: true,
809 },
810 ToolDocMeta {
811 id: "jq",
812 language: "JSON",
813 kind: ToolKind::Both,
814 doc_group: "jq",
815 display_command: None,
816 runtime: true,
817 },
818 ToolDocMeta {
819 id: "yamlfmt",
820 language: "YAML",
821 kind: ToolKind::Format,
822 doc_group: "yamlfmt",
823 display_command: None,
824 runtime: true,
825 },
826 ToolDocMeta {
827 id: "taplo",
828 language: "TOML",
829 kind: ToolKind::Format,
830 doc_group: "taplo",
831 display_command: None,
832 runtime: true,
833 },
834 ToolDocMeta {
835 id: "terraform-fmt",
836 language: "Terraform",
837 kind: ToolKind::Format,
838 doc_group: "terraform-fmt",
839 display_command: None,
840 runtime: true,
841 },
842 ToolDocMeta {
843 id: "nixfmt",
844 language: "Nix",
845 kind: ToolKind::Format,
846 doc_group: "nixfmt",
847 display_command: None,
848 runtime: true,
849 },
850 ToolDocMeta {
851 id: "stylua",
852 language: "Lua",
853 kind: ToolKind::Format,
854 doc_group: "stylua",
855 display_command: None,
856 runtime: true,
857 },
858 ToolDocMeta {
859 id: "ormolu",
860 language: "Haskell",
861 kind: ToolKind::Format,
862 doc_group: "ormolu",
863 display_command: None,
864 runtime: true,
865 },
866 ToolDocMeta {
867 id: "elm-format",
868 language: "Elm",
869 kind: ToolKind::Format,
870 doc_group: "elm-format",
871 display_command: None,
872 runtime: true,
873 },
874 ToolDocMeta {
875 id: "swift-format",
876 language: "Swift",
877 kind: ToolKind::Format,
878 doc_group: "swift-format",
879 display_command: None,
880 runtime: true,
881 },
882 ToolDocMeta {
883 id: "ktfmt",
884 language: "Kotlin",
885 kind: ToolKind::Format,
886 doc_group: "ktfmt",
887 display_command: None,
888 runtime: true,
889 },
890 ToolDocMeta {
891 id: "djlint",
892 language: "Jinja/HTML",
893 kind: ToolKind::Both,
894 doc_group: "djlint",
895 display_command: None,
896 runtime: true,
897 },
898 ToolDocMeta {
899 id: "djlint:lint",
900 language: "Jinja/HTML",
901 kind: ToolKind::Lint,
902 doc_group: "djlint:lint",
903 display_command: None,
904 runtime: true,
905 },
906 ToolDocMeta {
907 id: "djlint:reformat",
908 language: "Jinja/HTML",
909 kind: ToolKind::Format,
910 doc_group: "djlint:reformat",
911 display_command: None,
912 runtime: true,
913 },
914 ToolDocMeta {
915 id: "beautysh",
916 language: "Shell",
917 kind: ToolKind::Both,
918 doc_group: "beautysh",
919 display_command: None,
920 runtime: true,
921 },
922 ToolDocMeta {
923 id: "tombi",
924 language: "TOML",
925 kind: ToolKind::Lint,
926 doc_group: "tombi",
927 display_command: None,
928 runtime: true,
929 },
930 ToolDocMeta {
931 id: "tombi:format",
932 language: "TOML",
933 kind: ToolKind::Format,
934 doc_group: "tombi:format",
935 display_command: None,
936 runtime: true,
937 },
938 ToolDocMeta {
939 id: "tombi:lint",
940 language: "TOML",
941 kind: ToolKind::Lint,
942 doc_group: "tombi:lint",
943 display_command: None,
944 runtime: true,
945 },
946 ToolDocMeta {
947 id: "oxfmt",
948 language: "Multi",
949 kind: ToolKind::Format,
950 doc_group: "oxfmt",
951 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
952 runtime: true,
953 },
954 ToolDocMeta {
955 id: "oxfmt:js",
956 language: "Multi",
957 kind: ToolKind::Format,
958 doc_group: "oxfmt",
959 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
960 runtime: true,
961 },
962 ToolDocMeta {
963 id: "oxfmt:ts",
964 language: "Multi",
965 kind: ToolKind::Format,
966 doc_group: "oxfmt",
967 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
968 runtime: true,
969 },
970 ToolDocMeta {
971 id: "oxfmt:jsx",
972 language: "Multi",
973 kind: ToolKind::Format,
974 doc_group: "oxfmt",
975 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
976 runtime: true,
977 },
978 ToolDocMeta {
979 id: "oxfmt:tsx",
980 language: "Multi",
981 kind: ToolKind::Format,
982 doc_group: "oxfmt",
983 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
984 runtime: true,
985 },
986 ToolDocMeta {
987 id: "oxfmt:json",
988 language: "Multi",
989 kind: ToolKind::Format,
990 doc_group: "oxfmt",
991 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
992 runtime: true,
993 },
994 ToolDocMeta {
995 id: "oxfmt:css",
996 language: "Multi",
997 kind: ToolKind::Format,
998 doc_group: "oxfmt",
999 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1000 runtime: true,
1001 },
1002 ToolDocMeta {
1003 id: "deno-fmt",
1004 language: "Multi",
1005 kind: ToolKind::Format,
1006 doc_group: "deno-fmt",
1007 display_command: Some("deno fmt --ext=EXT -"),
1008 runtime: true,
1009 },
1010 ToolDocMeta {
1011 id: "deno-fmt:ts",
1012 language: "Multi",
1013 kind: ToolKind::Format,
1014 doc_group: "deno-fmt",
1015 display_command: Some("deno fmt --ext=EXT -"),
1016 runtime: true,
1017 },
1018 ToolDocMeta {
1019 id: "deno-fmt:js",
1020 language: "Multi",
1021 kind: ToolKind::Format,
1022 doc_group: "deno-fmt",
1023 display_command: Some("deno fmt --ext=EXT -"),
1024 runtime: true,
1025 },
1026 ToolDocMeta {
1027 id: "deno-fmt:json",
1028 language: "Multi",
1029 kind: ToolKind::Format,
1030 doc_group: "deno-fmt",
1031 display_command: Some("deno fmt --ext=EXT -"),
1032 runtime: true,
1033 },
1034 ToolDocMeta {
1035 id: "deno-fmt:jsonc",
1036 language: "Multi",
1037 kind: ToolKind::Format,
1038 doc_group: "deno-fmt",
1039 display_command: Some("deno fmt --ext=EXT -"),
1040 runtime: true,
1041 },
1042 ToolDocMeta {
1043 id: "deno-fmt:md",
1044 language: "Multi",
1045 kind: ToolKind::Format,
1046 doc_group: "deno-fmt",
1047 display_command: Some("deno fmt --ext=EXT -"),
1048 runtime: true,
1049 },
1050 ToolDocMeta {
1053 id: RUMDL_BUILTIN_TOOL,
1054 language: "Markdown",
1055 kind: ToolKind::Lint,
1056 doc_group: RUMDL_BUILTIN_TOOL,
1057 display_command: Some("built-in markdown linting"),
1058 runtime: false,
1059 },
1060];
1061
1062const TABLE_BEGIN: &str = "<!-- BEGIN builtin-tools (generated) -->";
1064const TABLE_END: &str = "<!-- END builtin-tools (generated) -->";
1065
1066#[derive(Debug, Clone, PartialEq, Eq)]
1068pub enum DocsError {
1069 MissingMarker,
1071 DuplicateMarker,
1073 MarkerOrder,
1075 CountRowMissing,
1077 CountRowAmbiguous,
1079 CountRowMalformed,
1081}
1082
1083impl std::fmt::Display for DocsError {
1084 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1085 let msg = match self {
1086 DocsError::MissingMarker => "missing `<!-- BEGIN/END builtin-tools (generated) -->` marker pair",
1087 DocsError::DuplicateMarker => "duplicate builtin-tools marker",
1088 DocsError::MarkerOrder => "END builtin-tools marker precedes BEGIN",
1089 DocsError::CountRowMissing => "`| Built-in tools` count row not found",
1090 DocsError::CountRowAmbiguous => "multiple `| Built-in tools` count rows found",
1091 DocsError::CountRowMalformed => "`| Built-in tools` count row has an unexpected layout",
1092 };
1093 f.write_str(msg)
1094 }
1095}
1096
1097impl std::error::Error for DocsError {}
1098
1099fn builtin_tools_group_count() -> usize {
1101 let mut seen: Vec<&str> = Vec::new();
1102 for m in BUILTIN_TOOLS_DOCS {
1103 if !seen.contains(&m.doc_group) {
1104 seen.push(m.doc_group);
1105 }
1106 }
1107 seen.len()
1108}
1109
1110pub fn render_builtin_tools_table() -> String {
1116 let headers = ["Tool ID", "Language", "Type", "Command"];
1117 let mut rows: Vec<[String; 4]> = Vec::new();
1118
1119 let mut seen: Vec<&str> = Vec::new();
1120 for m in BUILTIN_TOOLS_DOCS {
1121 if seen.contains(&m.doc_group) {
1122 continue;
1123 }
1124 seen.push(m.doc_group);
1125
1126 let command = match m.display_command {
1129 Some(cmd) => cmd.to_string(),
1130 None if m.runtime => runtime_command_for_kind(m.id, m.kind),
1131 None => String::new(),
1132 };
1133
1134 rows.push([
1135 format!("`{}`", m.doc_group),
1136 m.language.to_string(),
1137 m.kind.label().to_string(),
1138 format!("`{command}`"),
1139 ]);
1140 }
1141
1142 let mut widths = headers.map(str::chars).map(Iterator::count);
1144 for row in &rows {
1145 for (i, cell) in row.iter().enumerate() {
1146 widths[i] = widths[i].max(cell.chars().count());
1147 }
1148 }
1149
1150 let mut out = String::new();
1151 push_table_row(&mut out, &headers.map(String::from), &widths);
1152 let separators = std::array::from_fn(|i| "-".repeat(widths[i]));
1153 push_table_row(&mut out, &separators, &widths);
1154 for row in &rows {
1155 push_table_row(&mut out, row, &widths);
1156 }
1157 out
1158}
1159
1160fn runtime_command_for_kind(id: &str, kind: ToolKind) -> String {
1165 let Some(def) = BUILTIN_TOOLS.get(id) else {
1166 return String::new();
1167 };
1168 let invocation =
1169 |extra: &[String]| -> String { def.command.iter().chain(extra).cloned().collect::<Vec<_>>().join(" ") };
1170 match kind {
1171 ToolKind::Lint => invocation(&def.lint_args),
1172 ToolKind::Format => invocation(&def.format_args),
1173 ToolKind::Both => {
1174 let lint = invocation(&def.lint_args);
1175 let format = invocation(&def.format_args);
1176 if lint == format {
1177 lint
1178 } else {
1179 format!("{lint} / {format}")
1180 }
1181 }
1182 }
1183}
1184
1185fn push_table_row(out: &mut String, cells: &[String; 4], widths: &[usize; 4]) {
1187 out.push('|');
1188 for (i, cell) in cells.iter().enumerate() {
1189 out.push(' ');
1190 out.push_str(cell);
1191 for _ in cell.chars().count()..widths[i] {
1192 out.push(' ');
1193 }
1194 out.push_str(" |");
1195 }
1196 out.push('\n');
1197}
1198
1199pub fn splice_builtin_tools_docs(existing: &str) -> Result<String, DocsError> {
1205 if existing.matches(TABLE_BEGIN).count() == 0 || existing.matches(TABLE_END).count() == 0 {
1206 return Err(DocsError::MissingMarker);
1207 }
1208 if existing.matches(TABLE_BEGIN).count() > 1 || existing.matches(TABLE_END).count() > 1 {
1209 return Err(DocsError::DuplicateMarker);
1210 }
1211 let begin_pos = existing.find(TABLE_BEGIN).unwrap();
1212 let end_pos = existing.find(TABLE_END).unwrap();
1213 if end_pos < begin_pos {
1214 return Err(DocsError::MarkerOrder);
1215 }
1216
1217 let table = render_builtin_tools_table();
1218 let replacement = format!("{TABLE_BEGIN}\n\n{table}\n{TABLE_END}");
1219
1220 let mut result = String::with_capacity(existing.len() + replacement.len());
1221 result.push_str(&existing[..begin_pos]);
1222 result.push_str(&replacement);
1223 result.push_str(&existing[end_pos + TABLE_END.len()..]);
1224
1225 update_builtin_tools_count(&result, builtin_tools_group_count())
1226}
1227
1228fn update_builtin_tools_count(text: &str, count: usize) -> Result<String, DocsError> {
1231 let lines: Vec<&str> = text.lines().collect();
1232 let mut target: Option<usize> = None;
1233 for (i, line) in lines.iter().enumerate() {
1234 if line.trim_start().starts_with("| Built-in tools") {
1235 if target.is_some() {
1236 return Err(DocsError::CountRowAmbiguous);
1237 }
1238 target = Some(i);
1239 }
1240 }
1241 let i = target.ok_or(DocsError::CountRowMissing)?;
1242 let new_line = replace_count_cell(lines[i], count).ok_or(DocsError::CountRowMalformed)?;
1243
1244 let mut out = String::with_capacity(text.len());
1245 for (j, line) in lines.iter().enumerate() {
1246 out.push_str(if j == i { &new_line } else { line });
1247 out.push('\n');
1248 }
1249 if !text.ends_with('\n') {
1250 out.pop();
1251 }
1252 Ok(out)
1253}
1254
1255fn replace_count_cell(line: &str, count: usize) -> Option<String> {
1257 let pipes: Vec<usize> = line.match_indices('|').map(|(i, _)| i).collect();
1258 if pipes.len() < 3 {
1259 return None;
1260 }
1261 let start = pipes[1] + 1;
1262 let end = pipes[2];
1263 let cell = line.get(start..end)?;
1264 let width = cell.len();
1265 let num = count.to_string();
1266
1267 let mut new_cell = String::with_capacity(width.max(num.len() + 2));
1268 new_cell.push(' ');
1269 new_cell.push_str(&num);
1270 while new_cell.len() < width {
1271 new_cell.push(' ');
1272 }
1273 if new_cell.len() > width {
1274 new_cell = format!(" {num} ");
1276 }
1277
1278 Some(format!("{}{}{}", &line[..start], new_cell, &line[end..]))
1279}
1280
1281#[cfg(test)]
1282mod tests {
1283 use super::*;
1284
1285 #[test]
1286 fn test_get_builtin_tool() {
1287 let registry = ToolRegistry::default();
1288
1289 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1290 assert!(tool.command.contains(&"ruff".to_string()));
1291 assert!(tool.stdin);
1292 assert!(tool.stdout);
1293
1294 let tool = registry.get("shellcheck").expect("Should find shellcheck");
1295 assert!(tool.command.contains(&"shellcheck".to_string()));
1296 }
1297
1298 #[test]
1299 fn test_builtin_yamlfmt_lint_command_validates_stdin() {
1300 let registry = ToolRegistry::default();
1301
1302 let tool = registry.get("yamlfmt").expect("Should find yamlfmt");
1303 let mut argv = tool.command.clone();
1304 argv.extend(tool.lint_args.clone());
1305
1306 assert_eq!(argv, vec!["yamlfmt", "-lint", "-"]);
1307 }
1308
1309 #[test]
1310 fn test_get_user_tool_overrides_builtin() {
1311 let mut user_tools = BTreeMap::new();
1312 user_tools.insert(
1313 "ruff:check".to_string(),
1314 ToolDefinition {
1315 command: vec!["custom-ruff".to_string()],
1316 stdin: false,
1317 stdout: false,
1318 lint_args: vec![],
1319 format_args: vec![],
1320 },
1321 );
1322
1323 let registry = ToolRegistry::new(user_tools);
1324
1325 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1326 assert_eq!(tool.command, vec!["custom-ruff"]);
1327 assert!(!tool.stdin); }
1329
1330 #[test]
1331 fn test_contains() {
1332 let registry = ToolRegistry::default();
1333
1334 assert!(registry.contains("ruff:check"));
1335 assert!(registry.contains("prettier"));
1336 assert!(registry.contains("shellcheck"));
1337 assert!(!registry.contains("nonexistent-tool"));
1338 }
1339
1340 #[test]
1341 fn test_list_tools() {
1342 let registry = ToolRegistry::default();
1343 let tools = registry.list_tools();
1344
1345 assert!(tools.contains(&"ruff:check"));
1346 assert!(tools.contains(&"ruff:format"));
1347 assert!(tools.contains(&"prettier"));
1348 assert!(tools.contains(&"shellcheck"));
1349 assert!(tools.contains(&"shfmt"));
1350 assert!(tools.contains(&"rustfmt"));
1351 assert!(tools.contains(&"gofmt"));
1352 }
1353
1354 #[test]
1355 fn test_user_tools_in_list() {
1356 let mut user_tools = BTreeMap::new();
1357 user_tools.insert("my-custom-tool".to_string(), ToolDefinition::default());
1358
1359 let registry = ToolRegistry::new(user_tools);
1360 let tools = registry.list_tools();
1361
1362 assert!(tools.contains(&"my-custom-tool"));
1363 assert!(tools.contains(&"ruff:check")); }
1365
1366 #[test]
1367 fn test_new_builtin_tools() {
1368 let registry = ToolRegistry::default();
1369
1370 let tool = registry.get("djlint").expect("Should find djlint");
1372 assert!(tool.command.contains(&"djlint".to_string()));
1373 assert!(tool.stdin);
1374
1375 let tool = registry.get("beautysh").expect("Should find beautysh");
1377 assert!(tool.command.contains(&"beautysh".to_string()));
1378 assert!(tool.stdin);
1379
1380 let tool = registry.get("tombi").expect("Should find tombi");
1382 assert!(tool.command.contains(&"tombi".to_string()));
1383 assert!(tool.stdin);
1384
1385 let tool = registry.get("tombi:lint").expect("Should find tombi:lint");
1386 assert!(tool.command.contains(&"lint".to_string()));
1387
1388 let tool = registry.get("tombi:format").expect("Should find tombi:format");
1389 assert!(
1390 tool.command.contains(&"format".to_string()),
1391 "tombi:format should use 'format' subcommand, got: {:?}",
1392 tool.command
1393 );
1394
1395 let tool = registry.get("oxfmt").expect("Should find oxfmt");
1397 assert!(tool.command.contains(&"oxfmt".to_string()));
1398 assert!(tool.stdin);
1399
1400 let tool = registry.get("oxfmt:ts").expect("Should find oxfmt:ts");
1401 assert!(tool.command.iter().any(|s| s.contains("_.ts")));
1402 }
1403
1404 #[test]
1412 fn test_bare_tombi_resolves_to_lint_not_format() {
1413 let registry = ToolRegistry::default();
1414
1415 let bare = registry.get("tombi").expect("Should find bare tombi");
1416 let format = registry.get("tombi:format").expect("Should find tombi:format");
1417
1418 assert!(
1420 bare.command.contains(&"lint".to_string()),
1421 "Bare 'tombi' uses lint subcommand: {:?}",
1422 bare.command
1423 );
1424
1425 assert!(
1427 format.command.contains(&"format".to_string()),
1428 "tombi:format uses format subcommand: {:?}",
1429 format.command
1430 );
1431
1432 assert_ne!(
1434 bare.command, format.command,
1435 "Bare 'tombi' and 'tombi:format' should have different commands (this is the root cause of #527)"
1436 );
1437 }
1438
1439 #[test]
1442 fn test_tools_with_lint_format_variants_are_distinct() {
1443 let registry = ToolRegistry::default();
1444
1445 let ruff_check = registry.get("ruff:check").expect("ruff:check");
1447 let ruff_format = registry.get("ruff:format").expect("ruff:format");
1448 assert_ne!(
1449 ruff_check.command, ruff_format.command,
1450 "ruff:check and ruff:format should be distinct"
1451 );
1452
1453 let tombi_lint = registry.get("tombi:lint").expect("tombi:lint");
1455 let tombi_format = registry.get("tombi:format").expect("tombi:format");
1456 assert_ne!(
1457 tombi_lint.command, tombi_format.command,
1458 "tombi:lint and tombi:format should be distinct"
1459 );
1460 }
1461
1462 #[test]
1463 fn test_deno_fmt_has_per_extension_variants() {
1464 let registry = ToolRegistry::default();
1467
1468 let deno_json = registry.get("deno-fmt:json").expect("deno-fmt:json");
1469 assert!(deno_json.command.iter().any(|a| a == "--ext=json"));
1470 let deno_md = registry.get("deno-fmt:md").expect("deno-fmt:md");
1471 assert!(deno_md.command.iter().any(|a| a == "--ext=md"));
1472
1473 let deno = registry.get("deno-fmt").expect("deno-fmt");
1475 assert!(deno.command.iter().any(|a| a == "--ext=ts"));
1476 }
1477
1478 #[test]
1483 fn test_docs_metadata_ids_unique() {
1484 let mut seen = std::collections::BTreeSet::new();
1485 for m in BUILTIN_TOOLS_DOCS {
1486 assert!(seen.insert(m.id), "duplicate metadata id: {}", m.id);
1487 }
1488 }
1489
1490 #[test]
1491 fn test_runtime_metadata_matches_registry_keys() {
1492 let runtime_meta: std::collections::BTreeSet<&str> =
1493 BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime).map(|m| m.id).collect();
1494 let map_keys: std::collections::BTreeSet<&str> = BUILTIN_TOOLS.keys().copied().collect();
1495 assert_eq!(
1496 runtime_meta, map_keys,
1497 "BUILTIN_TOOLS_DOCS runtime ids must exactly match BUILTIN_TOOLS keys (add/remove the doc entry alongside the registry entry)"
1498 );
1499 }
1500
1501 #[test]
1502 fn test_docs_only_ids_absent_from_registry() {
1503 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
1504 assert!(
1505 !BUILTIN_TOOLS.contains_key(m.id),
1506 "docs-only id {} must not be a runtime registry tool",
1507 m.id
1508 );
1509 }
1510 assert!(
1512 BUILTIN_TOOLS_DOCS
1513 .iter()
1514 .any(|m| !m.runtime && m.id == RUMDL_BUILTIN_TOOL),
1515 "rumdl must be present as a docs-only entry"
1516 );
1517 }
1518
1519 #[test]
1520 fn test_docs_only_requires_display_command() {
1521 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
1522 assert!(
1523 m.display_command.is_some(),
1524 "docs-only id {} needs a display_command (no runtime command to derive)",
1525 m.id
1526 );
1527 }
1528 }
1529
1530 #[test]
1531 fn test_multi_runtime_group_requires_display_command() {
1532 let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
1533 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
1534 *counts.entry(m.doc_group).or_default() += 1;
1535 }
1536 for m in BUILTIN_TOOLS_DOCS {
1537 if counts.get(m.doc_group).copied().unwrap_or(0) > 1 {
1538 assert!(
1539 m.display_command.is_some(),
1540 "doc_group `{}` has multiple runtime entries; `{}` needs an explicit display_command",
1541 m.doc_group,
1542 m.id
1543 );
1544 }
1545 }
1546 }
1547
1548 #[test]
1549 fn test_doc_group_language_and_kind_consistent() {
1550 let mut groups: std::collections::BTreeMap<&str, (&str, ToolKind)> = std::collections::BTreeMap::new();
1551 for m in BUILTIN_TOOLS_DOCS {
1552 match groups.get(m.doc_group) {
1553 None => {
1554 groups.insert(m.doc_group, (m.language, m.kind));
1555 }
1556 Some((lang, kind)) => {
1557 assert_eq!(
1558 *lang, m.language,
1559 "doc_group `{}` has mismatched languages",
1560 m.doc_group
1561 );
1562 assert_eq!(*kind, m.kind, "doc_group `{}` has mismatched kinds", m.doc_group);
1563 }
1564 }
1565 }
1566 }
1567
1568 fn rendered_rows(table: &str) -> Vec<(String, String, String, String)> {
1575 table
1576 .lines()
1577 .skip(2) .filter(|l| l.starts_with('|'))
1579 .map(|l| {
1580 let cells: Vec<String> = l.trim().trim_matches('|').split('|').map(|c| c.trim().to_string()).collect();
1581 (cells[0].clone(), cells[1].clone(), cells[2].clone(), cells[3].clone())
1582 })
1583 .collect()
1584 }
1585
1586 #[test]
1587 fn test_render_table_golden_rows() {
1588 let table = render_builtin_tools_table();
1589 let rows = rendered_rows(&table);
1590 let has = |id: &str, lang: &str, kind: &str, cmd: &str| {
1591 rows.iter()
1592 .any(|(i, l, k, c)| i == id && l == lang && k == kind && c == cmd)
1593 };
1594 assert!(has("`shellcheck`", "Shell", "Lint", "`shellcheck --shell=bash -`"));
1596 assert!(has("`rustfmt`", "Rust", "Format", "`rustfmt`"));
1597 assert!(has("`jq`", "JSON", "Both", "`jq .`"));
1598 assert!(has(
1599 "`prettier`",
1600 "Multi",
1601 "Format",
1602 "`prettier --stdin-filepath=_.EXT`"
1603 ));
1604 assert!(has("`rumdl`", "Markdown", "Lint", "`built-in markdown linting`"));
1605 assert!(
1607 !table.contains("prettier:json"),
1608 "prettier variants must collapse into one row"
1609 );
1610 assert!(!table.contains("oxfmt:ts"), "oxfmt variants must collapse into one row");
1611 }
1612
1613 #[test]
1614 fn test_render_table_one_row_per_group() {
1615 let table = render_builtin_tools_table();
1616 assert_eq!(rendered_rows(&table).len(), builtin_tools_group_count());
1617 }
1618
1619 #[test]
1620 fn test_render_table_command_fallback_from_registry() {
1621 let table = render_builtin_tools_table();
1623 assert!(
1624 rendered_rows(&table)
1625 .iter()
1626 .any(|(i, _, _, c)| i == "`tombi`" && c == "`tombi lint -`")
1627 );
1628 }
1629
1630 #[test]
1631 fn test_render_command_reflects_mode_args() {
1632 let table = render_builtin_tools_table();
1633 let rows = rendered_rows(&table);
1634 let cmd = |id: &str| {
1635 let row = rows
1636 .iter()
1637 .find(|(i, _, _, _)| i == id)
1638 .unwrap_or_else(|| panic!("row {id} not found"));
1639 row.3.clone()
1640 };
1641 assert_eq!(cmd("`yamlfmt`"), "`yamlfmt -`");
1643 assert_eq!(cmd("`djlint`"), "`djlint - / djlint - --reformat`");
1645 assert_eq!(cmd("`sqlfluff:lint`"), "`sqlfluff lint --dialect ansi -`");
1647 }
1648
1649 #[test]
1650 fn test_runtime_command_for_kind_both_collapses_when_equal() {
1651 assert_eq!(runtime_command_for_kind("jq", ToolKind::Both), "jq .");
1653 }
1654
1655 #[test]
1660 fn test_splice_replaces_region_and_preserves_prose() {
1661 let doc = format!(
1662 "# Title\n\nIntro.\n\n{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\nAfter.\n\n| Built-in tools | 31 | 339 |\n"
1663 );
1664 let out = splice_builtin_tools_docs(&doc).expect("splice");
1665 assert!(out.starts_with("# Title\n\nIntro.\n\n"));
1666 assert!(out.contains("After.\n"));
1667 assert!(!out.contains("stale"));
1668 assert!(
1669 out.contains(&render_builtin_tools_table()),
1670 "generated table is spliced in verbatim"
1671 );
1672 assert!(out.contains(&format!("| Built-in tools | {} | 339 |", builtin_tools_group_count())));
1674 }
1675
1676 #[test]
1677 fn test_splice_missing_marker_errors() {
1678 let doc = "# Title\n\nno markers\n\n| Built-in tools | 31 | 339 |\n";
1679 assert_eq!(splice_builtin_tools_docs(doc), Err(DocsError::MissingMarker));
1680 }
1681
1682 #[test]
1683 fn test_splice_duplicate_marker_errors() {
1684 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n{TABLE_BEGIN}\n\ny\n\n{TABLE_END}\n");
1685 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::DuplicateMarker));
1686 }
1687
1688 #[test]
1689 fn test_splice_marker_order_errors() {
1690 let doc = format!("{TABLE_END}\n\nx\n\n{TABLE_BEGIN}\n\n| Built-in tools | 31 | 339 |\n");
1691 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::MarkerOrder));
1692 }
1693
1694 #[test]
1695 fn test_splice_missing_count_row_errors() {
1696 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n\nno count row here\n");
1697 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::CountRowMissing));
1698 }
1699
1700 #[test]
1701 fn test_splice_is_idempotent() {
1702 let doc = format!("{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\n| Built-in tools | 31 | 339 |\n");
1703 let once = splice_builtin_tools_docs(&doc).expect("first");
1704 let twice = splice_builtin_tools_docs(&once).expect("second");
1705 assert_eq!(once, twice, "splice must be idempotent");
1706 }
1707}