1use std::{collections::HashMap, fmt::Write as _, path::PathBuf};
2
3use clap::Parser;
4use itertools::Itertools;
5
6use crate::config;
7
8#[derive(Parser, Debug)]
9#[command(rename_all = "kebab-case")]
10pub struct Opts {
11 #[arg(
16 id = "config",
17 short,
18 long,
19 env = "VECTOR_CONFIG",
20 value_delimiter(',')
21 )]
22 paths: Vec<PathBuf>,
23
24 #[arg(id = "config-toml", long, value_delimiter(','))]
26 paths_toml: Vec<PathBuf>,
27
28 #[arg(id = "config-json", long, value_delimiter(','))]
30 paths_json: Vec<PathBuf>,
31
32 #[arg(id = "config-yaml", long, value_delimiter(','))]
34 paths_yaml: Vec<PathBuf>,
35
36 #[arg(
41 id = "config-dir",
42 short = 'C',
43 long,
44 env = "VECTOR_CONFIG_DIR",
45 value_delimiter(',')
46 )]
47 pub config_dirs: Vec<PathBuf>,
48
49 #[arg(id = "format", long, default_value = "dot")]
54 pub format: OutputFormat,
55}
56
57#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
58pub enum OutputFormat {
59 Dot,
60 Mermaid,
61}
62
63impl Opts {
64 fn paths_with_formats(&self) -> Vec<config::ConfigPath> {
65 config::merge_path_lists(vec![
66 (&self.paths, None),
67 (&self.paths_toml, Some(config::Format::Toml)),
68 (&self.paths_json, Some(config::Format::Json)),
69 (&self.paths_yaml, Some(config::Format::Yaml)),
70 ])
71 .map(|(path, hint)| config::ConfigPath::File(path, hint))
72 .chain(
73 self.config_dirs
74 .iter()
75 .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
76 )
77 .collect()
78 }
79}
80
81fn node_attributes_to_string(attributes: &HashMap<String, String>, default_shape: &str) -> String {
82 let mut attrs = attributes.clone();
83 if !attrs.contains_key("shape") {
84 attrs.insert("shape".to_string(), default_shape.to_string());
85 }
86 attrs.iter().map(|(k, v)| format!("{k}=\"{v}\"")).join(" ")
87}
88
89pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode {
90 let paths = opts.paths_with_formats();
91 let paths = match config::process_paths(&paths) {
92 Some(paths) => paths,
93 None => return exitcode::CONFIG,
94 };
95
96 let config = match config::load_from_paths(&paths) {
97 Ok(config) => config,
98 Err(errs) => {
99 #[allow(clippy::print_stderr)]
100 for err in errs {
101 eprintln!("{err}");
102 }
103 return exitcode::CONFIG;
104 }
105 };
106
107 let format = opts.format;
108 match format {
109 OutputFormat::Dot => render_dot(config),
110 OutputFormat::Mermaid => render_mermaid(config),
111 }
112}
113
114fn render_dot(config: config::Config) -> exitcode::ExitCode {
115 let mut dot = String::from("digraph {\n");
116
117 for (id, source) in config.sources() {
118 writeln!(
119 dot,
120 " \"{}\" [{}]",
121 id,
122 node_attributes_to_string(&source.graph.node_attributes, "trapezium")
123 )
124 .expect("write to String never fails");
125 }
126
127 for (id, transform) in config.transforms() {
128 writeln!(
129 dot,
130 " \"{}\" [{}]",
131 id,
132 node_attributes_to_string(&transform.graph.node_attributes, "diamond")
133 )
134 .expect("write to String never fails");
135
136 for input in transform.inputs.iter() {
137 if let Some(port) = &input.port {
138 writeln!(
139 dot,
140 " \"{}\" -> \"{}\" [label=\"{}\"]",
141 input.component, id, port
142 )
143 .expect("write to String never fails");
144 } else {
145 writeln!(dot, " \"{input}\" -> \"{id}\"").expect("write to String never fails");
146 }
147 }
148 }
149
150 for (id, sink) in config.sinks() {
151 writeln!(
152 dot,
153 " \"{}\" [{}]",
154 id,
155 node_attributes_to_string(&sink.graph.node_attributes, "invtrapezium")
156 )
157 .expect("write to String never fails");
158
159 for input in &sink.inputs {
160 if let Some(port) = &input.port {
161 writeln!(
162 dot,
163 " \"{}\" -> \"{}\" [label=\"{}\"]",
164 input.component, id, port
165 )
166 .expect("write to String never fails");
167 } else {
168 writeln!(dot, " \"{input}\" -> \"{id}\"").expect("write to String never fails");
169 }
170 }
171 }
172
173 dot += "}";
174
175 #[allow(clippy::print_stdout)]
176 {
177 println!("{dot}");
178 }
179
180 exitcode::OK
181}
182
183fn render_mermaid(config: config::Config) -> exitcode::ExitCode {
184 let mut mermaid = String::from("flowchart TD;\n");
185
186 writeln!(mermaid, "\n %% Sources").unwrap();
187 for (id, _) in config.sources() {
188 writeln!(mermaid, " {id}[/{id}/]").unwrap();
189 }
190
191 writeln!(mermaid, "\n %% Transforms").unwrap();
192 for (id, transform) in config.transforms() {
193 writeln!(mermaid, " {id}{{{id}}}").unwrap();
194
195 for input in transform.inputs.iter() {
196 if let Some(port) = &input.port {
197 writeln!(mermaid, " {0} -->|{port}| {id}", input.component).unwrap();
198 } else {
199 writeln!(mermaid, " {0} --> {id}", input.component).unwrap();
200 }
201 }
202 }
203
204 writeln!(mermaid, "\n %% Sinks").unwrap();
205 for (id, sink) in config.sinks() {
206 writeln!(mermaid, " {id}[\\{id}\\]").unwrap();
207
208 for input in &sink.inputs {
209 if let Some(port) = &input.port {
210 writeln!(mermaid, " {0} -->|{port}| {id}", input.component).unwrap();
211 } else {
212 writeln!(mermaid, " {0} --> {id}", input.component).unwrap();
213 }
214 }
215 }
216
217 #[allow(clippy::print_stdout)]
218 {
219 println!("{mermaid}");
220 }
221
222 exitcode::OK
223}