use crate::agent::CliError;
use saccade_core::schema_catalog;
use std::path::{Path, PathBuf};
#[derive(clap::Args)]
pub(crate) struct Args {
#[command(subcommand)]
operation: Operation,
}
#[derive(clap::Subcommand)]
enum Operation {
List {
#[arg(long)]
json: bool,
},
Get {
id: String,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
json: bool,
},
Path {
id: String,
#[arg(long)]
json: bool,
},
}
pub(crate) fn installed(id: &str) -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let prefix = exe.parent()?.parent()?;
let mut roots = vec![prefix.join("share/saccade/schemas")];
if let Some(root) = std::env::var_os("SACCADE_SCHEMA_DIR") {
roots.insert(0, root.into());
}
let embedded = schema_text(id).ok()?;
roots
.into_iter()
.map(|r| r.join(format!("{id}.schema.json")))
.find(|p| {
saccade_core::evidence_quality::read(p, 4 << 20).is_ok_and(|b| b == embedded.as_bytes())
})
}
fn schema_text(id: &str) -> Result<&'static str, CliError> {
#[cfg(feature = "print")]
if id == saccade_print::SCHEMA {
return Ok(saccade_print::JSON_SCHEMA);
}
Ok(schema_catalog::get(id)?)
}
pub(crate) fn run(args: Args) -> Result<u8, CliError> {
match args.operation {
Operation::List { json } => {
#[allow(unused_mut)]
let mut ids: Vec<_> = schema_catalog::DOCUMENTS
.iter()
.map(|(id, _)| *id)
.collect();
#[cfg(feature = "print")]
ids.push(saccade_print::SCHEMA);
if json {
crate::emit(&format!(
"{}\n",
serde_json::json!({"schema":"saccade-schema-list.v1","ids":ids})
))?;
} else {
crate::emit(&format!("{}\n", ids.join("\n")))?;
}
}
Operation::Get {
id,
out,
json: _json,
} => {
let text = schema_text(&id)?;
if let Some(out) = out {
write_new(&out, text.as_bytes())?;
} else {
crate::emit(text)?;
}
}
Operation::Path { id, json } => {
schema_text(&id)?;
let path = installed(&id);
if json {
crate::emit(&format!(
"{}\n",
serde_json::json!({"schema":"saccade-schema-path.v1","id":id,"path":path})
))?;
} else if let Some(p) = &path {
crate::emit(&format!("{}\n", p.display()))?;
}
return Ok(u8::from(path.is_none()));
}
}
Ok(0)
}
pub(crate) fn write_new(path: &Path, bytes: &[u8]) -> Result<(), CliError> {
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|e| CliError::io(e.to_string()))?;
file.write_all(bytes)
.map_err(|e| CliError::io(e.to_string()))
}
#[derive(clap::Args)]
pub(crate) struct PerfArgs {
#[command(subcommand)]
operation: PerfOperation,
}
#[derive(clap::Subcommand)]
enum PerfOperation {
Validate {
sidecar: PathBuf,
#[arg(long)]
json: bool,
},
}
pub(crate) fn validate(value: &serde_json::Value) -> Result<Vec<String>, CliError> {
let id = value["schema"]
.as_str()
.ok_or_else(|| CliError::usage("perf sidecar requires schema"))?;
if !matches!(id, "saccade-perf.v1" | "saccade-perf.v2") {
return Err(CliError::usage(
"expected saccade-perf.v1 or saccade-perf.v2",
));
}
let schema: serde_json::Value = serde_json::from_str(schema_catalog::get(id)?)?;
let validator = jsonschema::validator_for(&schema)
.map_err(|e| CliError::new("invalid_schema", e.to_string()))?;
Ok(validator
.iter_errors(value)
.take(32)
.map(|e| format!("{}: {}", e.instance_path, e))
.collect())
}
pub(crate) fn perf(args: PerfArgs) -> Result<u8, CliError> {
let PerfOperation::Validate { sidecar, json } = args.operation;
let value = serde_json::from_slice(&saccade_core::evidence_quality::read(&sidecar, 4 << 20)?)?;
let errors = validate(&value)?;
let report = serde_json::json!({"schema":"saccade-perf-validation.v1","valid":errors.is_empty(),"sidecar_schema":value["schema"],"errors":errors});
if json {
crate::emit(&format!("{report}\n"))?;
} else {
crate::emit(&format!(
"perf validation: {}\n{}",
if errors.is_empty() {
"valid"
} else {
"invalid"
},
errors.join("\n")
))?;
}
Ok(u8::from(!errors.is_empty()))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
#[test]
fn perf_validation_matches_file_schema_for_valid_and_invalid_sidecars() {
for id in ["saccade-perf.v1", "saccade-perf.v2"] {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join(format!("../saccade-core/schemas/{id}.schema.json"));
let schema: serde_json::Value =
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
let file_validator = jsonschema::validator_for(&schema).unwrap();
let mut valid = serde_json::json!({"schema":id,"unit":"ms","frame":{"value":1.0,"samples":8,"stat":"mean"},"terms":[],"counters":{}});
if id.ends_with("v2") {
valid["kind"] = serde_json::json!("measurement");
}
assert!(
file_validator.is_valid(&valid),
"valid producer fixture: {id}"
);
assert!(super::validate(&valid).unwrap().is_empty());
let values = [
valid,
serde_json::json!({"schema":id}),
serde_json::json!({"schema":id,"frames":[{"gpu_ms":-1}]}),
serde_json::json!({"schema":id,"frames":[]}),
];
for value in values {
assert_eq!(
super::validate(&value).unwrap().is_empty(),
file_validator.is_valid(&value)
);
}
}
let schema = saccade_core::schema_catalog::get("saccade-perf.v2").unwrap();
assert!(schema.contains("performance_noise"));
assert!(
!super::validate(&serde_json::json!({"schema":"saccade-perf.v2","kind":"wrong"}))
.unwrap()
.is_empty()
);
}
}