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