use serde::Serialize;
#[derive(Debug, Default, Clone, Serialize, schemars::JsonSchema)]
pub struct ResponseMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub applied_scope: Option<AppliedScope>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub next_steps: Vec<NextStep>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub warnings: Vec<Warning>,
}
impl ResponseMeta {
pub fn is_empty(&self) -> bool {
self.applied_scope.is_none() && self.next_steps.is_empty() && self.warnings.is_empty()
}
}
#[derive(Debug, Default, Clone, Serialize, schemars::JsonSchema)]
pub struct AppliedScope {
#[serde(skip_serializing_if = "Option::is_none")]
pub device: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub native_pid: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nvtx_pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_window_ns: Option<(i64, i64)>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub aggregated_over: Vec<String>,
}
#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
pub struct NextStep {
pub hint: String,
pub command: String,
}
#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
pub struct Warning {
pub severity: WarningSeverity,
pub code: WarningCode,
pub message: String,
}
#[derive(Debug, Clone, Copy, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum WarningSeverity {
Info,
Warn,
}
#[derive(Debug, Clone, Copy, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum WarningCode {
NarrowWindow,
EmptyWithScope,
MultiDeviceAmbiguous,
MultiRankAmbiguous,
CoverageLow,
SchemaFallback,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn at<'a>(v: &'a Value, ptr: &str) -> anyhow::Result<&'a Value> {
v.pointer(ptr)
.ok_or_else(|| anyhow::anyhow!("missing pointer `{ptr}` in {v}"))
}
#[test]
fn empty_meta_serialises_all_optional_fields_absent() -> anyhow::Result<()> {
let m = ResponseMeta::default();
assert!(m.is_empty());
let json = serde_json::to_value(&m)?;
assert!(json.get("applied_scope").is_none());
assert!(json.get("next_steps").is_none());
assert!(json.get("warnings").is_none());
Ok(())
}
#[test]
fn applied_scope_round_trip_with_aggregated_over() -> anyhow::Result<()> {
let m = ResponseMeta {
applied_scope: Some(AppliedScope {
device: None,
stream: None,
native_pid: Some(1234),
kind: Some("kernel".into()),
nvtx_pattern: Some("step_*".into()),
time_window_ns: Some((0, 1_000_000_000)),
aggregated_over: vec!["device".into()],
}),
..ResponseMeta::default()
};
let json = serde_json::to_value(&m)?;
assert_eq!(at(&json, "/applied_scope/native_pid")?.as_i64(), Some(1234));
assert_eq!(
at(&json, "/applied_scope/aggregated_over/0")?.as_str(),
Some("device")
);
assert!(json.pointer("/applied_scope/device").is_none());
Ok(())
}
#[test]
fn warning_severity_serialises_lowercase() -> anyhow::Result<()> {
let w = Warning {
severity: WarningSeverity::Warn,
code: WarningCode::EmptyWithScope,
message: "filter excluded all rows".into(),
};
let json = serde_json::to_value(&w)?;
assert_eq!(at(&json, "/severity")?.as_str(), Some("warn"));
assert_eq!(at(&json, "/code")?.as_str(), Some("empty-with-scope"));
Ok(())
}
#[test]
fn warning_code_kebab_case_for_every_variant() -> anyhow::Result<()> {
let codes = [
(WarningCode::NarrowWindow, "narrow-window"),
(WarningCode::EmptyWithScope, "empty-with-scope"),
(WarningCode::MultiDeviceAmbiguous, "multi-device-ambiguous"),
(WarningCode::MultiRankAmbiguous, "multi-rank-ambiguous"),
(WarningCode::CoverageLow, "coverage-low"),
(WarningCode::SchemaFallback, "schema-fallback"),
];
for (c, expected) in codes {
let v = serde_json::to_value(c)?;
assert_eq!(
v.as_str(),
Some(expected),
"{c:?} should serialise as {expected}"
);
}
Ok(())
}
#[test]
fn next_step_round_trip() -> anyhow::Result<()> {
let n = NextStep {
hint: "drill into top row".into(),
command: "veloq inspect path/to/trace nvtx:42".into(),
};
let json = serde_json::to_value(&n)?;
assert_eq!(at(&json, "/hint")?.as_str(), Some("drill into top row"));
assert_eq!(
at(&json, "/command")?.as_str(),
Some("veloq inspect path/to/trace nvtx:42")
);
Ok(())
}
}