use hocon::{tokenize, HoconError, TokenKind};
fn subst_segments(input: &str) -> Vec<(String, usize, usize)> {
let tokens = tokenize(input).unwrap();
let t = tokens
.iter()
.find(|t| t.kind == TokenKind::Substitution)
.expect("subst token");
let payload = t.subst.as_ref().expect("subst payload");
payload
.segments
.iter()
.map(|s| (s.text.clone(), s.line, s.col))
.collect()
}
#[test]
fn segment_position_unquoted_path() {
let segs = subst_segments("${foo.bar}");
assert_eq!(segs[0].0, "foo");
assert_eq!(segs[0].1, 1);
assert_eq!(segs[0].2, 3);
assert_eq!(segs[1].0, "bar");
assert_eq!(segs[1].2, 7);
}
#[test]
fn segment_position_quoted_dot_separator() {
let segs = subst_segments(r#"${"a"."b"}"#);
assert_eq!(segs[0].0, "a");
assert_eq!(segs[0].2, 3); assert_eq!(segs[1].0, "b");
assert_eq!(segs[1].2, 7); }
#[test]
fn segment_position_multiline() {
let segs = subst_segments("x=1\ny=${foo}");
assert_eq!(segs[0].0, "foo");
assert_eq!(segs[0].1, 2);
assert_eq!(segs[0].2, 5);
}
#[test]
fn segment_position_ws_concat() {
let segs = subst_segments(r#"${"a" "b"}"#);
assert_eq!(segs.len(), 1);
assert_eq!(segs[0].0, "a b");
assert_eq!(segs[0].2, 3);
}
#[test]
fn segment_position_empty_quoted_key() {
let segs = subst_segments(r#"${""}"#);
assert_eq!(segs.len(), 1);
assert_eq!(segs[0].0, "");
assert_eq!(segs[0].2, 3);
}
fn parse_err_pos(err: &HoconError) -> (usize, usize) {
match err {
HoconError::Parse(e) => (e.line, e.col),
other => panic!("expected HoconError::Parse, got {:?}", other),
}
}
#[test]
fn error_position_invalid_escape_inside_body() {
let err = hocon::parse(r#"x=${"a\xb"}"#).unwrap_err();
assert!(
err.to_string().contains("invalid escape sequence"),
"msg = {}",
err
);
let (line, col) = parse_err_pos(&err);
assert_eq!(line, 1, "line should be 1, got {} (err = {})", line, err);
assert!(
(3..=11).contains(&col),
"col {} not in subst body [3, 11] (err = {})",
col,
err
);
}
#[test]
fn error_position_empty_path() {
let err = hocon::parse("x=${}").unwrap_err();
assert!(
err.to_string().contains("empty substitution path"),
"err = {}",
err
);
let (line, _col) = parse_err_pos(&err);
assert_eq!(line, 1, "line should be 1, got {} (err = {})", line, err);
}
#[test]
fn surrogate_codepoint_rejected() {
let err = hocon::parse(r#"x="a\uD800b""#).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("invalid unicode escape"), "msg = {}", msg);
}
#[test]
fn surrogate_codepoint_rejected_inside_subst() {
let err = hocon::parse(r#"x=${"a\uD800b"}"#).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("invalid unicode escape"), "msg = {}", msg);
}
#[test]
fn lex_subst_list_suffix_basic() {
let tokens = hocon::tokenize("${X[]}").unwrap();
let t = tokens
.iter()
.find(|t| t.kind == hocon::TokenKind::Substitution)
.expect("Substitution token");
let p = t.subst.as_ref().expect("SubstPayload");
assert!(p.list_suffix, "list_suffix must be true for ${{X[]}}");
assert_eq!(p.segments.len(), 1, "exactly one segment");
assert_eq!(p.segments[0].text, "X");
assert!(!p.optional);
}
#[test]
fn lex_subst_list_suffix_optional() {
let tokens = hocon::tokenize("${?X[]}").unwrap();
let t = tokens
.iter()
.find(|t| t.kind == hocon::TokenKind::Substitution)
.expect("Substitution token");
let p = t.subst.as_ref().expect("SubstPayload");
assert!(p.list_suffix, "list_suffix must be true for ${{?X[]}}");
assert!(p.optional);
assert_eq!(p.segments[0].text, "X");
}
#[test]
fn lex_subst_list_suffix_multipath() {
let tokens = hocon::tokenize("${FOO.BAR[]}").unwrap();
let p = tokens
.iter()
.find(|t| t.kind == hocon::TokenKind::Substitution)
.and_then(|t| t.subst.as_ref())
.expect("SubstPayload");
assert!(p.list_suffix);
assert_eq!(p.segments.len(), 2);
assert_eq!(p.segments[0].text, "FOO");
assert_eq!(p.segments[1].text, "BAR");
}
#[test]
fn lex_subst_no_list_suffix_for_plain() {
let tokens = hocon::tokenize("${X}").unwrap();
let p = tokens
.iter()
.find(|t| t.kind == hocon::TokenKind::Substitution)
.and_then(|t| t.subst.as_ref())
.expect("SubstPayload");
assert!(!p.list_suffix, "plain ${{X}} must NOT set list_suffix");
}
#[test]
fn lex_subst_list_suffix_e7_space() {
let tokens = hocon::tokenize("${X []}").unwrap();
let p = tokens
.iter()
.find(|t| t.kind == hocon::TokenKind::Substitution)
.and_then(|t| t.subst.as_ref())
.expect("SubstPayload");
assert!(p.list_suffix, "space before [] must still set list_suffix");
assert_eq!(p.segments.len(), 1);
assert_eq!(p.segments[0].text, "X");
}
#[test]
fn lex_subst_list_suffix_e7_tab() {
let input = "${X\t[]}";
let tokens = hocon::tokenize(input).unwrap();
let p = tokens
.iter()
.find(|t| t.kind == hocon::TokenKind::Substitution)
.and_then(|t| t.subst.as_ref())
.expect("SubstPayload");
assert!(p.list_suffix, "tab before [] must still set list_suffix");
assert_eq!(p.segments[0].text, "X");
}
#[test]
fn lex_subst_list_suffix_empty_path_errors() {
let err = hocon::tokenize("${[]}").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("empty segment before") || msg.contains("empty substitution path"),
"expected empty-segment/path error, got: {}",
msg
);
}
#[test]
fn lex_subst_list_suffix_missing_close_bracket_errors() {
let err = hocon::tokenize("${X[}").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("expected ']'"),
"expected ']' missing error, got: {}",
msg
);
}
#[test]
fn lex_subst_list_suffix_whitespace_inside_brackets_errors() {
let err = hocon::tokenize("${X[ ]}").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("expected ']'"),
"expected ']' missing error for whitespace inside [], got: {}",
msg
);
}
#[test]
fn lex_subst_list_suffix_double_suffix_errors() {
let result = hocon::tokenize("x = ${X[][]}");
assert!(result.is_err(), "${{X[][]}} must be a lex/parse error");
}