Skip to main content

mk_lib/
utils.rs

1use std::path::{
2  Component,
3  Path,
4  PathBuf,
5};
6use std::{
7  fmt,
8  fs,
9};
10
11use anyhow::Context as _;
12use hashbrown::HashMap;
13use serde::de::{
14  self,
15  MapAccess,
16  Visitor,
17};
18use serde::{
19  Deserialize,
20  Deserializer,
21};
22use serde_json::Value as JsonValue;
23
24use crate::file::ToUtf8 as _;
25
26#[allow(dead_code)]
27#[derive(Debug)]
28enum AnyValue {
29  String(String),
30  Number(serde_json::Number),
31  Bool(bool),
32}
33
34impl fmt::Display for AnyValue {
35  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36    match self {
37      AnyValue::String(s) => write!(f, "{}", s),
38      AnyValue::Number(n) => write!(f, "{}", n),
39      AnyValue::Bool(b) => write!(f, "{}", b),
40    }
41  }
42}
43
44impl<'de> Deserialize<'de> for AnyValue {
45  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46  where
47    D: Deserializer<'de>,
48  {
49    let value: JsonValue = Deserialize::deserialize(deserializer)?;
50    match value {
51      JsonValue::String(s) => Ok(AnyValue::String(s)),
52      JsonValue::Number(n) => Ok(AnyValue::Number(n)),
53      JsonValue::Bool(b) => Ok(AnyValue::Bool(b)),
54      _ => Err(de::Error::custom("expected a string, number, or boolean")),
55    }
56  }
57}
58
59pub(crate) fn deserialize_environment<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
60where
61  D: Deserializer<'de>,
62{
63  struct EnvironmentVisitor;
64
65  impl<'de> Visitor<'de> for EnvironmentVisitor {
66    type Value = HashMap<String, String>;
67
68    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
69      formatter.write_str("a map of strings to any value (string, int, or bool)")
70    }
71
72    fn visit_map<M>(self, mut access: M) -> Result<HashMap<String, String>, M::Error>
73    where
74      M: MapAccess<'de>,
75    {
76      let mut map = HashMap::new();
77      while let Some((key, value)) = access.next_entry::<String, AnyValue>()? {
78        map.insert(key, value.to_string());
79      }
80      Ok(map)
81    }
82  }
83
84  deserializer.deserialize_map(EnvironmentVisitor)
85}
86
87pub(crate) fn resolve_path(base_dir: &Path, value: &str) -> PathBuf {
88  let path = Path::new(value);
89  let joined = if path.is_absolute() {
90    path.to_path_buf()
91  } else {
92    base_dir.join(path)
93  };
94
95  normalize_path(&joined)
96}
97
98pub(crate) fn normalize_path(path: &Path) -> PathBuf {
99  let mut normalized = PathBuf::new();
100
101  for component in path.components() {
102    match component {
103      Component::CurDir => {},
104      Component::ParentDir => {
105        normalized.pop();
106      },
107      other => normalized.push(other.as_os_str()),
108    }
109  }
110
111  normalized
112}
113
114pub(crate) fn load_env_files_in_dir(
115  env_files: &[String],
116  base_dir: &Path,
117) -> anyhow::Result<HashMap<String, String>> {
118  let mut local_env: HashMap<String, String> = HashMap::new();
119  for env_file in env_files {
120    let path = resolve_path(base_dir, env_file);
121    let contents = fs::read_to_string(&path).with_context(|| {
122      format!(
123        "Failed to read env file - {}",
124        path.to_utf8().unwrap_or("<non-utf8-path>")
125      )
126    })?;
127
128    local_env.extend(parse_env_contents(&contents));
129  }
130
131  Ok(local_env)
132}
133
134pub(crate) fn parse_env_contents(contents: &str) -> HashMap<String, String> {
135  let mut env_vars = HashMap::new();
136
137  for line in contents.lines() {
138    let line = line.trim();
139    if line.is_empty() || line.starts_with('#') {
140      continue;
141    }
142
143    if let Some((key, value)) = line.split_once('=') {
144      env_vars.insert(key.trim().to_string(), value.trim().to_string());
145    }
146  }
147
148  env_vars
149}