use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Array(Vec<Value>),
Object(BTreeMap<String, Value>),
}
impl Value {
pub fn get(&self, path: &str) -> Option<&Value> {
let mut cur = self;
for seg in path.split('.') {
match cur {
Value::Object(m) => cur = m.get(seg)?,
_ => return None,
}
}
Some(cur)
}
pub fn as_str(&self) -> Option<&str> {
match self {
Value::Str(s) => Some(s),
_ => None,
}
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", to_sml(self))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeSpec {
Any,
ContractRef(String),
Str,
Int,
Num,
Bool,
Array(Box<TypeSpec>),
Enum(Vec<String>),
}
impl TypeSpec {
fn name(&self) -> String {
match self {
TypeSpec::Any => "any".into(),
TypeSpec::Str => "str".into(),
TypeSpec::Int => "int".into(),
TypeSpec::Num => "num".into(),
TypeSpec::Bool => "bool".into(),
TypeSpec::Array(inner) => format!("[{}]", inner.name()),
TypeSpec::Enum(vals) => format!("enum [{}]", vals.join(" ")),
TypeSpec::ContractRef(name) => name.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct FieldSpec {
pub ty: TypeSpec,
pub required: bool,
pub default: Option<Value>,
pub min: Option<f64>,
pub max: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct Contract {
pub name: String,
pub fields: BTreeMap<String, FieldSpec>,
pub allow_extra: bool,
}
fn check_type(
contract: &str,
field: &str,
spec: &FieldSpec,
v: &Value,
contracts: &BTreeMap<String, Contract>,
) -> Result<(), String> {
if let TypeSpec::ContractRef(ref_name) = &spec.ty {
return match v {
Value::Object(_) => {
let mut sub = match v {
Value::Object(m) => m.clone(),
_ => unreachable!(),
};
let target = contracts.get(ref_name).ok_or_else(|| {
format!(
"sml: 字段 `{}` 引用了未定义的契约 `{}`(契约 `{}`)",
field, ref_name, contract
)
})?;
apply_contract(target, &mut sub, contracts)?;
Ok(())
}
_ => Err(format!(
"sml: 字段 `{}` 应为块并按契约 `{}` 校验,实际为 {}(契约 `{}`)",
field,
ref_name,
value_kind(v),
contract
)),
};
}
let ok = match (&spec.ty, v) {
(TypeSpec::Any, _) => true,
(TypeSpec::Str, Value::Str(_)) => true,
(TypeSpec::Int, Value::Int(_)) => true,
(TypeSpec::Num, Value::Int(_)) | (TypeSpec::Num, Value::Float(_)) => true,
(TypeSpec::Bool, Value::Bool(_)) => true,
(TypeSpec::Enum(vals), Value::Str(s)) => vals.iter().any(|x| x == s),
(TypeSpec::Enum(vals), Value::Int(i)) => vals.iter().any(|x| x == &i.to_string()),
(TypeSpec::Array(inner), Value::Array(items)) => items.iter().all(|it| {
check_type(
contract,
field,
&FieldSpec { ty: (**inner).clone(), required: true, default: None, min: None, max: None },
it,
contracts,
)
.is_ok()
}),
_ => false,
};
if !ok {
return Err(format!(
"sml: 字段 `{}` 类型应为 {},实际为 {}(契约 `{}`)",
field,
spec.ty.name(),
value_kind(v),
contract
));
}
if spec.min.is_some() || spec.max.is_some() {
let n = match v {
Value::Int(i) => Some(*i as f64),
Value::Float(f) => Some(*f),
_ => None,
};
if let Some(n) = n {
if let Some(lo) = spec.min {
if n < lo {
return Err(format!(
"sml: 字段 `{}` 值 {} 小于下界 {}(契约 `{}`)",
field, n, lo, contract
));
}
}
if let Some(hi) = spec.max {
if n > hi {
return Err(format!(
"sml: 字段 `{}` 值 {} 大于上界 {}(契约 `{}`)",
field, n, hi, contract
));
}
}
}
}
Ok(())
}
fn value_kind(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::Str(_) => "str",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn apply_contract(
c: &Contract,
node: &mut BTreeMap<String, Value>,
contracts: &BTreeMap<String, Contract>,
) -> Result<(), String> {
if !c.allow_extra {
for k in node.keys() {
if !c.fields.contains_key(k) {
return Err(format!(
"sml: 字段 `{}` 未在契约 `{}` 中声明(严格模式;如需允许额外字段请在契约名后写 `loose`)",
k, c.name
));
}
}
}
for (k, spec) in &c.fields {
match node.get(k) {
None => {
if let Some(d) = &spec.default {
node.insert(k.clone(), d.clone());
} else if spec.required {
return Err(format!(
"sml: 字段 `{}` 必填但缺失(契约 `{}`)",
k, c.name
));
}
}
Some(v) => {
if matches!(spec.ty, TypeSpec::ContractRef(_)) {
check_type(&c.name, k, spec, v, contracts)?;
let mut sub = match v {
Value::Object(m) => m.clone(),
_ => unreachable!("check_type 已保证为块"),
};
check_type_contract_ref(&c.name, k, spec, &mut sub, contracts)?;
node.insert(k.clone(), Value::Object(sub));
} else {
check_type(&c.name, k, spec, v, contracts)?;
}
}
}
}
Ok(())
}
fn check_type_contract_ref(
contract: &str,
field: &str,
spec: &FieldSpec,
sub: &mut BTreeMap<String, Value>,
contracts: &BTreeMap<String, Contract>,
) -> Result<(), String> {
let ref_name = match &spec.ty {
TypeSpec::ContractRef(n) => n.clone(),
_ => return Ok(()),
};
let target = contracts.get(&ref_name).ok_or_else(|| {
format!(
"sml: 字段 `{}` 引用了未定义的契约 `{}`(契约 `{}`)",
field, ref_name, contract
)
})?;
check_type(contract, field, spec, &Value::Object(sub.clone()), contracts)?;
apply_contract(target, sub, contracts)
}
#[derive(Debug, Clone, PartialEq)]
enum Tok {
LBrace, RBrace, LBrack, RBrack, Comma, Colon, At, Str(String), Word(String), }
fn tokenize(text: &str) -> Result<Vec<Tok>, String> {
let mut toks = Vec::new();
let mut chars = text.chars().peekable();
let mut buf = String::new();
let mut flush = |buf: &mut String, toks: &mut Vec<Tok>| {
if !buf.is_empty() {
toks.push(Tok::Word(std::mem::take(buf)));
}
};
while let Some(c) = chars.next() {
match c {
'#' => {
for c2 in chars.by_ref() {
if c2 == '\n' {
break;
}
}
}
'"' => {
flush(&mut buf, &mut toks);
let mut s = String::new();
loop {
match chars.next() {
Some('"') => break,
Some('\\') => {
match chars.next() {
Some('n') => s.push('\n'),
Some('t') => s.push('\t'),
Some('r') => s.push('\r'),
Some('0') => s.push('\0'),
Some('"') => s.push('"'),
Some('\\') => s.push('\\'),
Some('u') => {
let mut hex = String::new();
if chars.peek() == Some(&'{') {
chars.next();
for c2 in chars.by_ref() {
if c2 == '}' {
break;
}
hex.push(c2);
}
} else {
for _ in 0..4 {
if let Some(c2) = chars.next() {
hex.push(c2);
}
}
}
if let Ok(cp) = u32::from_str_radix(&hex, 16) {
if let Some(ch) = char::from_u32(cp) {
s.push(ch);
}
}
}
Some(other) => s.push(other),
None => break,
}
}
Some(other) => s.push(other),
None => break,
}
}
toks.push(Tok::Str(s));
}
'{' => {
flush(&mut buf, &mut toks);
toks.push(Tok::LBrace);
}
'}' => {
flush(&mut buf, &mut toks);
toks.push(Tok::RBrace);
}
'[' => {
flush(&mut buf, &mut toks);
toks.push(Tok::LBrack);
}
']' => {
flush(&mut buf, &mut toks);
toks.push(Tok::RBrack);
}
',' => {
flush(&mut buf, &mut toks);
toks.push(Tok::Comma);
}
':' => {
flush(&mut buf, &mut toks);
toks.push(Tok::Colon);
}
'@' => {
if buf.is_empty() {
toks.push(Tok::At);
} else {
buf.push(c);
}
}
' ' | '\t' | '\n' | '\r' => {
flush(&mut buf, &mut toks);
}
_ => {
buf.push(c);
}
}
}
flush(&mut buf, &mut toks);
Ok(toks)
}
fn coerce_word(w: &str, fragments: &BTreeMap<String, Value>) -> Value {
match w {
"true" => return Value::Bool(true),
"false" => return Value::Bool(false),
"null" => return Value::Null,
_ => {}
}
if let Some(ev) = w.strip_prefix("$env.") {
return Value::Str(std::env::var(ev).unwrap_or_default());
}
if let Some(name) = w.strip_prefix('&') {
if let Some(v) = fragments.get(name) {
return v.clone();
}
return Value::Str(w.to_string());
}
if let Ok(i) = w.parse::<i64>() {
return Value::Int(i);
}
if let Ok(f) = w.parse::<f64>() {
return Value::Float(f);
}
Value::Str(w.to_string())
}
struct Parser {
toks: Vec<Tok>,
i: usize,
fragments: BTreeMap<String, Value>,
contracts: BTreeMap<String, Contract>,
}
impl Parser {
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.i)
}
fn next(&mut self) -> Option<Tok> {
let t = self.toks.get(self.i).cloned();
if t.is_some() {
self.i += 1;
}
t
}
fn parse_contract_body(&mut self) -> Result<BTreeMap<String, FieldSpec>, String> {
let mut fields: BTreeMap<String, FieldSpec> = BTreeMap::new();
loop {
match self.peek().cloned() {
None | Some(Tok::RBrace) => {
self.next();
break;
}
Some(Tok::Comma) => {
self.next();
}
_ => {
let key = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => {
return Err(format!("sml: 契约字段期望键, 得 {:?}", other))
}
};
if self.peek() == Some(&Tok::Colon) {
self.next();
} else {
return Err(format!("sml: 契约字段 `{}` 后须有冒号", key));
}
let spec = self.parse_field_spec()?;
fields.insert(key, spec);
}
}
}
Ok(fields)
}
fn parse_field_spec(&mut self) -> Result<FieldSpec, String> {
let ty = match self.next() {
Some(Tok::Word(w)) => match w.as_str() {
"str" => TypeSpec::Str,
"int" => TypeSpec::Int,
"num" => TypeSpec::Num,
"bool" => TypeSpec::Bool,
"any" => TypeSpec::Any,
"enum" => {
if self.peek() != Some(&Tok::LBrack) {
return Err("sml: `enum` 后须为 [ ... ]".into());
}
self.next();
let mut vals = Vec::new();
loop {
match self.peek().cloned() {
None | Some(Tok::RBrack) => {
self.next();
break;
}
Some(Tok::Comma) => {
self.next();
}
Some(Tok::Word(s)) | Some(Tok::Str(s)) => {
vals.push(s);
self.next();
}
_ => {
self.next();
}
}
}
TypeSpec::Enum(vals)
}
other => TypeSpec::ContractRef(other.to_string()),
},
Some(Tok::LBrack) => {
let inner = match self.next() {
Some(Tok::Word(w)) => match w.as_str() {
"str" => TypeSpec::Str,
"int" => TypeSpec::Int,
"num" => TypeSpec::Num,
"bool" => TypeSpec::Bool,
"any" => TypeSpec::Any,
other => {
return Err(format!("sml: 未知数组元素类型 `{}`", other))
}
},
other => {
return Err(format!("sml: 数组元素类型期望标识符, 得 {:?}", other))
}
};
if self.peek() == Some(&Tok::RBrack) {
self.next();
}
TypeSpec::Array(Box::new(inner))
}
other => return Err(format!("sml: 字段类型期望标识符, 得 {:?}", other)),
};
let mut required = true;
let mut default = None;
let mut min = None;
let mut max = None;
loop {
let is_next_field = matches!(self.peek(), Some(Tok::Word(_)))
&& matches!(self.toks.get(self.i + 1), Some(Tok::Colon));
if is_next_field {
break;
}
match self.peek().cloned() {
Some(Tok::Word(w)) => match w.as_str() {
"optional" => {
required = false;
self.next();
}
"required" => {
required = true;
self.next();
}
"default" => {
self.next();
default = Some(match self.next() {
Some(Tok::Word(w2)) => coerce_word(&w2, &self.fragments),
Some(Tok::Str(s)) => Value::Str(s),
other => {
return Err(format!("sml: default 期望值, 得 {:?}", other))
}
});
}
"min" => {
self.next();
min = Some(self.parse_spec_number()?);
}
"max" => {
self.next();
max = Some(self.parse_spec_number()?);
}
_ => break,
},
_ => break,
}
}
Ok(FieldSpec { ty, required, default, min, max })
}
fn parse_spec_number(&mut self) -> Result<f64, String> {
match self.next() {
Some(Tok::Word(w)) => {
w.parse::<f64>().map_err(|_| format!("sml: 期望数字, 得 `{}`", w))
}
other => Err(format!("sml: 期望数字, 得 {:?}", other)),
}
}
fn parse_block(&mut self, closing: Option<Tok>) -> Result<Value, String> {
let mut node: BTreeMap<String, Value> = BTreeMap::new();
let mut applied_contract: Option<String> = None;
loop {
let tok = match self.peek().cloned() {
None => break,
Some(t) => t,
};
match tok {
Tok::RBrace | Tok::RBrack => {
if let Some(cl) = &closing {
if *cl == tok {
self.next();
break;
}
}
break;
}
Tok::Comma => {
self.next();
}
Tok::At => {
self.next();
let fname = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
_ => return Err("sml: @ 后需片段名".into()),
};
if self.peek() == Some(&Tok::Colon) {
self.next();
}
if fname == "contract" {
let cname = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => {
return Err(format!("sml: @contract 后须契约名, 得 {:?}", other))
}
};
let mut allow_extra = false;
if let Some(Tok::Word(w)) = self.peek().cloned() {
if w == "loose" {
allow_extra = true;
self.next();
}
}
if self.peek() != Some(&Tok::LBrace) {
return Err(format!("sml: @contract {} 后须 {{ ... }}", cname));
}
self.next();
let fields = self.parse_contract_body()?;
self.contracts.insert(
cname.clone(),
Contract { name: cname, fields, allow_extra },
);
continue;
}
if fname == "is" {
let cname = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => {
return Err(format!("sml: @is 后须契约名, 得 {:?}", other))
}
};
applied_contract = Some(cname);
continue;
}
let mut ftype: Option<String> = None;
let mut farg: Option<String> = None;
if let Some(Tok::Word(s)) = self.peek().cloned() {
if *self.peek().unwrap() != Tok::LBrace {
self.next();
ftype = Some(s);
if let Some(Tok::Word(s2)) = self.peek().cloned() {
if *self.peek().unwrap() != Tok::LBrace {
self.next();
farg = Some(s2);
}
}
}
}
if self.peek() == Some(&Tok::LBrace) {
self.next();
let mut sub = match self.parse_block(Some(Tok::RBrace))? {
Value::Object(m) => m,
other => {
let mut m = BTreeMap::new();
m.insert("_value".into(), other);
m
}
};
if let Some(t) = ftype {
sub.insert("__type".into(), Value::Str(t));
}
if let Some(a) = farg {
sub.insert("__name".into(), Value::Str(a));
}
self.fragments.insert(fname, Value::Object(sub));
}
}
_ => {
let key = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => return Err(format!("sml: 期望键, 得 {:?}", other)),
};
let colon = self.peek() == Some(&Tok::Colon);
if colon {
self.next();
}
let val = self.parse_value(&key, colon)?;
if let Some(existing) = node.get_mut(&key) {
match existing {
Value::Array(a) => a.push(val),
_ => {
let old = node.remove(&key).unwrap();
node.insert(key, Value::Array(vec![old, val]));
}
}
} else {
node.insert(key, val);
}
}
}
}
if let Some(cname) = applied_contract {
let c = self
.contracts
.get(&cname)
.cloned()
.ok_or_else(|| format!("sml: 未定义的契约 `{}`", cname))?;
apply_contract(&c, &mut node, &self.contracts)?;
}
Ok(Value::Object(node))
}
fn parse_value(&mut self, key: &str, colon: bool) -> Result<Value, String> {
if !colon && matches!(self.peek(), Some(Tok::Word(_))) {
let mut probe = self.i;
let mut found_block = false;
while probe < self.toks.len() {
match &self.toks[probe] {
Tok::Word(_) | Tok::Str(_) => probe += 1,
Tok::LBrace => {
found_block = true;
break;
}
_ => break,
}
}
if found_block {
let mut args: Vec<Value> = Vec::new();
while let Some(t) = self.peek().cloned() {
match t {
Tok::Word(w) => {
args.push(coerce_word(&w, &self.fragments));
self.next();
}
Tok::Str(_) => {
if let Some(Tok::Str(s)) = self.next() {
args.push(Value::Str(s));
}
}
_ => break,
}
}
if self.peek() == Some(&Tok::LBrace) {
self.next();
let mut sub = self.parse_block(Some(Tok::RBrace))?;
if let Value::Object(m) = &mut sub {
m.insert("__type".into(), Value::Str(key.to_string()));
if args.len() == 1 {
m.insert("__name".into(), args.remove(0));
}
}
return Ok(sub);
}
}
}
match self.peek().cloned() {
Some(Tok::LBrace) => {
self.next();
self.parse_block(Some(Tok::RBrace))
}
Some(Tok::LBrack) => {
self.next();
self.parse_array()
}
Some(tok @ (Tok::Word(_) | Tok::Str(_))) => {
let v = match tok {
Tok::Word(w) => coerce_word(&w, &self.fragments),
Tok::Str(s) => {
let ev = s.strip_prefix("$env.");
match ev {
Some(name) => Value::Str(std::env::var(name).unwrap_or_default()),
None => Value::Str(s),
}
}
_ => unreachable!(),
};
self.next();
Ok(v)
}
Some(Tok::RBrace) | Some(Tok::RBrack) | Some(Tok::Comma) | None => {
if colon {
Ok(Value::Null)
} else {
Ok(coerce_word(key, &self.fragments))
}
}
_ => Err("sml: 语法错误".into()),
}
}
fn parse_array(&mut self) -> Result<Value, String> {
let mut arr = Vec::new();
loop {
match self.peek().cloned() {
None => break,
Some(Tok::RBrack) => {
self.next();
break;
}
Some(Tok::Comma) => {
self.next();
}
Some(Tok::LBrace) => {
self.next();
arr.push(self.parse_block(Some(Tok::RBrace))?);
}
Some(Tok::Word(w)) => {
arr.push(coerce_word(&w, &self.fragments));
self.next();
}
Some(Tok::Str(_)) => {
if let Some(Tok::Str(s)) = self.next() {
arr.push(Value::Str(s));
}
}
_ => break,
}
}
Ok(Value::Array(arr))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Version {
V1,
}
impl Version {
pub const CURRENT: Version = Version::V1;
fn from_word(w: &str) -> Option<Version> {
match w {
"v1" | "1" => Some(Version::V1),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Version::V1 => "v1",
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
fn version_directive(line: &str) -> Result<Option<String>, String> {
let content = strip_line_comment(line).trim();
let toks = match tokenize(content) {
Ok(t) => t,
Err(_) => return Ok(None),
};
match toks.as_slice() {
[Tok::At, Tok::Word(w), Tok::Word(v)] if w == "version" => Ok(Some(v.clone())),
[Tok::At, Tok::Word(w), Tok::Str(v)] if w == "version" => Ok(Some(v.clone())),
[Tok::At, Tok::Word(w), ..] if w == "version" => Err(
"`@version` 是版本声明指令,须写作 `@version v1`;`version` 不可作为片段名".into(),
),
_ => Ok(None),
}
}
fn strip_version(text: &str) -> Result<(String, Option<Version>), String> {
let mut declared: Option<Version> = None;
let mut rest = String::new();
for line in text.lines() {
if let Some(lit) = version_directive(line)? {
let v = Version::from_word(&lit).ok_or_else(|| {
format!(
"不支持的 SML 版本 `{lit}`(本实现支持 {})",
Version::CURRENT.name()
)
})?;
match declared {
None => declared = Some(v),
Some(prev) if prev != v => {
return Err(format!("@version 冲突:{} 与 {}", prev.name(), v.name()))
}
Some(_) => {}
}
continue;
}
rest.push_str(line);
rest.push('\n');
}
Ok((rest, declared))
}
pub fn parse_versioned(text: &str) -> Result<(Value, Version), String> {
let (rest, declared) = strip_version(text)?;
Ok((parse_impl(&rest)?, declared.unwrap_or(Version::CURRENT)))
}
pub fn parse_file_versioned(path: impl AsRef<Path>) -> Result<(Value, Version), String> {
let path = path.as_ref();
let text =
std::fs::read_to_string(path).map_err(|e| format!("读取失败 {}: {e}", path.display()))?;
let base = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let expanded = resolve_includes(&text, &base)?;
parse_versioned(&expanded)
}
pub fn parse(text: &str) -> Result<Value, String> {
let (rest, _) = strip_version(text)?;
parse_impl(&rest)
}
fn parse_impl(text: &str) -> Result<Value, String> {
let toks = tokenize(text)?;
let mut p = Parser {
toks,
i: 0,
fragments: BTreeMap::new(),
contracts: BTreeMap::new(),
};
match p.peek() {
Some(Tok::LBrack) => {
p.next();
p.parse_array()
}
Some(Tok::LBrace) => {
p.next();
p.parse_block(Some(Tok::RBrace))
}
_ => p.parse_block(None),
}
}
const MAX_INCLUDE_DEPTH: usize = 32;
fn strip_line_comment(line: &str) -> &str {
let bytes = line.as_bytes();
let mut i = 0;
let mut in_quote = false;
while i < bytes.len() {
match bytes[i] {
b'"' => in_quote = !in_quote,
b'\\' if in_quote => i += 1,
b'#' if !in_quote => return &line[..i],
_ => {}
}
i += 1;
}
line
}
fn include_target(line: &str) -> Option<String> {
let content = strip_line_comment(line).trim();
let content = content.strip_prefix('@').unwrap_or(content).trim_start();
let toks = tokenize(content).ok()?;
match toks.as_slice() {
[Tok::Word(w), Tok::Str(p)] if w == "include" => Some(p.clone()),
[Tok::Word(w), Tok::Word(p)] if w == "include" => Some(p.clone()),
_ => None,
}
}
pub fn resolve_includes(text: &str, base: &Path) -> Result<String, String> {
let mut out = String::new();
let mut stack: Vec<PathBuf> = Vec::new();
expand_includes(text, base, &mut out, &mut stack)?;
Ok(out)
}
fn expand_includes(
text: &str,
base: &Path,
out: &mut String,
stack: &mut Vec<PathBuf>,
) -> Result<(), String> {
if stack.len() >= MAX_INCLUDE_DEPTH {
return Err(format!("include 嵌套超过 {MAX_INCLUDE_DEPTH} 层"));
}
for line in text.lines() {
match include_target(line) {
Some(rel) => {
let path = base.join(&rel);
let canon = path
.canonicalize()
.map_err(|e| format!("include 无法定位 {}: {e}", path.display()))?;
if stack.iter().any(|p| p == &canon) {
return Err(format!("include 循环引用: {}", canon.display()));
}
let content = std::fs::read_to_string(&canon)
.map_err(|e| format!("include 读取失败 {}: {e}", canon.display()))?;
let child_base = canon
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
stack.push(canon);
expand_includes(&content, &child_base, out, stack)?;
stack.pop();
}
None => {
out.push_str(line);
out.push('\n');
}
}
}
Ok(())
}
pub fn parse_file(path: impl AsRef<Path>) -> Result<Value, String> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)
.map_err(|e| format!("读取失败 {}: {e}", path.display()))?;
let base = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let expanded = resolve_includes(&text, &base)?;
parse(&expanded)
}
pub fn loads(text: &str) -> Result<Value, ParseError> {
parse(text).map_err(ParseError)
}
#[derive(Debug)]
pub struct ParseError(pub String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "sml parse error: {}", self.0)
}
}
impl std::error::Error for ParseError {}
fn quote_if_needed(s: &str) -> String {
if s.is_empty() || s.contains([' ', '\t', '\n', '\r', ':', '#', '{', '}']) {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
} else {
s.to_string()
}
}
fn dump_value(v: &Value, indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
match v {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => out.push_str(&i.to_string()),
Value::Float(f) => out.push_str(&format!("{}", f)),
Value::Str(s) => out.push_str("e_if_needed(s)),
Value::Array(a) => {
if a.is_empty() {
out.push_str("[]");
} else {
out.push('[');
for e in a {
out.push('\n');
out.push_str(&format!("{}{}", " ".repeat(indent + 1), dump_inline(e)));
}
out.push_str(&format!("\n{}]", pad));
}
}
Value::Object(m) => {
let has_body = m.iter().any(|(k, _)| k != "__type" && k != "__name");
if !has_body {
out.push_str("{}");
return;
}
out.push_str(&format!("\n{}{{", pad));
for (k, val) in m {
if k == "__type" || k == "__name" {
continue;
}
out.push_str(&format!("\n{}{}: ", " ".repeat(indent + 1), k));
dump_value(val, indent + 1, out);
}
out.push_str(&format!("\n{}}}", pad));
}
}
}
fn dump_scalar(v: &Value) -> String {
match v {
Value::Null => "null".into(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Str(s) => quote_if_needed(s),
_ => "".into(),
}
}
fn dump_inline(v: &Value) -> String {
match v {
Value::Object(m) => {
let parts: Vec<String> = m
.iter()
.filter(|(k, _)| k.as_str() != "__type" && k.as_str() != "__name")
.map(|(k, val)| format!("{}: {}", k, dump_inline(val)))
.collect();
format!("{{ {} }}", parts.join(", "))
}
Value::Array(a) => {
let parts: Vec<String> = a.iter().map(dump_inline).collect();
format!("[ {} ]", parts.join(", "))
}
other => dump_scalar(other),
}
}
pub fn to_sml(v: &Value) -> String {
let mut out = String::new();
if let Value::Object(m) = v {
for (k, val) in m {
if k == "__type" || k == "__name" {
continue;
}
out.push_str(&format!("{}: ", k));
dump_value(val, 0, &mut out);
out.push('\n');
}
} else {
out.push_str(&dump_inline(v));
}
out
}
use std::os::raw::{c_char, c_int};
use std::ptr;
fn cstr(s: &str) -> *mut c_char {
let c = std::ffi::CString::new(s).unwrap_or_default();
c.into_raw()
}
#[no_mangle]
pub extern "C" fn sml_parse(text: *const c_char) -> *mut c_char {
if text.is_null() {
return ptr::null_mut();
}
let t = unsafe { std::ffi::CStr::from_ptr(text) }.to_string_lossy().into_owned();
match parse(&t) {
Ok(v) => cstr(&jsonify(&v)),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn sml_dump(json: *const c_char) -> *mut c_char {
if json.is_null() {
return ptr::null_mut();
}
let j = unsafe { std::ffi::CStr::from_ptr(json) }.to_string_lossy().into_owned();
match json_to_value(&j) {
Some(v) => cstr(&to_sml(&v)),
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn sml_free(p: *mut c_char) {
if !p.is_null() {
drop(unsafe { std::ffi::CString::from_raw(p) });
}
}
#[no_mangle]
pub extern "C" fn sml_version() -> *mut c_char {
cstr(concat!("sml ", env!("CARGO_PKG_VERSION")))
}
fn jsonify(v: &Value) -> String {
fn esc(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
match v {
Value::Null => "null".into(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Str(s) => format!("\"{}\"", esc(s)),
Value::Array(a) => {
let parts: Vec<String> = a.iter().map(jsonify).collect();
format!("[{}]", parts.join(","))
}
Value::Object(m) => {
let parts: Vec<String> = m
.iter()
.map(|(k, val)| format!("\"{}\":{}", esc(k), jsonify(val)))
.collect();
format!("{{{}}}", parts.join(","))
}
}
}
fn json_to_value(s: &str) -> Option<Value> {
let bytes = s.as_bytes();
let mut i = 0;
let _n = bytes.len();
let mut skip_ws = |b: &[u8], i: &mut usize| {
while *i < b.len() && matches!(b[*i], b' ' | b'\t' | b'\n' | b'\r') {
*i += 1;
}
};
let mut parse_str = |b: &[u8], i: &mut usize| -> Option<String> {
skip_ws(b, i);
if *i >= b.len() || b[*i] != b'"' {
return None;
}
*i += 1;
let mut out = String::new();
while *i < b.len() {
let c = b[*i];
if c == b'"' {
*i += 1;
return Some(out);
}
if c == b'\\' && *i + 1 < b.len() {
*i += 1;
let e = b[*i];
out.push(match e {
b'n' => '\n',
b't' => '\t',
b'r' => '\r',
b'"' => '"',
b'\\' => '\\',
_ => e as char,
});
} else {
out.push(c as char);
}
*i += 1;
}
None
};
fn parse_val_impl(
b: &[u8],
i: &mut usize,
s: &str,
parse_str: &dyn Fn(&[u8], &mut usize) -> Option<String>,
) -> Option<Value> {
let mut skip_ws = |b: &[u8], i: &mut usize| {
while *i < b.len() && matches!(b[*i], b' ' | b'\t' | b'\n' | b'\r') {
*i += 1;
}
};
skip_ws(b, i);
if *i >= b.len() {
return None;
}
match b[*i] {
b'{' => {
*i += 1;
let mut m = BTreeMap::new();
skip_ws(b, i);
if *i < b.len() && b[*i] == b'}' {
*i += 1;
return Some(Value::Object(m));
}
loop {
skip_ws(b, i);
let k = parse_str(b, i)?;
skip_ws(b, i);
if *i < b.len() && b[*i] == b':' {
*i += 1;
}
let v = parse_val_impl(b, i, s, parse_str)?;
m.insert(k, v);
skip_ws(b, i);
if *i < b.len() && b[*i] == b',' {
*i += 1;
} else if *i < b.len() && b[*i] == b'}' {
*i += 1;
break;
}
}
Some(Value::Object(m))
}
b'[' => {
*i += 1;
let mut a = Vec::new();
skip_ws(b, i);
if *i < b.len() && b[*i] == b']' {
*i += 1;
return Some(Value::Array(a));
}
loop {
a.push(parse_val_impl(b, i, s, parse_str)?);
skip_ws(b, i);
if *i < b.len() && b[*i] == b',' {
*i += 1;
} else if *i < b.len() && b[*i] == b']' {
*i += 1;
break;
}
}
Some(Value::Array(a))
}
b'"' => parse_str(b, i).map(Value::Str),
b't' => {
if s[*i..].starts_with("true") {
*i += 4;
Some(Value::Bool(true))
} else {
None
}
}
b'f' => {
if s[*i..].starts_with("false") {
*i += 5;
Some(Value::Bool(false))
} else {
None
}
}
b'n' => {
if s[*i..].starts_with("null") {
*i += 4;
Some(Value::Null)
} else {
None
}
}
_ => {
let start = *i;
while *i < b.len()
&& (b[*i].is_ascii_digit()
|| matches!(b[*i], b'-' | b'+' | b'.' | b'e' | b'E'))
{
*i += 1;
}
let tok = s[start..*i].to_string();
if let Ok(iv) = tok.parse::<i64>() {
Some(Value::Int(iv))
} else if let Ok(fv) = tok.parse::<f64>() {
Some(Value::Float(fv))
} else {
None
}
}
}
}
parse_val_impl(bytes, &mut i, s, &parse_str)
}
#[cfg(feature = "serde")]
mod serde_impl {
use super::Value;
use serde::de::{self, MapAccess, SeqAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
use std::fmt;
impl Serialize for Value {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Value::Null => serializer.serialize_unit(),
Value::Bool(b) => serializer.serialize_bool(*b),
Value::Int(i) => serializer.serialize_i64(*i),
Value::Float(f) => serializer.serialize_f64(*f),
Value::Str(s) => serializer.serialize_str(s),
Value::Array(a) => a.serialize(serializer),
Value::Object(m) => {
let mut map = serializer.serialize_map(Some(m.len()))?;
for (k, v) in m {
map.serialize_entry(k, v)?;
}
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for Value {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ValueVisitor)
}
}
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("any valid SML/JSON value")
}
fn visit_unit<E: de::Error>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_none<E: de::Error>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_some<D>(self, d: D) -> Result<Value, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(d)
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Value, E> {
Ok(Value::Bool(v))
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Value, E> {
Ok(Value::Int(v))
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Value, E> {
Ok(i64::try_from(v)
.map(Value::Int)
.unwrap_or_else(|_| Value::Float(v as f64)))
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Value, E> {
Ok(Value::Float(v))
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Value, E> {
Ok(Value::Str(v.to_string()))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Value, E> {
Ok(Value::Str(v))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut v = Vec::new();
while let Some(x) = seq.next_element()? {
v.push(x);
}
Ok(Value::Array(v))
}
fn visit_map<A>(self, mut map: A) -> Result<Value, A::Error>
where
A: MapAccess<'de>,
{
let mut m = BTreeMap::new();
while let Some((k, v)) = map.next_entry::<String, Value>()? {
m.insert(k, v);
}
Ok(Value::Object(m))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_defaults_to_current_when_absent() {
let (v, ver) = parse_versioned("a: 1\n").unwrap();
assert_eq!(ver, Version::CURRENT);
assert_eq!(v.get("a"), Some(&Value::Int(1)));
}
#[test]
fn version_declared_as_v1() {
let (v, ver) = parse_versioned("@version v1\na: 1\n").unwrap();
assert_eq!(ver, Version::V1);
assert_eq!(v.get("a"), Some(&Value::Int(1)));
}
#[test]
fn version_declaration_is_stripped_not_parsed_as_content() {
let v = parse("@version v1\na: 1\n").unwrap();
assert_eq!(v.get("a"), Some(&Value::Int(1)));
assert!(v.get("version").is_none(), "@version 不应进入数据");
}
#[test]
fn unsupported_version_is_rejected() {
let err = parse_versioned("@version v99\na: 1\n").unwrap_err();
assert!(err.contains("不支持"), "应拒绝不支持的版本,got: {err}");
assert!(err.contains("v99"), "错误应含版本号,got: {err}");
}
#[test]
fn conflicting_version_is_rejected() {
let err = parse_versioned("@version v1\n@version v2\n").unwrap_err();
assert!(!err.is_empty());
let (_, ver) = parse_versioned("@version v1\n@version v1\n").unwrap();
assert_eq!(ver, Version::V1, "重复但一致的声明应被接受");
}
#[test]
fn version_is_reserved_as_fragment_name() {
let err = parse("@version { x: 1 }\n").unwrap_err();
assert!(err.contains("保留") || err.contains("版本声明"), "got: {err}");
}
#[test]
fn version_works_with_include() {
let d = tmpdir("version");
std::fs::write(d.join("p.sml"), "@version v1\nb: 2\n").unwrap();
std::fs::write(d.join("main.sml"), "@version v1\ninclude \"p.sml\"\n").unwrap();
let (v, ver) = parse_file_versioned(d.join("main.sml")).unwrap();
assert_eq!(ver, Version::V1);
assert_eq!(v.get("b"), Some(&Value::Int(2)), "版本与 include 应协同");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn version_display_matches_name() {
assert_eq!(Version::V1.name(), "v1");
assert_eq!(format!("{}", Version::V1), "v1");
}
fn tmpdir(tag: &str) -> std::path::PathBuf {
let mut d = std::env::temp_dir();
d.push(format!("sml_test_{tag}_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).expect("create tmpdir");
d
}
#[test]
fn include_inlines_external_file() {
let d = tmpdir("inline");
std::fs::write(d.join("part.sml"), "port: 8080\n").unwrap();
std::fs::write(d.join("main.sml"), "host: local\ninclude \"part.sml\"\n").unwrap();
let v = parse_file(d.join("main.sml")).unwrap();
assert_eq!(v.get("host").unwrap().as_str(), Some("local"));
assert_eq!(v.get("port"), Some(&Value::Int(8080)));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn include_at_prefix_is_equivalent() {
let d = tmpdir("at");
std::fs::write(d.join("p.sml"), "b: 2\n").unwrap();
std::fs::write(d.join("m.sml"), "@include \"p.sml\"\n").unwrap();
let v = parse_file(d.join("m.sml")).unwrap();
assert_eq!(v.get("b"), Some(&Value::Int(2)));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn include_resolves_relative_to_including_file() {
let d = tmpdir("nested");
std::fs::create_dir_all(d.join("sub")).unwrap();
std::fs::write(d.join("sub/leaf.sml"), "leaf: yes\n").unwrap();
std::fs::write(d.join("sub/mid2.sml"), "include \"leaf.sml\"\n").unwrap();
std::fs::write(d.join("main.sml"), "include \"sub/mid2.sml\"\n").unwrap();
let v = parse_file(d.join("main.sml")).unwrap();
assert_eq!(
v.get("leaf").unwrap().as_str(),
Some("yes"),
"嵌套 include 的路径应相对各自所在目录解析"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn include_inside_block_injects_fields() {
let d = tmpdir("block");
std::fs::write(d.join("fields.sml"), "region: cn-north-1\nzone: a\n").unwrap();
std::fs::write(d.join("main.sml"), "server web {\ninclude \"fields.sml\"\nport: 8080\n}\n").unwrap();
let v = parse_file(d.join("main.sml")).unwrap();
let server = v.get("server").expect("应有 server 块");
assert_eq!(server.get("region").unwrap().as_str(), Some("cn-north-1"));
assert_eq!(server.get("zone").unwrap().as_str(), Some("a"));
assert_eq!(server.get("port"), Some(&Value::Int(8080)));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn include_detects_cycles() {
let d = tmpdir("cycle");
std::fs::write(d.join("a.sml"), "include \"b.sml\"\n").unwrap();
std::fs::write(d.join("b.sml"), "include \"a.sml\"\n").unwrap();
let err = parse_file(d.join("a.sml")).unwrap_err();
assert!(err.contains("循环引用"), "应报循环引用,got: {err}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn include_missing_file_is_error() {
let d = tmpdir("missing");
std::fs::write(d.join("m.sml"), "include \"nope.sml\"\n").unwrap();
let err = parse_file(d.join("m.sml")).unwrap_err();
assert!(err.contains("nope.sml"), "错误应含缺失文件名,got: {err}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn hash_in_quoted_string_is_not_a_comment() {
assert_eq!(strip_line_comment("k: \"a#b\""), "k: \"a#b\"");
assert_eq!(strip_line_comment("k: v # comment"), "k: v ");
}
#[test]
fn include_line_is_not_confused_with_key_named_include() {
assert_eq!(include_target("key: include"), None);
assert_eq!(include_target("include \"a.sml\""), Some("a.sml".into()));
assert_eq!(include_target("@include \"a.sml\""), Some("a.sml".into()));
assert_eq!(include_target("# include \"a.sml\""), None, "注释行不生效");
}
#[test]
fn email_in_bare_word_survives() {
let v = parse("to: a@b.c\nfrom: \"sal <sal@mail.swebase.cn>\"\n").unwrap();
assert_eq!(v.get("to").unwrap().as_str(), Some("a@b.c"), "got: {v:?}");
assert_eq!(
v.get("from").unwrap().as_str(),
Some("sal <sal@mail.swebase.cn>"),
"got: {v:?}"
);
}
#[test]
fn email_roundtrips_through_to_sml() {
let v = Value::Object(BTreeMap::from([(
"to".to_string(),
Value::Str("SALflake@qq.com".into()),
)]));
let back = parse(&to_sml(&v)).unwrap();
assert_eq!(back, v, "邮箱必须能往返,got:\n{}", to_sml(&v));
}
#[test]
fn fragment_definition_still_works() {
let v = parse("@base { region: cn }\nregion: &base\n").unwrap();
assert_eq!(
v.get("region").unwrap().get("region").unwrap().as_str(),
Some("cn"),
"片段引用应展开为定义的内容,got: {v:?}"
);
}
#[test]
fn toplevel_array_roundtrips() {
let v = Value::Array(vec![
Value::Object(BTreeMap::from([
("ts".to_string(), Value::Str("2026-01-01".into())),
("to".to_string(), Value::Str("a@b.c".into())),
])),
Value::Object(BTreeMap::from([
("ts".to_string(), Value::Str("2026-01-02".into())),
("to".to_string(), Value::Str("x@y.z".into())),
])),
]);
let text = to_sml(&v);
let back = parse(&text).unwrap();
assert_eq!(back, v, "顶层对象数组必须能往返,got text:\n{text}");
}
#[test]
fn toplevel_array_of_scalars_roundtrips() {
let v = Value::Array(vec![
Value::Int(1),
Value::Str("two".into()),
Value::Bool(true),
]);
let back = parse(&to_sml(&v)).unwrap();
assert_eq!(back, v, "顶层标量数组必须能往返");
}
#[test]
fn toplevel_object_block_roundtrips() {
let mut m = BTreeMap::new();
m.insert("k".to_string(), Value::Int(1));
let v = Value::Object(m);
let back = parse(&to_sml(&v)).unwrap();
assert_eq!(back, v, "顶层对象块必须能往返");
}
#[test]
fn toplevel_empty_array_roundtrips() {
let v = Value::Array(vec![]);
let back = parse(&to_sml(&v)).unwrap();
assert_eq!(back, v, "空数组必须能往返");
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip_preserves_shape() {
let v = parse("name: John\nage: 27\ntags: [a b]\nnested { k: v }\n").unwrap();
let json = serde_json::to_string(&v).unwrap();
assert!(json.contains("\"name\":\"John\""), "got: {json}");
assert!(json.contains("\"age\":27"), "got: {json}");
assert!(json.contains("\"tags\":[\"a\",\"b\"]"), "got: {json}");
assert!(json.contains("\"nested\":{\"k\":\"v\"}"), "got: {json}");
let back: Value = serde_json::from_str(&json).unwrap();
assert_eq!(back, v, "serde 往返应还原原值");
}
#[cfg(feature = "serde")]
#[test]
fn serde_deserializes_json_into_value() {
let v: Value = serde_json::from_str(r#"{"s":"x","i":5,"f":1.5,"b":true,"n":null,"a":[1,2]}"#).unwrap();
assert_eq!(v.get("s").unwrap().as_str(), Some("x"));
assert_eq!(v.get("i"), Some(&Value::Int(5)));
assert_eq!(v.get("f"), Some(&Value::Float(1.5)));
assert_eq!(v.get("b"), Some(&Value::Bool(true)));
assert_eq!(v.get("n"), Some(&Value::Null));
assert!(matches!(v.get("a"), Some(Value::Array(a)) if a.len() == 2));
}
#[test]
fn nested_array_inside_object_inside_array_survives_roundtrip() {
let mut item = BTreeMap::new();
item.insert("path".to_string(), Value::Str("a.txt".into()));
item.insert(
"chunks".to_string(),
Value::Array(vec![
Value::Str("c1".into()),
Value::Str("c2".into()),
]),
);
let mut root = BTreeMap::new();
root.insert(
"entries".to_string(),
Value::Array(vec![Value::Object(item)]),
);
let text = to_sml(&Value::Object(root));
assert!(!text.contains("[..]"), "嵌套数组不得被缩略: {text}");
let back = parse(&text).unwrap();
let chunks = back.get("entries").and_then(|e| match e {
Value::Array(a) => a.first(),
_ => None,
});
let chunks = match chunks {
Some(Value::Object(m)) => m.get("chunks"),
_ => None,
};
match chunks {
Some(Value::Array(a)) => {
assert_eq!(a.len(), 2, "两个块都应保留: {text}");
assert_eq!(
a.iter().filter_map(|c| c.as_str()).collect::<Vec<_>>(),
vec!["c1", "c2"]
);
}
other => panic!("chunks 应解析为数组,实际 {other:?}"),
}
}
#[test]
fn utf8_in_quoted_string_survives_roundtrip() {
let v = parse(r#"note: "修复若干问题""#).unwrap();
assert_eq!(
v.get("note").and_then(|x| x.as_str()),
Some("修复若干问题"),
"引号串中的中文不应被破坏"
);
let v2 = parse("region: 华北").unwrap();
assert_eq!(v2.get("region").and_then(|x| x.as_str()), Some("华北"));
let v3 = parse(r#"k: "\u{4fee}\u{590d}""#).unwrap();
assert_eq!(v3.get("k").and_then(|x| x.as_str()), Some("修复"));
}
#[test]
fn parse_basic() {
let text = "firstName: John\nage: 27\nisAlive: true\nspouse: null\n";
let v = parse(text).unwrap();
assert_eq!(v.get("firstName"), Some(&Value::Str("John".into())));
assert_eq!(v.get("age"), Some(&Value::Int(27)));
assert_eq!(v.get("isAlive"), Some(&Value::Bool(true)));
assert_eq!(v.get("spouse"), Some(&Value::Null));
}
#[test]
fn parse_nested() {
let text = "address:\n{\n streetAddress: \"21 2nd Street\"\n state: NY\n}\n";
let v = parse(text).unwrap();
assert_eq!(
v.get("address.streetAddress"),
Some(&Value::Str("21 2nd Street".into()))
);
assert_eq!(v.get("address.state"), Some(&Value::Str("NY".into())));
}
#[test]
fn parse_array() {
let text = "phoneNumbers:\n[\n { type: home }\n { type: office }\n]\n";
let v = parse(text).unwrap();
if let Some(Value::Array(a)) = v.get("phoneNumbers") {
assert_eq!(a.len(), 2);
assert_eq!(a[0].get("type"), Some(&Value::Str("home".into())));
} else {
panic!("not array");
}
}
#[test]
fn parse_fragment() {
let text = "@base { region: cn-north-1 }\nserver web { &base }\n";
let v = parse(text).unwrap();
assert_eq!(
v.get("server.&base.region"),
Some(&Value::Str("cn-north-1".into()))
);
assert_eq!(v.get("server.__type"), Some(&Value::Str("server".into())));
assert_eq!(v.get("server.__name"), Some(&Value::Str("web".into())));
}
#[test]
fn roundtrip() {
let text = "name: myapp\nport: 8080\nflags: [ a b c ]\n";
let v = parse(text).unwrap();
let out = to_sml(&v);
let v2 = parse(&out).unwrap();
assert_eq!(v, v2);
}
#[test]
fn env_inline() {
std::env::set_var("SML_TEST_VAR", "hello");
let text = "greeting: $env.SML_TEST_VAR\n";
let v = parse(text).unwrap();
assert_eq!(v.get("greeting"), Some(&Value::Str("hello".into())));
}
#[test]
fn c_abi_json_bridge() {
let text = "name: John\nage: 27\n";
let v = parse(text).unwrap();
let j = jsonify(&v);
assert!(j.contains("\"name\":\"John\""));
let back = json_to_value(&j).unwrap();
assert_eq!(back, v);
}
}