use indexmap::IndexMap;
use serde_json::Value as JsonValue;
use super::{config_from_object, AdapterError};
use crate::value::{HoconValue, ScalarValue};
use crate::Config;
pub fn parse(input: &str, origin: Option<&str>) -> Result<Config, AdapterError> {
let cleaned = strip_trailing_commas(&strip_comments(super::strip_bom(input))?);
let doc: JsonValue =
serde_json::from_str(&cleaned).map_err(|e| AdapterError::new(format!("jsonc: {e}")))?;
if !doc.is_object() {
return Err(AdapterError::new(
"jsonc: document root must be an object (spec F0.3)",
));
}
Ok(config_from_object(convert(&doc, "")?, origin))
}
pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Config, AdapterError> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)
.map_err(|e| AdapterError::new(format!("jsonc: {}: {e}", path.display())))?;
parse(&text, Some(&path.display().to_string()))
}
fn convert(v: &JsonValue, at: &str) -> Result<HoconValue, AdapterError> {
match v {
JsonValue::Object(m) => {
let mut out: IndexMap<String, HoconValue> = IndexMap::new();
for (k, e) in m {
let path = if at.is_empty() {
k.clone()
} else {
format!("{at}.{k}")
};
out.insert(k.clone(), convert(e, &path)?);
}
Ok(HoconValue::Object(out))
}
JsonValue::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for (i, e) in items.iter().enumerate() {
out.push(convert(e, &format!("{at}[{i}]"))?);
}
Ok(HoconValue::Array(out))
}
JsonValue::String(s) => Ok(HoconValue::Scalar(ScalarValue::string(s.clone()))),
JsonValue::Bool(b) => Ok(HoconValue::Scalar(ScalarValue::boolean(*b))),
JsonValue::Null => Ok(HoconValue::Scalar(ScalarValue::null())),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(HoconValue::Scalar(ScalarValue::number(i.to_string())))
} else if let Some(f) = n.as_f64() {
if f.is_nan() || f.is_infinite() {
return Err(AdapterError::new(format!(
"jsonc: at {at}: {f} is not representable in HOCON (spec F0.6)"
)));
}
Ok(HoconValue::Scalar(ScalarValue::number(n.to_string())))
} else {
Err(AdapterError::new(format!(
"jsonc: at {at}: {n} does not fit in i64 (spec F0.5)"
)))
}
}
}
}
fn strip_comments(src: &str) -> Result<String, AdapterError> {
let b: Vec<char> = src.chars().collect();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < b.len() {
match b[i] {
'"' => {
let end = end_of_string(&b, i)?;
out.extend(&b[i..end]);
i = end;
}
'/' if i + 1 < b.len() && b[i + 1] == '/' => {
while i < b.len() && b[i] != '\n' && b[i] != '\r' {
i += 1;
}
}
'/' if i + 1 < b.len() && b[i + 1] == '*' => {
let mut j = i + 2;
loop {
if j + 1 >= b.len() {
return Err(AdapterError::new("jsonc: unterminated block comment"));
}
if b[j] == '*' && b[j + 1] == '/' {
break;
}
if b[j] == '\n' || b[j] == '\r' {
out.push(b[j]);
}
j += 1;
}
out.push(' ');
i = j + 2;
}
c => {
out.push(c);
i += 1;
}
}
}
Ok(out)
}
fn end_of_string(b: &[char], i: usize) -> Result<usize, AdapterError> {
let mut j = i + 1;
while j < b.len() {
match b[j] {
'\\' => j += 2,
'"' => return Ok(j + 1),
_ => j += 1,
}
}
Err(AdapterError::new("jsonc: unterminated string literal"))
}
fn strip_trailing_commas(src: &str) -> String {
let b: Vec<char> = src.chars().collect();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < b.len() {
if b[i] == '"' {
match end_of_string(&b, i) {
Ok(end) => {
out.extend(&b[i..end]);
i = end;
continue;
}
Err(_) => {
out.extend(&b[i..]);
return out;
}
}
}
if b[i] == ',' {
let mut j = i + 1;
while j < b.len() && b[j].is_whitespace() {
j += 1;
}
if j < b.len() && (b[j] == '}' || b[j] == ']') {
i += 1;
continue;
}
}
out.push(b[i]);
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn terminators(s: &str) -> String {
s.chars().filter(|c| *c == '\r' || *c == '\n').collect()
}
#[test]
fn stripping_preserves_line_structure() {
for src in [
"{\n /* a\n b */\n \"k\": 1\n}",
"{\r\n /* a\r\n b */\r\n \"k\": 1\r\n}",
"{\r /* a\r b */\r \"k\": 1\r}",
"{\"k\": /*a\rb*/ 1}",
"{\"k\": /*a\r\r\nb*/ 1}",
"{\n // a\n \"k\": 1\n}",
"{\r\n // a\r\n \"k\": 1\r\n}",
"{\r // a\r \"k\": 1\r}",
"{\r\n \"k\": 1,\r\n}",
"[\r 1,\r]",
"{\"k\": \"a/*b*/c\",\r\n \"j\": 2}",
] {
let stripped = strip_trailing_commas(&strip_comments(src).expect(src));
assert_eq!(
terminators(&stripped),
terminators(src),
"line structure changed for {src:?} -> {stripped:?}"
);
}
}
#[test]
fn a_comment_never_joins_its_neighbours() {
for src in ["1/*x*/2", "1/*\r*/2", "1/*\r\n*/2", "tr/*x*/ue"] {
let stripped = strip_comments(src).expect(src);
assert!(
!stripped.contains("12") && !stripped.contains("true"),
"{src:?} spliced into {stripped:?}"
);
}
}
}