1use std::collections::BTreeMap;
63
64mod value;
65mod core;
66mod c_abi;
67#[cfg(feature = "serde")]
68mod serde_bridge;
69mod derive_macro;
70
71pub use crate::value::*;
73pub use crate::core::*;
74pub use crate::c_abi::*;
75
76pub use crate::derive_macro::{SmlSerialize, SmlDeserialize, __private, to_string, from_str};
78pub use swsml_derive::{SmlDeserialize, SmlSerialize};
79
80#[cfg(feature = "serde")]
83pub mod serde {
84 pub use crate::serde_bridge::*;
85}
86
87
88mod tests {
89 use super::*;
90
91 #[test]
94 fn version_defaults_to_v1_when_absent() {
95 let (v, ver) = parse_versioned("a: 1\n").unwrap();
97 assert_eq!(ver, Version::V1);
98 assert_eq!(v.get("a"), Some(&Value::Int(1)));
99 }
100
101 #[test]
102 fn version_declared_as_v1() {
103 let (v, ver) = parse_versioned("@version v1\na: 1\n").unwrap();
104 assert_eq!(ver, Version::V1);
105 assert_eq!(v.get("a"), Some(&Value::Int(1)));
106 }
107
108 #[test]
109 fn version_declaration_is_stripped_not_parsed_as_content() {
110 let v = parse("@version v1\na: 1\n").unwrap();
112 assert_eq!(v.get("a"), Some(&Value::Int(1)));
113 assert!(v.get("version").is_none(), "@version 不应进入数据");
114 }
115
116 #[test]
117 fn unsupported_version_is_rejected() {
118 let err = parse_versioned("@version v99\na: 1\n").unwrap_err();
119 assert!(err.contains("不支持"), "应拒绝不支持的版本,got: {err}");
120 assert!(err.contains("v99"), "错误应含版本号,got: {err}");
121 }
122
123 #[test]
124 fn conflicting_version_is_rejected() {
125 let err = parse_versioned("@version v1\n@version v2\n").unwrap_err();
126 assert!(!err.is_empty());
128 let (_, ver) = parse_versioned("@version v1\n@version v1\n").unwrap();
130 assert_eq!(ver, Version::V1, "重复但一致的声明应被接受");
131 }
132
133 #[test]
134 fn version_is_reserved_as_fragment_name() {
135 let err = parse("@version { x: 1 }\n").unwrap_err();
136 assert!(err.contains("保留") || err.contains("版本声明"), "got: {err}");
137 }
138
139 #[test]
140 fn version_works_with_include() {
141 let d = tmpdir("version");
142 std::fs::write(d.join("p.sml"), "@version v1\nb: 2\n").unwrap();
143 std::fs::write(d.join("main.sml"), "@version v1\ninclude \"p.sml\"\n").unwrap();
144 let (v, ver) = parse_file_versioned(d.join("main.sml")).unwrap();
145 assert_eq!(ver, Version::V1);
146 assert_eq!(v.get("b"), Some(&Value::Int(2)), "版本与 include 应协同");
147 let _ = std::fs::remove_dir_all(&d);
148 }
149
150 #[test]
151 fn version_display_matches_name() {
152 assert_eq!(Version::V1.name(), "v1");
153 assert_eq!(format!("{}", Version::V1), "v1");
154 }
155
156 fn tmpdir(tag: &str) -> std::path::PathBuf {
160 let mut d = std::env::temp_dir();
161 d.push(format!("sml_test_{tag}_{}", std::process::id()));
162 let _ = std::fs::remove_dir_all(&d);
163 std::fs::create_dir_all(&d).expect("create tmpdir");
164 d
165 }
166
167 #[test]
168 fn include_inlines_external_file() {
169 let d = tmpdir("inline");
170 std::fs::write(d.join("part.sml"), "port: 8080\n").unwrap();
171 std::fs::write(d.join("main.sml"), "@version v1\nhost: local\ninclude \"part.sml\"\n").unwrap();
172
173 let v = parse_file(d.join("main.sml")).unwrap();
174 assert_eq!(v.get("host").unwrap().as_str(), Some("local"));
175 assert_eq!(v.get("port"), Some(&Value::Int(8080)));
176 let _ = std::fs::remove_dir_all(&d);
177 }
178
179 #[test]
180 fn include_at_prefix_is_equivalent() {
181 let d = tmpdir("at");
182 std::fs::write(d.join("p.sml"), "b: 2\n").unwrap();
183 std::fs::write(d.join("m.sml"), "@include \"p.sml\"\n").unwrap();
184 let v = parse_file(d.join("m.sml")).unwrap();
185 assert_eq!(v.get("b"), Some(&Value::Int(2)));
186 let _ = std::fs::remove_dir_all(&d);
187 }
188
189 #[test]
190 fn include_resolves_relative_to_including_file() {
191 let d = tmpdir("nested");
193 std::fs::create_dir_all(d.join("sub")).unwrap();
194 std::fs::write(d.join("sub/leaf.sml"), "@version v1\nleaf: yes\n").unwrap();
195 std::fs::write(d.join("sub/mid2.sml"), "@version v1\ninclude \"leaf.sml\"\n").unwrap();
197 std::fs::write(d.join("main.sml"), "@version v1\ninclude \"sub/mid2.sml\"\n").unwrap();
198
199 let v = parse_file(d.join("main.sml")).unwrap();
200 assert_eq!(
201 v.get("leaf").unwrap().as_str(),
202 Some("yes"),
203 "嵌套 include 的路径应相对各自所在目录解析"
204 );
205 let _ = std::fs::remove_dir_all(&d);
206 }
207
208 #[test]
209 fn include_inside_block_injects_fields() {
210 let d = tmpdir("block");
212 std::fs::write(d.join("fields.sml"), "@version v1\nregion: cn-north-1\nzone: a\n").unwrap();
213 std::fs::write(d.join("main.sml"), "@version v1\nserver web {\ninclude \"fields.sml\"\nport: 8080\n}\n").unwrap();
214
215 let v = parse_file(d.join("main.sml")).unwrap();
216 let server = v.get("server").expect("应有 server 块");
217 assert_eq!(server.get("region").unwrap().as_str(), Some("cn-north-1"));
218 assert_eq!(server.get("zone").unwrap().as_str(), Some("a"));
219 assert_eq!(server.get("port"), Some(&Value::Int(8080)));
220 let _ = std::fs::remove_dir_all(&d);
221 }
222
223 #[test]
224 fn include_detects_cycles() {
225 let d = tmpdir("cycle");
226 std::fs::write(d.join("a.sml"), "include \"b.sml\"\n").unwrap();
227 std::fs::write(d.join("b.sml"), "include \"a.sml\"\n").unwrap();
228 let err = parse_file(d.join("a.sml")).unwrap_err();
229 assert!(err.contains("循环引用"), "应报循环引用,got: {err}");
230 let _ = std::fs::remove_dir_all(&d);
231 }
232
233 #[test]
234 fn include_missing_file_is_error() {
235 let d = tmpdir("missing");
236 std::fs::write(d.join("m.sml"), "include \"nope.sml\"\n").unwrap();
237 let err = parse_file(d.join("m.sml")).unwrap_err();
238 assert!(err.contains("nope.sml"), "错误应含缺失文件名,got: {err}");
239 let _ = std::fs::remove_dir_all(&d);
240 }
241
242 #[test]
243 fn hash_in_quoted_string_is_not_a_comment() {
244 assert_eq!(strip_line_comment("k: \"a#b\""), "k: \"a#b\"");
246 assert_eq!(strip_line_comment("k: v # comment"), "k: v ");
247 }
248
249 #[test]
250 fn glob_include_requires_feature() {
251 let d = tmpdir("globoff");
253 std::fs::write(d.join("a.sml"), "x: 1\n").unwrap();
254 std::fs::write(d.join("main.sml"), "@version v1\ninclude \"*.sml\"\n").unwrap();
255 let err = parse_file(d.join("main.sml")).unwrap_err();
256 assert!(err.contains("glob-include"), "应要求 glob-include,got: {err}");
257 let _ = std::fs::remove_dir_all(&d);
258 }
259
260 #[test]
261 fn glob_include_expands_multiple_files() {
262 let d = tmpdir("glob");
264 std::fs::create_dir_all(d.join("lib")).unwrap();
265 std::fs::write(d.join("lib/a.sml"), "@version v1\nx: 1\n").unwrap();
266 std::fs::write(d.join("lib/b.sml"), "@version v1\ny: 2\n").unwrap();
267 std::fs::write(d.join("note.txt"), "ignored\n").unwrap();
268 std::fs::write(d.join("main.sml"), "@version v1\n@feature enable glob-include\ninclude \"lib/*.sml\"\n").unwrap();
269 let v = parse_file(d.join("main.sml")).unwrap();
270 assert_eq!(v.get("x"), Some(&Value::Int(1)));
271 assert_eq!(v.get("y"), Some(&Value::Int(2)));
272 let _ = std::fs::remove_dir_all(&d);
273 }
274
275 #[test]
276 fn regex_include_requires_feature() {
277 let d = tmpdir("regexoff");
278 std::fs::write(d.join("a.sml"), "x: 1\n").unwrap();
279 std::fs::write(d.join("main.sml"), "@version v1\ninclude \"re:.*\\.sml\"\n").unwrap();
280 let err = parse_file(d.join("main.sml")).unwrap_err();
281 assert!(err.contains("regex-include"), "应要求 regex-include,got: {err}");
282 let _ = std::fs::remove_dir_all(&d);
283 }
284
285 #[test]
286 fn regex_include_matches_files() {
287 let d = tmpdir("regex");
288 std::fs::write(d.join("widget_a.sml"), "@version v1\nx: 1\n").unwrap();
289 std::fs::write(d.join("widget_b.sml"), "@version v1\ny: 2\n").unwrap();
290 std::fs::write(d.join("other.sml"), "@version v1\nz: 3\n").unwrap();
291 std::fs::write(
292 d.join("main.sml"),
293 "@version v1\n@feature enable regex-include\ninclude \"re:widget_.*\\.sml\"\n",
294 )
295 .unwrap();
296 let v = parse_file(d.join("main.sml")).unwrap();
297 assert_eq!(v.get("x"), Some(&Value::Int(1)));
298 assert_eq!(v.get("y"), Some(&Value::Int(2)));
299 assert_eq!(v.get("z"), None, "other.sml 不应被正则匹配");
300 let _ = std::fs::remove_dir_all(&d);
301 }
302
303 #[test]
304 fn ext_rewrite_allows_non_sml() {
305 let d = tmpdir("exrew");
307 std::fs::write(d.join("conf.smlc"), "@version v1\nx: 9\n").unwrap();
308 std::fs::write(
309 d.join("main.sml"),
310 "@version v1\n@feature enable ext-rewrite\ninclude \"conf.smlc\"\n",
311 )
312 .unwrap();
313 let v = parse_file(d.join("main.sml")).unwrap();
314 assert_eq!(v.get("x"), Some(&Value::Int(9)));
315 let _ = std::fs::remove_dir_all(&d);
316 }
317
318 #[test]
319 fn include_line_is_not_confused_with_key_named_include() {
320 let f = FeatureSet::baseline();
321 assert_eq!(parse_include_line("key: include", f), Ok(None));
323 assert_eq!(
325 parse_include_line("include \"a.sml\"", f),
326 Ok(Some(vec![IncludeTarget { raw: "a.sml".into(), namespace: None, via_import: false, keys: None }]))
327 );
328 assert_eq!(
330 parse_include_line("@include \"a.sml\"", f),
331 Ok(Some(vec![IncludeTarget { raw: "a.sml".into(), namespace: None, via_import: false, keys: None }]))
332 );
333 assert_eq!(
335 parse_include_line("include \"a.sml\" as ui.form", f),
336 Ok(Some(vec![IncludeTarget { raw: "a.sml".into(), namespace: Some("ui.form".into()), via_import: false, keys: None }]))
337 );
338 assert_eq!(
340 parse_include_line("include \"widgets\"", f),
341 Ok(Some(vec![IncludeTarget { raw: "widgets".into(), namespace: Some("widgets".into()), via_import: false, keys: None }]))
342 );
343 assert_eq!(
345 parse_include_line("import ui.buttons", f),
346 Ok(Some(vec![IncludeTarget { raw: "ui.buttons".into(), namespace: Some("ui.buttons".into()), via_import: true, keys: None }]))
347 );
348 let fm = FeatureSet::all();
350 assert_eq!(
351 parse_include_line("include \"a.sml\", \"b\" as y", fm),
352 Ok(Some(vec![
353 IncludeTarget { raw: "a.sml".into(), namespace: None, via_import: false, keys: None },
354 IncludeTarget { raw: "b".into(), namespace: Some("y".into()), via_import: false, keys: None },
355 ]))
356 );
357 assert_eq!(parse_include_line("# include \"a.sml\"", f), Ok(None));
359 }
360
361 #[test]
362 fn import_partial_keys_both_syntaxes() {
363 let f = FeatureSet::all();
364 assert_eq!(
366 parse_include_line("import \"m.sml\" as w { a, b }", f),
367 Ok(Some(vec![IncludeTarget {
368 raw: "m.sml".into(),
369 namespace: Some("w".into()),
370 via_import: true,
371 keys: Some(vec!["a".into(), "b".into()]),
372 }]))
373 );
374 assert_eq!(
376 parse_include_line("import \"m.sml\" { a, b }", f),
377 Ok(Some(vec![IncludeTarget {
378 raw: "m.sml".into(),
379 namespace: None,
380 via_import: true,
381 keys: Some(vec!["a".into(), "b".into()]),
382 }]))
383 );
384 assert_eq!(
386 parse_include_line("import { a, b } as w in \"m.sml\"", f),
387 Ok(Some(vec![IncludeTarget {
388 raw: "m.sml".into(),
389 namespace: Some("w".into()),
390 via_import: true,
391 keys: Some(vec!["a".into(), "b".into()]),
392 }]))
393 );
394 assert_eq!(
396 parse_include_line("import { a, b } in \"m.sml\"", f),
397 Ok(Some(vec![IncludeTarget {
398 raw: "m.sml".into(),
399 namespace: None,
400 via_import: true,
401 keys: Some(vec!["a".into(), "b".into()]),
402 }]))
403 );
404 assert!(parse_include_line("import \"m.sml\" { }", f).is_err());
406 assert!(parse_include_line("import { a, b } as w", f).is_err());
408 assert!(parse_include_line("import \"*.sml\" { a }", f).is_err());
410 }
411
412 #[test]
415 fn email_in_bare_word_survives() {
416 let v = parse("to: a@b.c\nfrom: \"SML Team <dev@mail.swebase.cn>\"\n").unwrap();
418 assert_eq!(v.get("to").unwrap().as_str(), Some("a@b.c"), "got: {v:?}");
419 assert_eq!(
420 v.get("from").unwrap().as_str(),
421 Some("SML Team <dev@mail.swebase.cn>"),
422 "got: {v:?}"
423 );
424 }
425
426 #[test]
427 fn email_roundtrips_through_to_sml() {
428 let v = Value::Object(BTreeMap::from([(
429 "to".to_string(),
430 Value::Str("dev@mail.swebase.cn".into()),
431 )]));
432 let back = parse(&to_sml(&v)).unwrap();
433 assert_eq!(back, v, "邮箱必须能往返,got:\n{}", to_sml(&v));
434 }
435
436 #[test]
437 fn fragment_definition_still_works() {
438 let v = parse("@base { region: cn }\nregion: &base\n").unwrap();
442 assert_eq!(
443 v.get("region").unwrap().get("region").unwrap().as_str(),
444 Some("cn"),
445 "片段引用应展开为定义的内容,got: {v:?}"
446 );
447 }
448
449 #[test]
452 fn toplevel_array_roundtrips() {
453 let v = Value::Array(vec![
456 Value::Object(BTreeMap::from([
457 ("ts".to_string(), Value::Str("2026-01-01".into())),
458 ("to".to_string(), Value::Str("a@b.c".into())),
459 ])),
460 Value::Object(BTreeMap::from([
461 ("ts".to_string(), Value::Str("2026-01-02".into())),
462 ("to".to_string(), Value::Str("x@y.z".into())),
463 ])),
464 ]);
465 let text = to_sml(&v);
466 let back = parse(&text).unwrap();
467 assert_eq!(back, v, "顶层对象数组必须能往返,got text:\n{text}");
468 }
469
470 #[test]
471 fn toplevel_array_of_scalars_roundtrips() {
472 let v = Value::Array(vec![
473 Value::Int(1),
474 Value::Str("two".into()),
475 Value::Bool(true),
476 ]);
477 let back = parse(&to_sml(&v)).unwrap();
478 assert_eq!(back, v, "顶层标量数组必须能往返");
479 }
480
481 #[test]
482 fn toplevel_object_block_roundtrips() {
483 let mut m = BTreeMap::new();
484 m.insert("k".to_string(), Value::Int(1));
485 let v = Value::Object(m);
486 let back = parse(&to_sml(&v)).unwrap();
487 assert_eq!(back, v, "顶层对象块必须能往返");
488 }
489
490 #[test]
491 fn toplevel_empty_array_roundtrips() {
492 let v = Value::Array(vec![]);
493 let back = parse(&to_sml(&v)).unwrap();
494 assert_eq!(back, v, "空数组必须能往返");
495 }
496
497 #[cfg(feature = "serde")]
500 #[test]
501 fn serde_roundtrip_preserves_shape() {
502 let v = parse("name: John\nage: 27\ntags: [a b]\nnested { k: v }\n").unwrap();
503 let json = serde_json::to_string(&v).unwrap();
504 assert!(json.contains("\"name\":\"John\""), "got: {json}");
506 assert!(json.contains("\"age\":27"), "got: {json}");
507 assert!(json.contains("\"tags\":[\"a\",\"b\"]"), "got: {json}");
508 assert!(json.contains("\"nested\":{\"k\":\"v\"}"), "got: {json}");
509
510 let back: Value = serde_json::from_str(&json).unwrap();
511 assert_eq!(back, v, "serde 往返应还原原值");
512 }
513
514 #[cfg(feature = "serde")]
515 #[test]
516 fn serde_deserializes_json_into_value() {
517 let v: Value = serde_json::from_str(r#"{"s":"x","i":5,"f":1.5,"b":true,"n":null,"a":[1,2]}"#).unwrap();
518 assert_eq!(v.get("s").unwrap().as_str(), Some("x"));
519 assert_eq!(v.get("i"), Some(&Value::Int(5)));
520 assert_eq!(v.get("f"), Some(&Value::Float(1.5)));
521 assert_eq!(v.get("b"), Some(&Value::Bool(true)));
522 assert_eq!(v.get("n"), Some(&Value::Null));
523 assert!(matches!(v.get("a"), Some(Value::Array(a)) if a.len() == 2));
524 }
525
526 #[test]
527 fn nested_array_inside_object_inside_array_survives_roundtrip() {
528 let mut item = BTreeMap::new();
531 item.insert("path".to_string(), Value::Str("a.txt".into()));
532 item.insert(
533 "chunks".to_string(),
534 Value::Array(vec![
535 Value::Str("c1".into()),
536 Value::Str("c2".into()),
537 ]),
538 );
539 let mut root = BTreeMap::new();
540 root.insert(
541 "entries".to_string(),
542 Value::Array(vec![Value::Object(item)]),
543 );
544 let text = to_sml(&Value::Object(root));
545 assert!(!text.contains("[..]"), "嵌套数组不得被缩略: {text}");
546
547 let back = parse(&text).unwrap();
548 let chunks = back.get("entries").and_then(|e| match e {
549 Value::Array(a) => a.first(),
550 _ => None,
551 });
552 let chunks = match chunks {
553 Some(Value::Object(m)) => m.get("chunks"),
554 _ => None,
555 };
556 match chunks {
557 Some(Value::Array(a)) => {
558 assert_eq!(a.len(), 2, "两个块都应保留: {text}");
559 assert_eq!(
560 a.iter().filter_map(|c| c.as_str()).collect::<Vec<_>>(),
561 vec!["c1", "c2"]
562 );
563 }
564 other => panic!("chunks 应解析为数组,实际 {other:?}"),
565 }
566 }
567
568 #[test]
569 fn utf8_in_quoted_string_survives_roundtrip() {
570 let v = parse(r#"note: "修复若干问题""#).unwrap();
574 assert_eq!(
575 v.get("note").and_then(|x| x.as_str()),
576 Some("修复若干问题"),
577 "引号串中的中文不应被破坏"
578 );
579 let v2 = parse("region: 华北").unwrap();
581 assert_eq!(v2.get("region").and_then(|x| x.as_str()), Some("华北"));
582 let v3 = parse(r#"k: "\u{4fee}\u{590d}""#).unwrap();
584 assert_eq!(v3.get("k").and_then(|x| x.as_str()), Some("修复"));
585 }
586
587 #[test]
588 fn nested_depth_within_limit_succeeds() {
589 let open = "a: {".repeat(120);
591 let close = "}".repeat(120);
592 let text = format!("{open} leaf: 1{close}");
593 let v = parse(&text);
594 assert!(v.is_ok(), "120 层嵌套应解析成功,实际 {v:?}");
595 }
596
597 #[test]
598 fn deep_nesting_exceeding_limit_is_rejected() {
599 let open = "a: {".repeat(1000);
601 let close = "}".repeat(1000);
602 let text = format!("{open} leaf: 1{close}");
603 let v = parse(&text);
604 assert!(
605 v.is_err(),
606 "超深嵌套必须被拒绝(返回 Err),而不是栈溢出崩溃;实际 {v:?}"
607 );
608 assert!(
609 v.unwrap_err().contains("嵌套过深"),
610 "错误信息应提示嵌套过深"
611 );
612 }
613
614 #[test]
615 fn serialize_string_literalization_is_prevented() {
616 for (raw, expect) in [
618 ("true", "\"true\""),
619 ("false", "\"false\""),
620 ("null", "\"null\""),
621 ("8080", "\"8080\""),
622 ("1.5", "\"1.5\""),
623 ("inf", "\"inf\""),
624 ] {
625 let v = Value::Object({
626 let mut m = BTreeMap::new();
627 m.insert("k".into(), Value::Str(raw.into()));
628 m
629 });
630 let out = to_sml(&v);
631 assert!(
632 out.contains(expect),
633 "字符串 {:?} 序列化应加引号得到 {},实际: {:?}",
634 raw,
635 expect,
636 out
637 );
638 let back = parse(&out).unwrap();
640 assert_eq!(
641 back.get("k").and_then(|x| x.as_str()),
642 Some(raw),
643 "round-trip 后 {:?} 应仍是字符串",
644 raw
645 );
646 }
647 }
648
649 #[test]
650 fn b8_unicode_escape_rejects_short_and_invalid() {
651 let short = r#"k: "\uD80""#;
655 assert!(
656 parse(short).is_err(),
657 "B8: \\u 后不足 4 位十六进制必须报错,实际解析为 {:?}",
658 parse(short)
659 );
660 for bad in [r#"k: "\uD800""#, r#"k: "\uZZZZ""#, r#"k: "\u""#] {
662 assert!(
663 parse(bad).is_err(),
664 "B8: 非法 \\u 转义 {:?} 必须报错",
665 bad
666 );
667 }
668 let good = r#"k: "\u4e2d""#; let v = parse(good).expect("B8: 合法 \\u4e2d 应成功");
671 assert_eq!(v.get("k").and_then(|x| x.as_str()), Some("中"));
672 }
673
674 #[test]
675 fn b9_unterminated_string_errors() {
676 let unclosed = "k: \"hello";
678 assert!(
679 parse(unclosed).is_err(),
680 "B9: 未闭合字符串必须报错,实际 {:?}",
681 parse(unclosed)
682 );
683 let dangling_escape = "k: \"abc\\";
685 assert!(
686 parse(dangling_escape).is_err(),
687 "B9: 转义符后 EOF 必须报错,实际 {:?}",
688 parse(dangling_escape)
689 );
690 let after = "k: \"hi\"\nother: 1";
692 let v = parse(after).expect("B9: 正常多行应成功");
693 assert_eq!(
694 match v.get("other") {
695 Some(Value::Int(i)) => Some(*i),
696 _ => None,
697 },
698 Some(1)
699 );
700 }
701
702 #[test]
703 fn b10_overflow_int_preserved_as_string() {
704 let big = "k: 9223372036854775808"; let v = parse(big).expect("B10: 大整数应可解析");
708 match v.get("k") {
709 Some(Value::Str(s)) => {
710 assert_eq!(s, "9223372036854775808", "B10: 超 i64 整数应保留为字符串");
711 let out = to_sml(&v);
713 let back = parse(&out).expect("B10: round-trip 应成功");
714 assert_eq!(
715 back.get("k").and_then(|x| x.as_str()),
716 Some("9223372036854775808")
717 );
718 }
719 other => panic!("B10: 超 i64 整数应保留为 Str,实际 {:?}", other),
720 }
721 let normal = "k: 12345";
723 let v = parse(normal).unwrap();
724 assert_eq!(
725 match v.get("k") {
726 Some(Value::Int(i)) => Some(*i),
727 _ => None,
728 },
729 Some(12345)
730 );
731 }
732
733 #[test]
734 fn b11_fragment_reference_is_value_not_merge() {
735 assert!(
739 parse("@base { region: cn }\nserver web { &base port: 8080 }").is_err(),
740 "B11: 块内裸写 &base 必须报错(不是合并语义)"
741 );
742 let ok = "@base { region: cn-north-1 }\nregion: &base";
744 let v = parse(ok).expect("B11: region: &base 应成功");
745 let region = v.get("region").expect("B11: 应有 region 键");
746 assert!(
747 matches!(region, Value::Object(_)),
748 "B11: &base 作为值应展开为对象,实际 {:?}",
749 region
750 );
751 assert_eq!(
752 region.get("region").and_then(|x| x.as_str()),
753 Some("cn-north-1")
754 );
755 }
756
757 #[test]
758 fn serialize_comment_prefix_is_quoted() {
759 for raw in ["--flag", "//path", "/*x*/", "_*x*_"] {
761 let v = Value::Object({
762 let mut m = BTreeMap::new();
763 m.insert("k".into(), Value::Str(raw.into()));
764 m
765 });
766 let out = to_sml(&v);
767 assert!(out.contains(&format!("\"{}\"", raw)), "{} 应被引号, 得 {:?}", raw, out);
768 let back = parse(&out).unwrap();
769 assert_eq!(back.get("k").and_then(|x| x.as_str()), Some(raw));
770 }
771 }
772
773 #[test]
774 fn serialize_comma_and_brackets_are_quoted() {
775 for raw in ["a,b", "a[b", "a]b", "[", "]"] {
777 let v = Value::Object({
778 let mut m = BTreeMap::new();
779 m.insert("k".into(), Value::Str(raw.into()));
780 m
781 });
782 let out = to_sml(&v);
783 assert!(out.contains(&format!("\"{}\"", raw)), "{} 应被引号, 得 {:?}", raw, out);
784 let back = parse(&out).unwrap();
785 assert_eq!(back.get("k").and_then(|x| x.as_str()), Some(raw));
786 }
787 }
788
789 #[test]
790 fn serialize_keys_are_quoted_when_needed() {
791 for key in ["a b", "a#b", "a,b", "http://x"] {
793 let mut m = BTreeMap::new();
794 m.insert(key.to_string(), Value::Int(1));
795 let out = to_sml(&Value::Object(m));
796 let back = parse(&out).unwrap();
797 let got = back.get(key).and_then(|x| match x {
798 Value::Int(n) => Some(*n),
799 _ => None,
800 });
801 assert_eq!(got, Some(1), "键 {:?} 应可 round-trip, 得 {:?}", key, out);
802 }
803 }
804
805 #[test]
806 fn serialize_float_keeps_decimal_point() {
807 let v = Value::Object({
809 let mut m = BTreeMap::new();
810 m.insert("f".into(), Value::Float(1.0));
811 m
812 });
813 let out = to_sml(&v);
814 let back = parse(&out).unwrap();
815 assert_eq!(back.get("f"), Some(&Value::Float(1.0)), "Float(1.0) 不能变成 Int, 得 {:?}", out);
816 }
817
818 #[test]
819 fn parse_nested_array_roundtrip() {
820 let v = parse("a: [[1 2] [3 4]]").unwrap();
822 let a = match v.get("a") {
823 Some(Value::Array(a)) => a,
824 _ => panic!("a 应为数组"),
825 };
826 assert_eq!(a.len(), 2);
827 assert_eq!(
828 match &a[0] {
829 Value::Array(x) => x.len(),
830 _ => 0,
831 },
832 2
833 );
834 }
835
836 #[test]
837 fn parse_basic() {
838 let text = "firstName: John\nage: 27\nisAlive: true\nspouse: null\n";
839 let v = parse(text).unwrap();
840 assert_eq!(v.get("firstName"), Some(&Value::Str("John".into())));
841 assert_eq!(v.get("age"), Some(&Value::Int(27)));
842 assert_eq!(v.get("isAlive"), Some(&Value::Bool(true)));
843 assert_eq!(v.get("spouse"), Some(&Value::Null));
844 }
845
846 #[test]
847 fn parse_nested() {
848 let text = "address:\n{\n streetAddress: \"21 2nd Street\"\n state: NY\n}\n";
849 let v = parse(text).unwrap();
850 assert_eq!(
851 v.get("address.streetAddress"),
852 Some(&Value::Str("21 2nd Street".into()))
853 );
854 assert_eq!(v.get("address.state"), Some(&Value::Str("NY".into())));
855 }
856
857 #[test]
858 fn parse_array() {
859 let text = "phoneNumbers:\n[\n { type: home }\n { type: office }\n]\n";
860 let v = parse(text).unwrap();
861 if let Some(Value::Array(a)) = v.get("phoneNumbers") {
862 assert_eq!(a.len(), 2);
863 assert_eq!(a[0].get("type"), Some(&Value::Str("home".into())));
864 } else {
865 panic!("not array");
866 }
867 }
868
869 #[test]
870 fn parse_fragment() {
871 let text = "@base { region: cn-north-1 }\nserver web { &base }\n";
872 let v = parse(text).unwrap();
873 assert_eq!(
875 v.get("server.&base.region"),
876 Some(&Value::Str("cn-north-1".into()))
877 );
878 assert_eq!(v.get("server.__type"), Some(&Value::Str("server".into())));
879 assert_eq!(v.get("server.__name"), Some(&Value::Str("web".into())));
880 }
881
882 #[test]
883 fn roundtrip() {
884 let text = "name: myapp\nport: 8080\nflags: [ a b c ]\n";
885 let v = parse(text).unwrap();
886 let out = to_sml(&v);
887 let v2 = parse(&out).unwrap();
888 assert_eq!(v, v2);
889 }
890
891 #[test]
892 fn env_inline() {
893 unsafe { std::env::set_var("SML_TEST_VAR", "hello") };
895 let text = "greeting: $env.SML_TEST_VAR\n";
896 let v = parse(text).unwrap();
897 assert_eq!(v.get("greeting"), Some(&Value::Str("hello".into())));
898 }
899
900 #[test]
901 fn c_abi_json_bridge() {
902 let text = "name: John\nage: 27\n";
903 let v = parse(text).unwrap();
904 let j = jsonify(&v);
905 assert!(j.contains("\"name\":\"John\""));
906 let back = json_to_value(&j).unwrap();
907 assert_eq!(back, v);
908 }
909}
910
911#[cfg(test)]
916mod feature {
917 use super::*;
918
919 #[test]
920 fn feature_unknown_name_errors() {
921 let r = parse("@feature enable nope\nx: 1\n");
922 assert!(r.is_err());
923 assert!(r.unwrap_err().contains("未知特性"));
924 }
925
926 #[test]
927 fn feature_whitelist_narrows() {
928 let v = match parse("@feature whitelist bareword-string,include\nx: John\n").unwrap() {
930 Value::Object(m) => m,
931 _ => panic!("应为对象"),
932 };
933 assert_eq!(v.get("x"), Some(&Value::Str("John".into())));
934 }
935
936 #[test]
937 fn feature_blacklist_removes() {
938 let r = parse("@feature blacklist bareword-string\nx: John\n");
940 assert!(r.is_err());
941 assert!(r.unwrap_err().contains("字符串必须加引号"));
942 }
943
944 #[test]
945 fn feature_mode_whitelist_enable() {
946 let r = parse("@feature mode whitelist\n@feature enable fragment\nx: &frag\n");
948 assert!(r.is_ok());
950 }
951
952 #[test]
953 fn caller_allowed_intersection_empty_errors() {
954 let allowed = FeatureSet::none().with(Feature::Env);
956 let r = parse_with_features(
957 "@feature mode whitelist\n@feature enable contract\nx: 1\n",
958 allowed,
959 );
960 assert!(r.is_err());
961 }
962
963 #[test]
964 fn caller_allowed_subset_ok() {
965 let allowed = FeatureSet::all();
967 let (v, eff) = parse_with_features(
968 "@feature whitelist bareword-string,include\nx: John\n",
969 allowed,
970 )
971 .unwrap();
972 assert!(eff.has(Feature::BarewordStr));
973 assert!(eff.has(Feature::Include));
974 assert!(!eff.has(Feature::Env));
975 assert_eq!(v.get("x"), Some(&Value::Str("John".into())));
976 }
977
978 #[test]
979 fn feature_namespace_include() {
980 let dir = std::env::temp_dir().join("sml_feat_ns_test");
984 let _ = std::fs::create_dir_all(&dir);
985 let sub = dir.join("sub.sml");
986 let main = dir.join("main.sml");
987 std::fs::write(&sub, "a: 1\nb: 2\n").unwrap();
988 let rel = format!("include \"sub.sml\" as pkg\n");
990 std::fs::write(&main, &rel).unwrap();
991 let v = match parse_file(&main) {
992 Ok(v) => v,
993 Err(e) => {
994 let _ = std::fs::remove_dir_all(&dir);
995 panic!("parse_file 失败: {e}");
996 }
997 };
998 let _ = std::fs::remove_dir_all(&dir);
999 let pkg = match v.get("pkg") {
1000 Some(Value::Object(m)) => m.clone(),
1001 _ => panic!("pkg 应为对象"),
1002 };
1003 assert_eq!(pkg.get("a"), Some(&Value::Int(1)));
1004 assert_eq!(pkg.get("b"), Some(&Value::Int(2)));
1005 }
1006
1007 #[test]
1008 fn version_v3_disables_bareword() {
1009 let r = parse("@version v3\nname: John\n");
1011 assert!(r.is_err());
1012 let v = parse("@version v3\nname: \"John\"\nage: 27\n").unwrap();
1014 assert_eq!(v.get("name"), Some(&Value::Str("John".into())));
1015 assert_eq!(v.get("age"), Some(&Value::Int(27)));
1016 }
1017
1018 #[test]
1019 fn feature_base_derives_strict() {
1020 let r = parse("@feature base v3\nname: John\n");
1022 assert!(r.is_err());
1023 }
1024}