use std::borrow::Cow;
use std::fmt::Write;
use std::path::{Path, PathBuf};
pub fn flatten_string<'text, 'raw>(
value: nojson::RawJsonValue<'text, 'raw>,
) -> Result<Cow<'text, str>, nojson::JsonParseError> {
if let Ok(s) = value.to_unquoted_string_str() {
Ok(s)
} else if let Ok(array) = value.to_array() {
let mut buf = String::new();
for value in array {
flatten_string_to_buf(value, &mut buf)?;
}
Ok(Cow::Owned(buf))
} else {
Err(value.invalid("expected string or array of strings"))
}
}
pub fn parse_from_flattened_string<T>(
value: nojson::RawJsonValue<'_, '_>,
) -> Result<T, nojson::JsonParseError>
where
T: std::str::FromStr,
T::Err: Into<Box<dyn Send + Sync + std::error::Error>>,
{
let s = flatten_string(value)?;
s.parse().map_err(|e| value.invalid(e))
}
fn flatten_string_to_buf<'text, 'raw>(
value: nojson::RawJsonValue<'text, 'raw>,
buf: &mut String,
) -> Result<(), nojson::JsonParseError> {
if let Ok(s) = value.to_unquoted_string_str() {
buf.push_str(&s);
Ok(())
} else if let Ok(array) = value.to_array() {
for value in array {
flatten_string_to_buf(value, buf)?;
}
Ok(())
} else {
Err(value.invalid("expected string or array of strings"))
}
}
pub(crate) fn load_jsonc_file<P: AsRef<Path>, F, T>(path: P, f: F) -> Result<T, LoadJsonError>
where
F: for<'text, 'raw> FnOnce(
nojson::RawJsonValue<'text, 'raw>,
) -> Result<T, nojson::JsonParseError>,
{
let text = std::fs::read_to_string(&path).map_err(|e| LoadJsonError::Io {
path: path.as_ref().to_path_buf(),
error: e,
})?;
load_jsonc_str(&path.as_ref().display().to_string(), &text, f)
}
pub(crate) fn load_jsonc_str<F, T>(name: &str, text: &str, f: F) -> Result<T, LoadJsonError>
where
F: for<'text, 'raw> FnOnce(
nojson::RawJsonValue<'text, 'raw>,
) -> Result<T, nojson::JsonParseError>,
{
let (json, _) = nojson::RawJson::parse_jsonc(text)
.map_err(|error| LoadJsonError::json(name, text, error))?;
let mut preprocessor = Preprocessor::new(&json);
preprocessor
.process()
.map_err(|error| LoadJsonError::json(name, text, error))?;
let text = preprocessor.processed;
let value = nojson::RawJson::parse_jsonc(&text)
.and_then(|(json, _)| f(json.value()))
.map_err(|error| LoadJsonError::json(name, &text, error))?;
Ok(value)
}
#[derive(Debug)]
pub enum LoadJsonError {
Io {
path: PathBuf,
error: std::io::Error,
},
Json {
path: PathBuf,
text: String,
error: nojson::JsonParseError,
},
}
impl LoadJsonError {
fn json(path: &str, text: &str, error: nojson::JsonParseError) -> Self {
Self::Json {
path: PathBuf::from(path),
text: text.to_owned(),
error,
}
}
}
impl std::fmt::Display for LoadJsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadJsonError::Io { path, error } => {
write!(f, "failed to read file '{}': {error}", path.display())
}
LoadJsonError::Json { path, error, text } => format_json_error(f, path, error, text),
}
}
}
impl std::error::Error for LoadJsonError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if let Self::Io { error, .. } = self {
Some(error)
} else {
None
}
}
}
fn format_json_error(
f: &mut std::fmt::Formatter<'_>,
path: &Path,
error: &nojson::JsonParseError,
text: &str,
) -> std::fmt::Result {
let (line_num, column_num) = error
.get_line_and_column_numbers(text)
.unwrap_or((std::num::NonZeroUsize::MIN, std::num::NonZeroUsize::MIN));
let line = error.get_line(text).unwrap_or("");
let (display_line, display_column) = format_line_around_position(line, column_num.get());
writeln!(f, "{error}")?;
writeln!(f, "--> {}:{line_num}:{column_num}", path.display())?;
writeln!(f, "{line_num:4} |{display_line}")?;
writeln!(f, " |{:>column$} error", "^", column = display_column)?;
Ok(())
}
fn format_line_around_position(line: &str, column_pos: usize) -> (String, usize) {
const MAX_ERROR_LINE_CHARS: usize = 80;
let chars: Vec<char> = line.chars().collect();
let max_context = MAX_ERROR_LINE_CHARS / 2;
let error_pos = column_pos.saturating_sub(1).min(chars.len());
let start_pos = error_pos.saturating_sub(max_context);
let end_pos = (error_pos + max_context + 1).min(chars.len());
let mut result = String::new();
let mut new_column_pos = error_pos - start_pos + 1;
if start_pos > 0 {
result.push_str("...");
new_column_pos += 3;
}
result.push_str(&chars[start_pos..end_pos].iter().collect::<String>());
if end_pos < chars.len() {
result.push_str("...");
}
(result, new_column_pos)
}
#[derive(Debug)]
struct Preprocessor<'text, 'raw> {
json: &'raw nojson::RawJson<'text>,
processed: String,
last_position: usize,
}
impl<'text, 'raw> Preprocessor<'text, 'raw> {
fn new(json: &'raw nojson::RawJson<'text>) -> Self {
Self {
json,
processed: String::new(),
last_position: 0,
}
}
fn process(&mut self) -> Result<(), nojson::JsonParseError> {
self.process_value(self.json.value())
}
fn process_value(
&mut self,
value: nojson::RawJsonValue<'text, 'raw>,
) -> Result<(), nojson::JsonParseError> {
let start_position = value.position();
let end_position = start_position + value.as_raw_str().len();
self.processed
.push_str(&self.json.text()[self.last_position..start_position]);
self.last_position = start_position;
if let Some(env_name) = value.to_member("env!").ok().and_then(|v| v.get()) {
let default_value = value.to_member("default")?.get();
self.process_env(env_name, default_value)?;
self.last_position = end_position;
} else if let Ok(elements) = value.to_array() {
for element in elements {
self.process_value(element)?;
}
} else if let Ok(members) = value.to_object() {
for (_member_name, member_value) in members {
self.process_value(member_value)?;
}
}
self.processed
.push_str(&self.json.text()[self.last_position..end_position]);
self.last_position = end_position;
Ok(())
}
fn process_env(
&mut self,
name: nojson::RawJsonValue<'text, 'raw>,
default: Option<nojson::RawJsonValue<'text, 'raw>>,
) -> Result<(), nojson::JsonParseError> {
let name_str = name.to_unquoted_string_str()?;
let json = if let Ok(value) = std::env::var(name_str.as_ref())
&& !value.is_empty()
{
nojson::RawJsonOwned::parse(&value)
.or_else(|_| nojson::RawJsonOwned::parse(nojson::Json(value).to_string()))
.expect("infallible")
} else if let Some(default) = default {
default.extract().into_owned()
} else {
return Err(name.invalid("environment variable is not set or empty"));
};
write!(self.processed, "{json}").expect("infallible");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_preprocessing_with_default() -> Result<(), nojson::JsonParseError> {
let original_text = r#"[1, {"env!": "TEST_ENVVAR_SECOND", "default": 2}, 3]"#;
let json = nojson::RawJson::parse(original_text)?;
let mut preprocessor = Preprocessor::new(&json);
preprocessor.process()?;
assert_eq!(preprocessor.processed, "[1, 2, 3]");
Ok(())
}
}