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 expanded = expand_home_path(value);
89  let path = expanded.as_deref().unwrap_or_else(|| Path::new(value));
90  let joined = if path.is_absolute() {
91    path.to_path_buf()
92  } else {
93    base_dir.join(path)
94  };
95
96  normalize_path(&joined)
97}
98
99pub(crate) fn expand_home_path(value: &str) -> Option<PathBuf> {
100  if value == "~" {
101    return home_dir();
102  }
103
104  if let Some(rest) = value.strip_prefix("~/") {
105    return home_dir().map(|home| home.join(rest));
106  }
107
108  None
109}
110
111fn home_dir() -> Option<PathBuf> {
112  #[cfg(windows)]
113  {
114    std::env::var_os("HOME")
115      .or_else(|| std::env::var_os("USERPROFILE"))
116      .map(PathBuf::from)
117  }
118
119  #[cfg(not(windows))]
120  {
121    std::env::var_os("HOME").map(PathBuf::from)
122  }
123}
124
125pub(crate) fn normalize_path(path: &Path) -> PathBuf {
126  let mut normalized = PathBuf::new();
127
128  for component in path.components() {
129    match component {
130      Component::CurDir => {},
131      Component::ParentDir => {
132        normalized.pop();
133      },
134      other => normalized.push(other.as_os_str()),
135    }
136  }
137
138  normalized
139}
140
141pub(crate) fn load_env_files_in_dir(
142  env_files: &[String],
143  base_dir: &Path,
144) -> anyhow::Result<HashMap<String, String>> {
145  let mut local_env: HashMap<String, String> = HashMap::new();
146  for env_file in env_files {
147    let path = resolve_path(base_dir, env_file);
148    let contents = fs::read_to_string(&path).with_context(|| {
149      format!(
150        "Failed to read env file - {}",
151        path.to_utf8().unwrap_or("<non-utf8-path>")
152      )
153    })?;
154
155    local_env.extend(parse_env_contents(&contents));
156  }
157
158  Ok(local_env)
159}
160
161pub(crate) fn parse_env_contents(contents: &str) -> HashMap<String, String> {
162  let mut env_vars = HashMap::new();
163
164  for line in contents.lines() {
165    let line = line.trim();
166    if line.is_empty() || line.starts_with('#') {
167      continue;
168    }
169
170    if let Some((key, value)) = line.split_once('=') {
171      env_vars.insert(key.trim().to_string(), value.trim().to_string());
172    }
173  }
174
175  env_vars
176}
177
178#[cfg(test)]
179mod tests {
180  use super::*;
181
182  #[test]
183  fn resolve_path_expands_home_directory() {
184    let home = std::env::temp_dir().join("mk-utils-home");
185    unsafe {
186      std::env::set_var("HOME", &home);
187    }
188    let resolved = resolve_path(Path::new("/tmp/project"), "~/.mk-test-env");
189    assert_eq!(resolved, home.join(".mk-test-env"));
190    unsafe {
191      std::env::remove_var("HOME");
192    }
193  }
194}