use sml::{SmlDeserialize, SmlSerialize};
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct Server {
host: String,
#[sml(default)]
port: i32,
#[sml(rename = "tls-enabled")]
tls_enabled: bool,
#[sml(skip)]
secret: String,
upstream: Option<String>,
}
#[test]
fn struct_roundtrip() {
let s = Server {
host: "a.example".into(),
port: 8080,
tls_enabled: true,
secret: "hunter2".into(),
upstream: Some("b.example".into()),
};
let v = s.to_sml_value();
match &v {
sml::Value::Object(m) => {
assert_eq!(m.get("host"), Some(&sml::Value::Str("a.example".into())));
assert_eq!(m.get("port"), Some(&sml::Value::Int(8080)));
assert_eq!(m.get("tls-enabled"), Some(&sml::Value::Bool(true)));
assert!(!m.contains_key("secret"), "skip 字段不应出现在序列化结果");
assert_eq!(
m.get("upstream"),
Some(&sml::Value::Str("b.example".into()))
);
}
other => panic!("期望块,实际为 {other:?}"),
}
let back = Server::from_sml_value(&v).unwrap();
assert_eq!(back.host, s.host);
assert_eq!(back.port, s.port);
assert_eq!(back.tls_enabled, s.tls_enabled);
assert_eq!(back.upstream, s.upstream);
assert_eq!(back.secret, "", "skip 字段反序列化后应为 Default");
}
#[test]
fn option_none_is_omitted() {
let s = Server {
host: "a.example".into(),
port: 8080,
tls_enabled: false,
secret: "x".into(),
upstream: None,
};
let v = s.to_sml_value();
match &v {
sml::Value::Object(m) => assert!(!m.contains_key("upstream")),
other => panic!("期望块,实际为 {other:?}"),
}
let back = Server::from_sml_value(&v).unwrap();
assert_eq!(back.host, s.host);
assert_eq!(back.upstream, None);
assert_eq!(back.secret, "");
}
#[test]
fn missing_required_field_is_error() {
let v = sml::Value::Object(std::collections::BTreeMap::new());
let err = Server::from_sml_value(&v).unwrap_err();
assert!(err.contains("host"), "错误应指出缺失字段: {err}");
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
enum Status {
Active,
#[sml(rename = "stand-by")]
StandBy,
}
#[test]
fn enum_word_roundtrip() {
assert_eq!(Status::Active.to_sml_value(), sml::Value::Str("Active".into()));
assert_eq!(
Status::StandBy.to_sml_value(),
sml::Value::Str("stand-by".into())
);
assert_eq!(
Status::from_sml_value(&sml::Value::Str("Active".into())).unwrap(),
Status::Active
);
assert_eq!(
Status::from_sml_value(&sml::Value::Str("stand-by".into())).unwrap(),
Status::StandBy
);
assert!(Status::from_sml_value(&sml::Value::Str("nope".into())).is_err());
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
enum Shape {
Circle(f64),
Point(f64, f64),
Rect { w: f64, h: f64 },
Unit,
}
#[test]
fn enum_data_roundtrip() {
let c = Shape::Circle(3.0);
match &c.to_sml_value() {
sml::Value::Object(m) => {
assert_eq!(m.get("__type"), Some(&sml::Value::Str("Circle".into())));
assert_eq!(m.get("_value"), Some(&sml::Value::Float(3.0)));
}
other => panic!("期望块,实际为 {other:?}"),
}
assert_eq!(Shape::from_sml_value(&c.to_sml_value()).unwrap(), c);
let p = Shape::Point(1.0, 2.0);
match &p.to_sml_value() {
sml::Value::Object(m) => {
assert_eq!(m.get("__type"), Some(&sml::Value::Str("Point".into())));
assert_eq!(
m.get("_value"),
Some(&sml::Value::Array(vec![
sml::Value::Float(1.0),
sml::Value::Float(2.0)
]))
);
}
other => panic!("期望块,实际为 {other:?}"),
}
assert_eq!(Shape::from_sml_value(&p.to_sml_value()).unwrap(), p);
let r = Shape::Rect { w: 4.0, h: 5.0 };
match &r.to_sml_value() {
sml::Value::Object(m) => {
assert_eq!(m.get("__type"), Some(&sml::Value::Str("Rect".into())));
assert_eq!(m.get("w"), Some(&sml::Value::Float(4.0)));
assert_eq!(m.get("h"), Some(&sml::Value::Float(5.0)));
}
other => panic!("期望块,实际为 {other:?}"),
}
assert_eq!(Shape::from_sml_value(&r.to_sml_value()).unwrap(), r);
assert_eq!(Shape::Unit.to_sml_value(), sml::Value::Str("Unit".into()));
assert_eq!(
Shape::from_sml_value(&sml::Value::Str("Unit".into())).unwrap(),
Shape::Unit
);
let as_block = sml::Value::Object(
[("__type".to_string(), sml::Value::Str("Unit".into()))]
.into_iter()
.collect(),
);
assert_eq!(Shape::from_sml_value(&as_block).unwrap(), Shape::Unit);
}
#[test]
fn enum_data_text_roundtrip() {
let c = Shape::Circle(3.0);
let text = c.to_sml();
assert!(text.contains("__type: Circle"), "应保留 __type: {text}");
assert_eq!(Shape::from_sml(&text).unwrap(), c);
let r = Shape::Rect { w: 4.0, h: 5.0 };
let text = r.to_sml();
assert!(text.contains("__type: Rect"), "应保留 __type: {text}");
assert!(text.contains("w: 4"), "字段应输出: {text}");
assert_eq!(Shape::from_sml(&text).unwrap(), r);
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct Id(u64);
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct Pair(i32, String);
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct Marker;
#[test]
fn struct_shapes_roundtrip() {
let id = Id(42);
assert_eq!(id.to_sml_value(), sml::Value::Int(42));
assert_eq!(Id::from_sml_value(&id.to_sml_value()).unwrap(), id);
let pair = Pair(7, "sml".into());
assert_eq!(
pair.to_sml_value(),
sml::Value::Array(vec![sml::Value::Int(7), sml::Value::Str("sml".into())])
);
assert_eq!(Pair::from_sml_value(&pair.to_sml_value()).unwrap(), pair);
let m = Marker;
assert_eq!(m.to_sml_value(), sml::Value::Str("Marker".into()));
assert_eq!(Marker::from_sml_value(&m.to_sml_value()).unwrap(), m);
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
#[sml(rename_all = "kebab-case")]
struct Kebab {
first_name: String,
is_ready: bool,
}
#[test]
fn rename_all_kebab() {
let k = Kebab {
first_name: "张三".into(),
is_ready: true,
};
let text = k.to_sml();
assert!(text.contains("first-name:"), "应输出 kebab-case 键: {text}");
assert!(text.contains("is-ready:"), "应输出 kebab-case 键: {text}");
assert_eq!(Kebab::from_sml(&text).unwrap(), k);
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct Wrap<T> {
inner: T,
extra: Option<T>,
}
#[test]
fn generic_roundtrip() {
let w = Wrap {
inner: 42i32,
extra: Some(7),
};
let v = w.to_sml_value();
assert_eq!(Wrap::<i32>::from_sml_value(&v).unwrap(), w);
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct Base {
region: String,
zone: String,
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct App {
name: String,
#[sml(flatten)]
base: Base,
}
#[test]
fn flatten_roundtrip() {
let a = App {
name: "web".into(),
base: Base {
region: "cn-north-1".into(),
zone: "a".into(),
},
};
let v = a.to_sml_value();
match &v {
sml::Value::Object(m) => {
assert_eq!(m.get("name"), Some(&sml::Value::Str("web".into())));
assert_eq!(m.get("region"), Some(&sml::Value::Str("cn-north-1".into())));
assert_eq!(m.get("zone"), Some(&sml::Value::Str("a".into())));
}
other => panic!("期望块,实际为 {other:?}"),
}
assert_eq!(App::from_sml_value(&v).unwrap(), a);
}
#[derive(SmlSerialize, SmlDeserialize, Debug, PartialEq)]
struct Service {
name: String,
replicas: u32,
labels: std::collections::BTreeMap<String, String>,
nodes: Vec<String>,
status: Status,
}
#[test]
fn text_roundtrip_and_nesting() {
let svc = Service {
name: "auth".into(),
replicas: 3,
labels: [("tier".to_string(), "backend".to_string())]
.into_iter()
.collect(),
nodes: vec!["n1".into(), "n2".into()],
status: Status::Active,
};
let text = svc.to_sml();
assert!(text.contains("replicas: 3"), "数字应保持为 SML 整数: {text}");
assert!(text.contains("status: Active"), "枚举应输出裸词: {text}");
let back = Service::from_sml(&text).unwrap();
assert_eq!(back, svc);
}
#[test]
fn type_error_message_is_informative() {
let bad = sml::Value::Object(
[(
"host".to_string(),
sml::Value::Array(vec![sml::Value::Int(1)]),
)]
.into_iter()
.collect(),
);
let err = Server::from_sml_value(&bad).unwrap_err();
assert!(err.contains("host"), "错误应定位到字段: {err}");
assert!(err.contains("字符串"), "错误应说明期望类型: {err}");
}
fn v_int(i: i64) -> sml::Value {
sml::Value::Int(i)
}
fn v_float(f: f64) -> sml::Value {
sml::Value::Float(f)
}
#[test]
fn u64_float_overflow_rejected() {
let r: Result<u64, _> = u64::from_sml_value(&v_float(1e30));
assert!(r.is_err(), "1e30 超出 u64 范围,应报错而非饱和");
}
#[test]
fn u128_int_negative_rejected() {
let r: Result<u128, _> = u128::from_sml_value(&v_int(-1));
assert!(r.is_err(), "负数不能填入 u128,应报错");
}
#[test]
fn i128_float_overflow_rejected() {
let r: Result<i128, _> = i128::from_sml_value(&v_float(1e40));
assert!(r.is_err(), "1e40 超出 i128 范围,应报错");
}
#[test]
fn i64_float_boundary_no_saturation() {
let r: Result<i64, _> = i64::from_sml_value(&v_float(9223372036854775808.0));
assert!(r.is_err(), "i64::MAX+1 浮点应被拒绝(边界舍入)");
let ok: i64 = i64::from_sml_value(&v_int(i64::MAX)).unwrap();
assert_eq!(ok, i64::MAX);
}
#[test]
fn usize_serialize_no_wrap() {
let v = u64::MAX.to_sml_value();
match v {
sml::Value::Int(i) => assert!(i >= 0, "u64::MAX 不应回绕为负数,实际 {i}"),
sml::Value::Float(_) => {} other => panic!("意外类型 {other:?}"),
}
}
#[test]
fn i8_out_of_range_rejected() {
let r: Result<i8, _> = i8::from_sml_value(&v_float(200.0));
assert!(r.is_err(), "200 超出 i8 范围");
let r2: Result<i8, _> = i8::from_sml_value(&v_int(200));
assert!(r2.is_err(), "200 超出 i8 范围");
}
#[test]
fn float_fraction_rejected_for_int() {
let r: Result<i32, _> = i32::from_sml_value(&v_float(1.5));
assert!(r.is_err(), "1.5 不是合法整数");
}
#[test]
fn toml_rs_style_top_level_functions() {
let s = Server {
host: "web.example".into(),
port: 8080,
tls_enabled: true,
secret: "hunter2".into(),
upstream: Some("b.example".into()),
};
let text = sml::to_string(&s);
assert!(text.contains("host: web.example"), "{text}");
assert!(text.contains("tls-enabled: true"), "{text}");
let back: Server = sml::from_str(&text).unwrap();
assert_eq!(back, Server {
host: "web.example".into(),
port: 8080,
tls_enabled: true,
secret: String::new(), upstream: Some("b.example".into()),
});
assert_eq!(sml::to_string(&s), s.to_sml());
assert_eq!(sml::to_string("hello"), "hello");
assert_eq!(sml::to_string("hello"), "hello".to_sml());
}