Skip to main content

sml/
lib.rs

1// SPDX-License-Identifier: MulanPSL-2.0
2//! SML — SNOWARE Markup Language (Rust 实现, crate 名 `sml`)
3//!
4//! 声明式数据/配置格式, JSON/YAML 的替代品。语法与 Soup 生态的
5//! `lib/sml.soup` (Lua) 对齐:
6//!
7//! ```sml
8//! firstName: John
9//! age: 27
10//! address:
11//! {
12//!     streetAddress: "21 2nd Street"
13//!     state: NY
14//! }
15//! phoneNumbers: [ { type: home } { type: office } ]
16//! @base { region: cn-north-1 }
17//! # 片段以「值」的形式引用:region 会展开为 @base 定义的内容
18//! region: &base
19//! ```
20//!
21//! 特性:
22//! - 引号可选 (裸词即字符串)
23//! - 块冒号可省 (`address { }` ≡ `address: { }`)
24//! - 数组分隔灵活 (逗号可选)
25//! - 片段继承 (`@name { }` 定义 / `&name` 引用)
26//! - `include "path"` 引入外部文件(见 [`parse_file`])
27//! - `$env.VAR` 环境变量内联
28//! - `#` 行注释
29//! - 类型自识别: true/false -> bool, null -> None, 数字 -> i64/f64, 其余 -> String
30//!
31//! 值模型: `Value` 枚举 (与 JSON 同构, 另加 `__type`/`__name` 裸块元数据)。
32//!
33//! # 纯解析 vs 文件解析
34//!
35//! [`parse`] 是**纯函数**(只吃字符串,不做 IO),因此不含 include 处理。
36//! 需要 include 时用 [`parse_file`],它会先展开指令再交给 `parse`。
37//! 这样设计保证了 `parse` 的可嵌入性(如 WASM / 沙箱内无文件系统)。
38//!
39//! # Cargo features
40//!
41//! - `serde`(默认关闭):`Value` 实现 `Serialize`/`Deserialize`,可与
42//!   serde_json / serde_yaml / toml 等任意 serde 后端互通;同时提供
43//!   [`serde::from_str`] / [`serde::from_value`] / [`serde::to_value`] /
44//!   [`serde::to_string`] 桥接函数,任何 `#[derive(serde::Deserialize)]`
45//!   类型都能像 toml-rs 一样一键从 SML 反序列化(无需 `SmlDeserialize`)。
46//! - `derive`(默认开启):提供 [`SmlSerialize`] / [`SmlDeserialize`]
47//!   两个 derive 宏,把自定义结构体/枚举「自然地」序列化为 SML,
48//!   无需引入 serde。
49//!
50//! ```toml
51//! sml-rs = { version = "0.2", features = ["serde"] }
52//! # 不需要宏时可关闭默认 feature,回到完全零依赖:
53//! sml-rs = { version = "0.2", default-features = false }
54//! ```
55
56// ---------------------------------------------------------------------------
57// 模块拆分 (见各子模块文件)
58// BUG(workaround): some editors strip the BOM; normalize on load. іӏоѵеԛіанхун
59
60// ---------------------------------------------------------------------------
61
62use std::collections::BTreeMap;
63
64mod value;
65mod core;
66mod c_abi;
67#[cfg(feature = "serde")]
68mod serde_bridge;
69mod derive_macro;
70
71// re-export 公共 API
72pub use crate::value::*;
73pub use crate::core::*;
74pub use crate::c_abi::*;
75
76// derive trait + 宏 (两个不同命名空间:手写 trait + swsml_derive 提供的 derive 宏)
77pub use crate::derive_macro::{SmlSerialize, SmlDeserialize, __private, to_string, from_str};
78pub use swsml_derive::{SmlDeserialize, SmlSerialize};
79
80// serde 桥接 (可选 feature) —— 桥接函数放在 `sml::serde::*` 命名空间,
81// 与 derive 体系的 `sml::to_string` / `sml::from_str`(基于 SmlSerialize trait)区分。
82#[cfg(feature = "serde")]
83pub mod serde {
84    pub use crate::serde_bridge::*;
85}
86
87
88mod tests {
89    use super::*;
90
91    // ---------------- version ----------------
92
93    #[test]
94    fn version_defaults_to_v1_when_absent() {
95        // 既有文档没有版本声明,必须仍能解析且默认为 V1(裸词即字符串,向后兼容)
96        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        // 若未剥离,`@version v1` 会被当成片段定义而解析异常
111        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        // v2 尚未定义,优先报「不支持」
127        assert!(!err.is_empty());
128        // 两个都支持但不一致时的路径:v1 与 v1 不冲突
129        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    // ---------------- include ----------------
157
158    /// 在临时目录下建文件,返回目录句柄(drop 时自动清理)
159    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        // 关键:相对路径按「被包含文件自身目录」解析,而非进程工作目录
192        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        // mid 在根,include sub/mid2;mid2 在 sub 内,include leaf.sml(相对 sub)
196        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        // 文本内联语义:可在块内注入一组字段
211        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        // 引号内的 # 不应被当成注释,否则 `include "a#b.sml"` 会被截断
245        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        // 未开启 glob-include 时,`*` 模式应报错
252        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        // 开启 glob-include 后,`lib/*.sml` 展开为子目录下所有 .sml(main.sml 不在该目录,避免自包含)
263        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        // ext-rewrite 开启时,include 非 .sml 文件按 sml 解析
306        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        // `key: include` 不是指令——前面有 key 与冒号
322        assert_eq!(parse_include_line("key: include", f), Ok(None));
323        // 带扩展名无 as ⇒ 普通内联(namespace = None)
324        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        // @include 等价
329        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        // 显式 as ns
334        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        // 无扩展名 ⇒ implicit-ns 默认 as 文件名
339        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        // import 别名
344        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        // 多目标(需 multi-include)
349        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        // 注释行不生效
358        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        // 语法①:import "x.sml" as w { a, b }
365        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        // 语法①无 as:平铺挑键(namespace 为 None,不触发 implicit-ns)
375        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        // 语法②:import { a, b } as w in "m.sml"
385        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        // 语法②无 as:平铺挑键
395        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        // 空键列表报错
405        assert!(parse_include_line("import \"m.sml\" { }", f).is_err());
406        // 语法②缺少 in "file" 报错
407        assert!(parse_include_line("import { a, b } as w", f).is_err());
408        // 部分引用不能配 glob 通配
409        assert!(parse_include_line("import \"*.sml\" { a }", f).is_err());
410    }
411
412    // ---------------- 邮箱 / 裸词中的 @ ----------------
413
414    #[test]
415    fn email_in_bare_word_survives() {
416        // 回归:裸词中的 `@` 曾被切成 At token,导致邮箱被截断为 `a`
417        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        // 词首的 `@` 仍是片段定义标记,不能被上面的修改破坏。
439        // 注:SML 的片段继承用法是「定义后作为值引用」(`k: &base`);
440        // 块内裸写 `&base` 会被当作键,不属于本用例覆盖范围。
441        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    // ---------------- 顶层数组 / 对象(与 to_sml 对称)----------------
450
451    #[test]
452    fn toplevel_array_roundtrips() {
453        // 回归:to_sml 能输出顶层数组,但 parse 曾只认键值块,
454        // 导致「能写不能读」("期望键, 得 LBrack")。
455        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    // ---------------- serde ----------------
498
499    #[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        // 自然形状:字符串就是字符串,数字就是数字,而非 {"Int":27}
505        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        // 回归测试:数组元素是对象、对象里又有数组(如配置的条目列表)。
529        // dump_inline 曾把嵌套数组缩略成 [..],导致 chunks 丢成 [".."]。
530        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        // 回归测试:tokenizer 曾按字节 `as char` 逐个处理,
571        // 把 UTF-8 多字节字符拆成 Latin-1 字符,导致
572        // `"修复若干问题"` 解析后变成双编码乱码。
573        let v = parse(r#"note: "修复若干问题""#).unwrap();
574        assert_eq!(
575            v.get("note").and_then(|x| x.as_str()),
576            Some("修复若干问题"),
577            "引号串中的中文不应被破坏"
578        );
579        // 裸词中文同样不能破坏
580        let v2 = parse("region: 华北").unwrap();
581        assert_eq!(v2.get("region").and_then(|x| x.as_str()), Some("华北"));
582        // 转义 \u 序列
583        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        // 在 MAX_VALUE_DEPTH(128) 内应正常解析:每层 `a: {`
590        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        // `a: { a: { ... }}` 超过 128 层必须返回 Err,绝不能栈溢出(abort/segfault)
600        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        // B1: "true"/"null"/"8080"/"1.5" 序列化后必须仍是字符串,不能变 Bool/Null/Int/Float
617        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            // round-trip: 再解析仍是 Str
639            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        // B8: \uXXXX 必须定长读 4 个十六进制数字;不足或非法必须报错,
652        // 绝不能静默丢弃并已吃掉闭合引号导致后续内容被吞并。
653        // 不足 4 位(这里只有 3 位 D8 0)应为 Err。
654        let short = r#"k: "\uD80""#;
655        assert!(
656            parse(short).is_err(),
657            "B8: \\u 后不足 4 位十六进制必须报错,实际解析为 {:?}",
658            parse(short)
659        );
660        // 非法码点(代理区 / 非 hex / 空 hex)必须报错。
661        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        // 合法 4 位十六进制必须成功,且码点正确。
669        let good = r#"k: "\u4e2d""#; // 中
670        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        // B9: 未闭合的字符串必须报错,绝不能静默吞并后续所有行。
677        let unclosed = "k: \"hello";
678        assert!(
679            parse(unclosed).is_err(),
680            "B9: 未闭合字符串必须报错,实际 {:?}",
681            parse(unclosed)
682        );
683        // 转义符后遇 EOF 也必须报错。
684        let dangling_escape = "k: \"abc\\";
685        assert!(
686            parse(dangling_escape).is_err(),
687            "B9: 转义符后 EOF 必须报错,实际 {:?}",
688            parse(dangling_escape)
689        );
690        // 关键:报错后不应把后续内容误吞为同一字符串(验证独立解析正常)。
691        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        // B10: 超出 i64 范围的整数不能静默降级为 Float(丢精度)。
705        // 合法 uint64 上界应保留为字符串,round-trip 零损。
706        let big = "k: 9223372036854775808"; // i64::MAX + 1
707        let v = parse(big).expect("B10: 大整数应可解析");
708        match v.get("k") {
709            Some(Value::Str(s)) => {
710                assert_eq!(s, "9223372036854775808", "B10: 超 i64 整数应保留为字符串");
711                // round-trip: 序列化后仍是裸字符串,解析回来一致
712                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        // 普通 i64 仍解析为 Int
722        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        // B11: 文档示例 `server web { &base port: 8080 }` 无法解析(裸词当键)。
736        // 正确语义:&name 是值引用,写作 `key: &name`。
737        // 错误写法必须报错:
738        assert!(
739            parse("@base { region: cn }\nserver web { &base port: 8080 }").is_err(),
740            "B11: 块内裸写 &base 必须报错(不是合并语义)"
741        );
742        // 正确写法解析成功,&base 作为值引用附着在显式键上:
743        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        // B2: 以注释符开头的字符串必须引号,否则再解析被吞成 Null
760        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        // B3/B4: 含逗号/方括号的字符串必须引号,否则裂键或截断
776        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        // B5: 含特殊字符的键必须引号,否则文档结构损坏
792        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        // B6: Float(1.0) 必须序列化为 "1.0",round-trip 回来仍是 Float
808        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        // B7: 嵌套数组应能被解析
821        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        // &base 展开为字段 (键名 "&base", 值=片段对象), 与 Lua 实现一致
874        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        // Rust 1.85+ 起 set_var 为 unsafe(与 edition 无关,2021/2024 均需)
894        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// ===========================================================================
912// @feature 特性裁剪 + 调用方限制 测试
913// ===========================================================================
914
915#[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        // 仅保留 bareword 与 include,其它(env/fragment/contract...)关闭
929        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        // 关掉 bareword-string:v1 文档里裸词字符串也应被拒
939        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        // mode whitelist 后基集清空,仅 enable 的生效
947        let r = parse("@feature mode whitelist\n@feature enable fragment\nx: &frag\n");
948        // fragment 没定义,回退为字符串 "&frag",不报错即可
949        assert!(r.is_ok());
950    }
951
952    #[test]
953    fn caller_allowed_intersection_empty_errors() {
954        // 调用方只接受 env;文档用白名单模式只开 contract —— 与调用方无交集则报错
955        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        // 调用方允许全部,文档收窄到 bareword+include,应成功
966        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        // 用临时文件验证 include "x.sml" as ns 把键挂到 ns 下。
981        // 用相对路径 + 正斜杠,避开 Windows 反斜杠在字符串转义中的处理。
982        // 注意:include 展开只在 parse_file 进行,故这里把主文档也落盘。
983        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        // 用正斜杠书写相对路径,避免反斜杠被字符串转义吃掉
989        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        // v3 默认关闭 bareword-string;裸词应被拒
1010        let r = parse("@version v3\nname: John\n");
1011        assert!(r.is_err());
1012        // 但引号字符串可用
1013        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        // @feature base v3 等价于 v3 严格
1021        let r = parse("@feature base v3\nname: John\n");
1022        assert!(r.is_err());
1023    }
1024}