use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use serde::Deserialize;
use crate::error::NlResult;
#[derive(Debug, Clone, Deserialize)]
pub struct Manifest {
pub model_version: String,
pub release_tag: String,
pub archive: String,
pub sha256: String,
pub download_url: String,
pub files: BTreeMap<String, String>,
}
impl Manifest {
pub fn parse(json: &str) -> NlResult<Self> {
Ok(serde_json::from_str(json)?)
}
pub fn parse_path(path: &Path) -> NlResult<Self> {
let contents = fs::read_to_string(path)?;
Self::parse(&contents)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::NlError;
const SAMPLE: &str = r#"{
"model_version": "1.0.0",
"release_tag": "models-v1.0.0",
"archive": "sqry-models-v1.0.0.tar.gz",
"sha256": "4f7886e2a381ee4a17bfca81bd41ec88e10169d498b68d2166587e6265d27b3e",
"download_url": "https://example.invalid/sqry-models-v1.0.0.tar.gz",
"files": {
"intent_classifier.onnx": "f8aef10721d7f55f882576fd0f9f9fefaa1a9ededbf1df157e61b736f811833e",
"tokenizer.json": "79ea220416a57ca8acdf069d71875d5d2ddadef66021aeb6c120a7c546c04344"
}
}"#;
#[test]
fn parses_sample_manifest() {
let m = Manifest::parse(SAMPLE).expect("parse");
assert_eq!(m.model_version, "1.0.0");
assert_eq!(m.release_tag, "models-v1.0.0");
assert_eq!(m.archive, "sqry-models-v1.0.0.tar.gz");
assert_eq!(m.sha256.len(), 64);
assert_eq!(m.files.len(), 2);
assert!(m.files.contains_key("intent_classifier.onnx"));
}
#[test]
fn tolerates_unknown_top_level_fields() {
let json = r#"{
"model_version": "1.0.0",
"release_tag": "models-v1.0.0",
"archive": "x.tar.gz",
"sha256": "00",
"download_url": "https://example.invalid/x.tar.gz",
"files": {},
"future_field": "ignored"
}"#;
let m = Manifest::parse(json).expect("parse");
assert_eq!(m.archive, "x.tar.gz");
}
#[test]
fn malformed_json_is_manifest_parse_failed() {
let err = Manifest::parse("{not json}").unwrap_err();
assert!(
matches!(err, NlError::ManifestParseFailed(_)),
"expected ManifestParseFailed, got {err:?}"
);
}
#[test]
fn parses_real_committed_manifest() {
let baked = include_str!("../../models/manifest.json");
let m = Manifest::parse(baked).expect("baked manifest parses");
assert!(!m.sha256.is_empty());
assert!(!m.files.is_empty());
assert!(
m.files.contains_key("intent_classifier.onnx"),
"baked manifest is missing intent_classifier.onnx entry"
);
}
}