use crate::fs;
use regex::Regex;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt::Debug;
use std::path::Path;
use std::sync::LazyLock;
use tracing::{instrument, trace};
pub use crate::yaml_error::YamlError;
pub use serde_norway as serde_yaml;
pub use serde_norway::{
Mapping as YamlMapping, Number as YamlNumber, Sequence as YamlSequence, Value as YamlValue,
};
static WHITESPACE_PREFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s+)").unwrap());
#[inline]
#[instrument(name = "merge_yaml", skip_all)]
pub fn merge(prev: &YamlValue, next: &YamlValue) -> YamlValue {
match (prev, next) {
(YamlValue::Mapping(prev_object), YamlValue::Mapping(next_object)) => {
let mut object = prev_object.clone();
for (key, value) in next_object.iter() {
if let Some(prev_value) = prev_object.get(key) {
object.insert(key.to_owned(), merge(prev_value, value));
} else {
object.insert(key.to_owned(), value.to_owned());
}
}
YamlValue::Mapping(object)
}
_ => next.to_owned(),
}
}
#[inline]
#[instrument(name = "parse_yaml", skip(data))]
pub fn parse<D>(data: impl AsRef<str>) -> Result<D, YamlError>
where
D: DeserializeOwned,
{
trace!("Parsing YAML");
serde_norway::from_str(data.as_ref()).map_err(|error| YamlError::Parse {
error: Box::new(error),
})
}
#[inline]
#[instrument(name = "format_yaml", skip(data))]
pub fn format<D>(data: &D) -> Result<String, YamlError>
where
D: ?Sized + Serialize,
{
trace!("Formatting YAML");
serde_norway::to_string(&data).map_err(|error| YamlError::Format {
error: Box::new(error),
})
}
#[inline]
#[instrument(name = "format_yaml_with_identation", skip(data))]
pub fn format_with_identation<D>(data: &D, indent: &str) -> Result<String, YamlError>
where
D: ?Sized + Serialize,
{
trace!("Formatting YAML with preserved indentation");
let mut data = serde_norway::to_string(data)
.map_err(|error| YamlError::Format {
error: Box::new(error),
})?
.trim()
.to_string();
if indent != " " {
data = data
.split('\n')
.map(|line| {
if !line.starts_with(" ") {
return line.to_string();
}
WHITESPACE_PREFIX
.replace_all(line, |caps: ®ex::Captures| {
indent.repeat(caps.get(1).unwrap().as_str().len() / 2)
})
.to_string()
})
.collect::<Vec<_>>()
.join("\n");
}
Ok(data)
}
#[inline]
#[instrument(name = "read_yaml")]
pub fn read_file<D>(path: impl AsRef<Path> + Debug) -> Result<D, YamlError>
where
D: DeserializeOwned,
{
let path = path.as_ref();
let contents = fs::read_file(path)?;
trace!(file = ?path, "Reading YAML file");
serde_norway::from_str(&contents).map_err(|error| YamlError::ReadFile {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument(name = "write_yaml", skip(data))]
pub fn write_file<D>(path: impl AsRef<Path> + Debug, data: &D) -> Result<(), YamlError>
where
D: ?Sized + Serialize,
{
let path = path.as_ref();
trace!(file = ?path, "Writing YAML file");
let data = serde_norway::to_string(&data).map_err(|error| YamlError::WriteFile {
path: path.to_path_buf(),
error: Box::new(error),
})?;
fs::write_file(path, data)?;
Ok(())
}
#[cfg(feature = "editor-config")]
#[inline]
#[instrument(name = "write_yaml_with_config", skip(data))]
pub fn write_file_with_config<D>(path: impl AsRef<Path> + Debug, data: &D) -> Result<(), YamlError>
where
D: ?Sized + Serialize,
{
trace!(file = ?path, "Writing YAML file with .editorconfig");
let path = path.as_ref();
let editor_config = fs::get_editor_config_props(path)?;
let mut data = format_with_identation(data, &editor_config.indent)?;
editor_config.apply_eof(&mut data);
fs::write_file(path, data)?;
Ok(())
}