core_lib/config/
parser.rs1use std::{
2 collections::{HashMap, HashSet},
3 fs,
4 io::Write,
5 path::Path,
6};
7
8use anyhow::{Context, Result};
9
10use crate::{
11 config::{Job, ProjectConfig, stdin_is_tty},
12 log::logger::{LogLevel, Logger},
13};
14
15pub fn check_dependency_graph(config: &ProjectConfig) -> Result<()> {
16 let pipeline = &config.pipeline;
17
18 for (name, job) in pipeline.jobs.iter() {
19 if job.needs.contains(name) {
20 return Err(anyhow::anyhow!("Job '{}' cannot depend on itself", name));
21 }
22 for dep in &job.needs {
23 if !pipeline.jobs.contains_key(dep) {
24 return Err(anyhow::anyhow!(
25 "Job '{}' depends on unknown job '{}'",
26 name,
27 dep
28 ));
29 }
30 }
31 }
32
33 fn visit(
34 name: &str,
35 pipeline: &HashMap<String, Job>,
36 temp: &mut HashSet<String>,
37 perm: &mut HashSet<String>,
38 path: &mut Vec<String>,
39 ) -> anyhow::Result<()> {
40 if perm.contains(name) {
41 return Ok(());
42 }
43 if !temp.insert(name.to_string()) {
44 let cycle_start_index: usize = path.iter().position(|n| n == name).unwrap_or(0);
45 let cycle_path: Vec<_> = path[cycle_start_index..]
46 .iter()
47 .chain(std::iter::once(&name.to_string()))
48 .cloned()
49 .collect();
50 return Err(anyhow::anyhow!(
51 "Cycle detected: [{}]",
52 cycle_path.join(" -> ")
53 ));
54 }
55
56 path.push(name.to_string());
57 if let Some(job) = pipeline.get(name) {
58 for dep in &job.needs {
59 visit(dep, pipeline, temp, perm, path)?;
60 }
61 }
62 temp.remove(name);
63 perm.insert(name.to_string());
64 path.pop();
65 Ok(())
66 }
67 let mut temp = HashSet::new();
68 let mut perm = HashSet::new();
69 let mut path = Vec::new();
70 for name in pipeline.jobs.keys() {
71 visit(name, &pipeline.jobs, &mut temp, &mut perm, &mut path)?;
72 }
73 Ok(())
74}
75
76pub fn load_config(path: &Path) -> Result<ProjectConfig> {
77 let content: String =
78 fs::read_to_string(path).with_context(|| format!("Error reading config file {path:?}"))?;
79
80 let mut config: ProjectConfig =
81 serde_yaml::from_str(&content).with_context(|| "Error parsing YAML configuration file")?;
82
83 let mut skipped_missing_variables = HashSet::new();
84
85 for (job_name, job) in config.pipeline.jobs.iter_mut() {
87 let env_map = job.env.as_mut();
88 if env_map.is_none() {
89 continue;
90 }
91
92 for (name, value) in env_map.unwrap().iter_mut() {
93 if !value.starts_with("$") {
94 continue;
95 }
96
97 let env_key = if !&value[1..].is_empty() {
98 &value[1..]
99 } else {
100 name
101 };
102
103 let extraction_result = std::env::var(env_key);
104 if let Ok(env_value) = extraction_result {
105 *value = env_value;
106 continue;
107 }
108
109 Logger::write(
110 &format!(r#""${}" not found for job "{job_name}""#, env_key),
111 LogLevel::Warning,
112 );
113
114 if skipped_missing_variables.contains(env_key) {
115 *value = "".to_string();
116 continue;
117 }
118
119 if stdin_is_tty() {
120 if ask_continue_anyway()? {
121 skipped_missing_variables.insert(env_key.to_string());
122 *value = "".to_string();
123 continue;
124 } else {
125 return Err(extraction_result.unwrap_err().into());
126 }
127 } else {
128 return Err(anyhow::anyhow!(
129 "Missing env variable '{}' and no TTY to ask user",
130 env_key
131 ));
132 }
133 }
134 }
135 check_dependency_graph(&config)?;
137 Ok(config)
138}
139
140fn ask_continue_anyway() -> Result<bool> {
141 loop {
142 eprint!("Continue anyway ? [y/N] ");
143 std::io::stdout().flush()?;
144
145 let mut buffer = String::new();
146 std::io::stdin().read_line(&mut buffer)?;
147
148 let input = buffer.chars().next();
149
150 match input {
151 Some('y' | 'Y') => return Ok(true),
152 Some('n' | 'N') => return Ok(false),
153
154 _ => {
155 eprintln!("Invalid input, please retry.");
156 continue;
157 }
158 }
159 }
160}