use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct PublishDef {
pub wit: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rev: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub fanout: bool,
}
impl<'de> Deserialize<'de> for PublishDef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct LongForm {
wit: String,
#[serde(default)]
version: Option<String>,
#[serde(default)]
tag: Option<String>,
#[serde(default)]
rev: Option<String>,
#[serde(default)]
branch: Option<String>,
#[serde(default)]
path: Option<String>,
#[serde(default)]
fanout: bool,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Short(String),
Long(LongForm),
}
let raw = Raw::deserialize(deserializer)?;
Ok(match raw {
Raw::Short(wit) => PublishDef {
wit,
version: None,
tag: None,
rev: None,
branch: None,
path: None,
fanout: false,
},
Raw::Long(l) => {
let pins = [&l.version, &l.tag, &l.rev, &l.branch, &l.path]
.iter()
.filter(|o| o.is_some())
.count();
if pins > 1 {
return Err(serde::de::Error::custom(
"[publish] entry: at most one of version / tag / rev / branch / path may be set",
));
}
PublishDef {
wit: l.wit,
version: l.version,
tag: l.tag,
rev: l.rev,
branch: l.branch,
path: l.path,
fanout: l.fanout,
}
},
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SubscribeDef {
pub wit: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rev: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handler: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<u32>,
}
impl<'de> Deserialize<'de> for SubscribeDef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct LongForm {
wit: String,
#[serde(default)]
version: Option<String>,
#[serde(default)]
tag: Option<String>,
#[serde(default)]
rev: Option<String>,
#[serde(default)]
branch: Option<String>,
#[serde(default)]
path: Option<String>,
#[serde(default)]
handler: Option<String>,
#[serde(default)]
priority: Option<u32>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Short(String),
Long(LongForm),
}
let raw = Raw::deserialize(deserializer)?;
Ok(match raw {
Raw::Short(wit) => SubscribeDef {
wit,
version: None,
tag: None,
rev: None,
branch: None,
path: None,
handler: None,
priority: None,
},
Raw::Long(l) => {
let pins = [&l.version, &l.tag, &l.rev, &l.branch, &l.path]
.iter()
.filter(|o| o.is_some())
.count();
if pins > 1 {
return Err(serde::de::Error::custom(
"[subscribe] entry: at most one of version / tag / rev / branch / path may be set",
));
}
if l.priority.is_some() && l.handler.is_none() {
return Err(serde::de::Error::custom(
"[subscribe] entry: `priority` requires a `handler` — a handler-less \
subscribe is ACL-only and has no dispatch order",
));
}
SubscribeDef {
wit: l.wit,
version: l.version,
tag: l.tag,
rev: l.rev,
branch: l.branch,
path: l.path,
handler: l.handler,
priority: l.priority,
}
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_publish(entry: &str) -> Result<PublishDef, toml::de::Error> {
let toml = format!("\"x.v1.y\" = {entry}\n");
let map: std::collections::HashMap<String, PublishDef> = toml::from_str(&toml)?;
Ok(map.into_iter().next().unwrap().1)
}
fn parse_subscribe(entry: &str) -> Result<SubscribeDef, toml::de::Error> {
let toml = format!("\"x.v1.y\" = {entry}\n");
let map: std::collections::HashMap<String, SubscribeDef> = toml::from_str(&toml)?;
Ok(map.into_iter().next().unwrap().1)
}
#[test]
fn publish_short_form_parses() {
let p = parse_publish("\"@scope/wit/iface/rec\"").unwrap();
assert_eq!(p.wit, "@scope/wit/iface/rec");
assert!(p.version.is_none() && p.tag.is_none() && p.rev.is_none());
}
#[test]
fn publish_long_form_zero_pins_parses() {
let p = parse_publish("{ wit = \"r\" }").unwrap();
assert_eq!(p.wit, "r");
}
#[test]
fn publish_long_form_one_pin_parses() {
let p = parse_publish("{ wit = \"r\", version = \"1.0\" }").unwrap();
assert_eq!(p.version.as_deref(), Some("1.0"));
}
#[test]
fn publish_long_form_two_pins_rejected() {
let err = parse_publish("{ wit = \"r\", version = \"1.0\", tag = \"v1\" }").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("at most one of version / tag / rev / branch / path"),
"missing invariant message: {msg}"
);
assert!(
msg.contains("x.v1.y"),
"TOML deserializer should include the topic key in the error context: {msg}"
);
}
#[test]
fn subscribe_short_form_parses() {
let s = parse_subscribe("\"r\"").unwrap();
assert_eq!(s.wit, "r");
assert!(s.handler.is_none());
}
#[test]
fn subscribe_long_form_one_pin_with_handler_parses() {
let s = parse_subscribe("{ wit = \"r\", rev = \"abc123\", handler = \"on_x\" }").unwrap();
assert_eq!(s.rev.as_deref(), Some("abc123"));
assert_eq!(s.handler.as_deref(), Some("on_x"));
}
#[test]
fn subscribe_long_form_two_pins_rejected() {
let err =
parse_subscribe("{ wit = \"r\", branch = \"main\", path = \"./local\" }").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("at most one of version / tag / rev / branch / path"),
"missing invariant message: {msg}"
);
}
#[test]
fn subscribe_priority_with_handler_parses() {
let s = parse_subscribe("{ wit = \"r\", handler = \"on_x\", priority = 10 }").unwrap();
assert_eq!(s.handler.as_deref(), Some("on_x"));
assert_eq!(s.priority, Some(10));
}
#[test]
fn subscribe_handler_without_priority_leaves_priority_unset() {
let s = parse_subscribe("{ wit = \"r\", handler = \"on_x\" }").unwrap();
assert_eq!(s.handler.as_deref(), Some("on_x"));
assert!(s.priority.is_none());
}
#[test]
fn subscribe_priority_without_handler_rejected() {
let err = parse_subscribe("{ wit = \"r\", priority = 10 }").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("`priority` requires a `handler`"),
"missing priority-needs-handler message: {msg}"
);
}
}