use sml::{parse, Value};
use std::collections::HashSet;
fn keys_of(v: &Value) -> HashSet<String> {
let mut out = HashSet::new();
collect(v, &mut out);
out
}
fn collect(v: &Value, out: &mut HashSet<String>) {
match v {
Value::Object(m) => {
for (k, val) in m {
out.insert(k.clone());
collect(val, out);
}
}
Value::Array(a) => {
for x in a {
collect(x, out);
}
}
_ => {}
}
}
fn input_keys(src: &str) -> Vec<String> {
let mut out = Vec::new();
let mut depth = 0i32;
let mut in_contract = false;
for raw in src.lines() {
let t = raw.trim();
if t.starts_with("@contract") {
in_contract = true;
depth = 0;
}
if !in_contract && !t.is_empty() {
out.extend(key_of_line(t));
}
if in_contract {
for c in t.chars() {
match c {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
}
if depth <= 0 {
in_contract = false;
}
}
}
out
}
fn key_of_line(t: &str) -> Option<String> {
if t.starts_with('#')
|| t.starts_with("//")
|| t.starts_with("--")
|| t.starts_with("/*")
|| t.starts_with("_*")
|| t.starts_with('@')
{
return None;
}
let is_ident = |c: char| c.is_alphanumeric() || matches!(c, '_' | '.' | '-' | '$');
let tok: String = t.chars().take_while(|&c| is_ident(c)).collect();
if tok.is_empty() {
return None;
}
let rest = t[tok.len()..].trim_start();
if rest.starts_with(':') || rest.starts_with('{') {
return Some(tok);
}
let w: String = rest.chars().take_while(|&c| is_ident(c)).collect();
let r2 = rest[w.len()..].trim_start();
if !w.is_empty() && r2.starts_with('{') {
return Some(tok);
}
None
}
fn assert_keys_preserved(src: &str) {
if let Ok(v) = parse(src) {
let got = keys_of(&v);
for k in input_keys(src) {
assert!(
got.contains(&k),
"内容被静默丢弃:源码中出现的键 `{k}` 未出现在解析结果中\n\
结果: {v:?}\n\
源码:\n{src}"
);
}
}
}
const BASE: &str = r#"
@contract Cfg loose {
host: str
port: int default 465 min 1 max 65535
}
@is Cfg
host: example.com
port: 465
mailer {
retry: 3
tags: [ alpha beta ]
}
extra: 1
"#;
#[test]
fn deleting_structural_chars_never_drops_keys() {
for (i, ch) in BASE.char_indices() {
if matches!(ch, '{' | '}' | '[' | ']' | '"' | ':' | '@') {
let mut s = String::with_capacity(BASE.len() - 1);
s.push_str(&BASE[..i]);
s.push_str(&BASE[i + ch.len_utf8()..]);
assert_keys_preserved(&s);
}
}
}
#[test]
fn inserting_at_before_scalar_never_drops_keys() {
let lines: Vec<&str> = BASE.lines().collect();
for n in 0..lines.len() {
let target = lines[n].trim();
if target.ends_with('{') || target.starts_with('@') || target.starts_with('}') {
continue;
}
if lines[n..].iter().any(|l| l.trim().ends_with('{')) {
continue;
}
let mut v = String::new();
for (m, l) in lines.iter().enumerate() {
if n == m {
v.push('@');
}
v.push_str(l);
v.push('\n');
}
assert_keys_preserved(&v);
}
}
#[test]
fn truncating_never_drops_visible_keys() {
for cut in [10usize, 40, 80, 120, 180] {
if cut < BASE.len() {
assert_keys_preserved(&BASE[..cut]);
}
}
}
#[test]
fn unclosed_must_error() {
let e = parse("@contract C { a: str\nreal_key: 1\n").unwrap_err();
assert!(e.to_string().contains("未闭合"), "实际错误: {e}");
}
#[test]
fn contract_missing_closing_brace_must_error() {
for src in [
"@contract C { a: str\nreal_key: 1\n",
"@contract C { a: str\nk1: 1\nk2: 2\nk3: 3\n",
] {
let e = parse(src).unwrap_err();
assert!(
e.to_string().contains("未闭合"),
"源码 {src:?} 应报未闭合,实际: {e}"
);
}
assert!(parse("@contract C { a: str\nother { x: 1 }\n").is_err());
}
#[test]
fn lone_at_with_scalar_must_error() {
let e = parse("@\nk: 1\n").unwrap_err();
assert!(e.to_string().contains("不是合法指令"), "实际: {e}");
}
#[test]
fn unknown_directive_with_scalar_must_error() {
for src in ["@nosuch\nk: 1\n", "@versoin v1\nk: 1\n"] {
let e = parse(src).unwrap_err();
assert!(e.to_string().contains("不是合法指令"), "源码 {src:?} 实际: {e}");
}
}
#[test]
fn unclosed_block_must_error() {
for src in ["a {\n x: 1\nb: 2\n", "a {\n x: 1\nb {\n y: 2\n}\n"] {
let e = parse(src).unwrap_err();
assert!(e.to_string().contains("未闭合"), "源码 {src:?} 实际: {e}");
}
}
#[test]
fn extra_closing_brace_must_error() {
let e = parse("a: 1\n}\n").unwrap_err();
assert!(e.to_string().contains("多余"), "实际: {e}");
}
#[test]
fn undefined_fragment_must_error() {
let e = parse("k: &nope\n").unwrap_err();
assert!(e.to_string().contains("未定义的片段"), "实际: {e}");
}
#[test]
fn unknown_escape_must_error() {
for src in ["x: \"\\z\"\n", "re: \"\\d+\"\n", "p: \"C:\\Users\"\n"] {
let e = parse(src).unwrap_err();
assert!(e.to_string().contains("未知转义"), "源码 {src:?} 实际: {e}");
}
}
#[test]
fn leading_zero_preserved_as_string() {
for (src, want) in [
("mode: 0755\n", "0755"),
("mode: 0644\n", "0644"),
("id: 007\n", "007"),
] {
let v = parse(src).unwrap_or_else(|e| panic!("{src:?} 不应报错: {e}"));
assert_eq!(
v.get(src.split(':').next().unwrap()).and_then(|x| x.as_str()),
Some(want),
"源码 {src:?} 前导零被破坏"
);
}
}
#[test]
fn bare_block_third_word_preserved_in_args() {
let v = parse("server web prod { port: 80 }\n").unwrap();
let inner = v.get("server").expect("缺 server");
let args = inner.get("__args").expect("第三个词 `prod` 被丢弃,应保留于 __args");
assert!(
format!("{args:?}").contains("prod"),
"__args 中应含 prod,实际: {args:?}"
);
}
#[test]
fn comma_in_bareword_must_error() {
let e = parse("a: x,y\n").unwrap_err();
assert!(e.to_string().contains("逗号"), "实际: {e}");
}
#[test]
fn multiline_string_keeps_directive_text() {
let v = parse("note: \"line1\n@version v1\nline2\"\nother: 2\n").unwrap();
let note = v.get("note").and_then(|x| x.as_str()).expect("缺 note");
assert!(note.contains("@version"), "字符串内指令被剥离: {note:?}");
}
#[test]
fn lone_at_anywhere_must_error() {
for src in [
"@\nblk { x: 1 }\n", "@\nserver web { x: 1 }\n",
"@ blk { x: 1 }\n", "@\tblk { x: 1 }\n", "@\nk: 1\n",
"a: 1\n@\nb: 2\n",
"o {\n@\nk: 1\n}\n", "a: 1\n@\n", ] {
let e = parse(src).unwrap_err();
assert!(
e.to_string().contains("孤立的 `@`"),
"源码 {src:?} 应报「孤立的 @」,实际: {e}"
);
}
}
#[test]
fn lone_at_in_middle_must_error_not_partially_drop() {
let src = "a: 1\n@\nb { x: 2 }\nc: 3\n";
let e = parse(src).unwrap_err();
assert!(
e.to_string().contains("孤立的 `@`"),
"孤立 @ 不应静默吞掉紧随的块,实际: {e}"
);
}
#[test]
fn at_related_valid_syntax_still_works() {
let v = parse("@base { x: 1 }\nk: &base\n").unwrap();
assert!(
matches!(v.get("k").and_then(|x| x.get("x")), Some(Value::Int(1))),
"片段引用失效: {:?}",
v.get("k")
);
let v = parse("@f type: Server { x: 1 }\nk: &f\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("Server"));
let v = parse("@f type: S name: prod { x: 1 }\nk: &f\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("prod"));
let v = parse("@type { x: 1 }\nk: &type\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("x"));
let v = parse("@name { y: 2 }\nk: &name\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("y"));
let v = parse("@contract C { a: str }\n@is C\na: hello\n").unwrap();
assert_eq!(v.get("a").and_then(|x| x.as_str()), Some("hello"));
assert!(parse("@version v1\na: 1\n").is_ok());
assert!(parse("@feature enable bareword-string\na: 1\n").is_ok());
let v = parse("email: a@b.c\n").unwrap();
assert_eq!(v.get("email").and_then(|x| x.as_str()), Some("a@b.c"));
let v = parse("k: x@y\n").unwrap();
assert_eq!(v.get("k").and_then(|x| x.as_str()), Some("x@y"));
}
#[test]
fn v4_explicit_fragment_params_work() {
let v = parse("@f type: Server { host: a }\nk: &f\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("Server"));
let v = parse("@f type: S name: prod { host: a }\nk: &f\n").unwrap();
let s = format!("{:?}", v.get("k"));
assert!(s.contains("S") && s.contains("prod"), "实际: {s}");
let v = parse("@f name: prod { host: a }\nk: &f\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("prod"));
let v = parse("@f { host: a }\nk: &f\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("host"));
let v = parse("@f type: \"My Type\" { host: a }\nk: &f\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("My Type"));
}
#[test]
fn v4_positional_fragment_params_rejected() {
for src in [
"@f Server { host: a }\n",
"@f Server prod { host: a }\n",
] {
let e = parse(src).unwrap_err();
assert!(
e.contains("type:") && e.contains("name:"),
"错误信息应指引显式写法,源码 {src:?},实际: {e}"
);
}
}
#[test]
fn unknown_directive_name_with_block_must_error() {
for src in ["@nosuch\nblk { x: 1 }\n", "@versoin v1\nblk { x: 1 }\n"] {
let e = parse(src).unwrap_err();
assert!(
e.contains("type:") || e.contains("不是合法指令"),
"源码 {src:?} 应报错,实际: {e}"
);
}
}
#[test]
fn v4_duplicate_params_rejected() {
let e = parse("@f type: A type: B { x: 1 }\n").unwrap_err();
assert!(e.contains("重复"), "实际: {e}");
let e = parse("@f name: A name: B { x: 1 }\n").unwrap_err();
assert!(e.contains("重复"), "实际: {e}");
}
#[test]
fn fragment_named_type_or_name_still_works() {
let v = parse("@type { x: 1 }\nk: &type\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("x"));
let v = parse("@name { y: 2 }\nk: &name\n").unwrap();
assert!(format!("{:?}", v.get("k")).contains("y"));
}
#[test]
fn v4_version_accepted() {
let v = parse("@version v4\nk: \"s\"\n").unwrap();
assert!(v.get("k").is_some());
assert!(parse("@version 4\nk: \"s\"\n").is_ok());
assert!(parse("@version v4\nk: bare\n").is_err());
assert!(parse("@version v4\n@f S { x: 1 }\n").is_err());
assert!(parse("@version v4\n@f type: S { x: 1 }\nk: &f\n").is_ok());
}