use std::collections::BTreeMap;
use std::fmt;
use std::path::Path;
use std::time::Duration;
use serde::Deserialize;
use crate::adapters_external::{
default_decode_args, default_encode_args, resolve_cmd, ExternalCodec, InputFormat, OutputFormat,
};
use crate::adapters_external::DEFAULT_TIMEOUT_SECS;
use crate::subprocess::SampleDtype;
fn default_spec_version() -> String {
crate::SPEC_VERSION.to_string()
}
fn default_timeout_secs() -> u64 {
DEFAULT_TIMEOUT_SECS
}
#[derive(Debug, Clone, Deserialize)]
pub struct CodecManifest {
#[serde(default = "default_spec_version")]
pub spec_version: String,
pub codec: CodecSpec,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CodecSpec {
pub name: String,
pub cmd: String,
pub declared_lossless: bool,
#[serde(default)]
pub prefix_args: Vec<String>,
#[serde(default)]
pub sample_dtype: SampleDtype,
#[serde(default)]
pub input_format: InputFormat,
#[serde(default)]
pub output_format: OutputFormat,
#[serde(default = "default_timeout_secs")]
pub timeout_secs: u64,
pub encode_args: Option<Vec<String>>,
pub decode_args: Option<Vec<String>>,
#[serde(default)]
pub env: BTreeMap<String, String>,
}
#[derive(Debug)]
pub enum ManifestError {
Io(std::io::Error),
Parse(toml::de::Error),
UnsupportedVersion(String),
}
impl fmt::Display for ManifestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ManifestError::Io(e) => write!(f, "reading codec manifest: {e}"),
ManifestError::Parse(e) => write!(f, "parsing codec manifest: {e}"),
ManifestError::UnsupportedVersion(v) => write!(
f,
"codec manifest spec_version {v:?} has a major this grader (OpenECS {}) does not implement",
crate::SPEC_VERSION
),
}
}
}
impl std::error::Error for ManifestError {}
pub fn load_codec_manifest<P: AsRef<Path>>(path: P) -> Result<CodecManifest, ManifestError> {
let text = std::fs::read_to_string(path).map_err(ManifestError::Io)?;
let manifest: CodecManifest = toml::from_str(&text).map_err(ManifestError::Parse)?;
match crate::spec_major(&manifest.spec_version) {
Some(m) if m <= crate::SPEC_MAJOR => Ok(manifest),
_ => Err(ManifestError::UnsupportedVersion(manifest.spec_version)),
}
}
impl CodecManifest {
pub fn into_adapter(&self) -> Option<ExternalCodec> {
let c = &self.codec;
let cmd = resolve_cmd(&c.name, &c.cmd)?;
let encode = c.encode_args.clone().unwrap_or_else(default_encode_args);
let decode = c.decode_args.clone().unwrap_or_else(default_decode_args);
let env: Vec<(String, String)> =
c.env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
Some(
ExternalCodec::new(&c.name, cmd)
.with_prefix_args(c.prefix_args.clone())
.with_templates(encode, decode)
.with_env(env)
.with_declared_lossless(c.declared_lossless)
.with_sample_dtype(c.sample_dtype)
.with_formats(c.input_format, c.output_format)
.with_timeout(Duration::from_secs(c.timeout_secs)),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::Codec;
const MINIMAL: &str = r#"
[codec]
name = "gzip-ext"
cmd = "python3"
declared_lossless = true
"#;
const FULL: &str = r#"
spec_version = "1.0"
[codec]
name = "neural-lmq"
cmd = "/nonexistent/codec"
declared_lossless = false
prefix_args = ["-m", "neural_lmq.cli"]
sample_dtype = "i16"
input_format = "edf"
output_format = "ecs0"
timeout_secs = 1800
encode_args = ["enc", "{input}", "{output}"]
decode_args = ["dec", "{input}", "{output}"]
[codec.env]
CUDA_VISIBLE_DEVICES = "0"
"#;
#[test]
fn parses_minimal_with_defaults() {
let m: CodecManifest = toml::from_str(MINIMAL).expect("minimal parses");
assert_eq!(m.spec_version, crate::SPEC_VERSION); assert_eq!(m.codec.name, "gzip-ext");
assert!(m.codec.declared_lossless);
assert_eq!(m.codec.sample_dtype, SampleDtype::I32); assert_eq!(m.codec.input_format, InputFormat::Edf);
assert_eq!(m.codec.output_format, OutputFormat::Raw);
assert_eq!(m.codec.timeout_secs, DEFAULT_TIMEOUT_SECS);
assert!(m.codec.encode_args.is_none());
}
#[test]
fn parses_full() {
let m: CodecManifest = toml::from_str(FULL).expect("full parses");
assert_eq!(m.codec.sample_dtype, SampleDtype::I16);
assert_eq!(m.codec.output_format, OutputFormat::Ecs0);
assert_eq!(m.codec.timeout_secs, 1800);
assert_eq!(m.codec.prefix_args, vec!["-m", "neural_lmq.cli"]);
assert_eq!(m.codec.env.get("CUDA_VISIBLE_DEVICES").map(|s| s.as_str()), Some("0"));
}
#[test]
fn into_adapter_resolves_bare_command() {
let m: CodecManifest = toml::from_str(MINIMAL).expect("parses");
let codec = m.into_adapter().expect("bare 'python3' resolves");
assert_eq!(codec.name(), "gzip-ext");
assert!(codec.declared_lossless());
}
#[test]
fn into_adapter_none_when_path_missing() {
let m: CodecManifest = toml::from_str(FULL).expect("parses");
assert!(m.into_adapter().is_none());
}
#[test]
fn malformed_toml_errors() {
assert!(toml::from_str::<CodecManifest>("not = [valid").is_err());
assert!(toml::from_str::<CodecManifest>("spec_version = \"1.0\"").is_err());
}
#[test]
fn unsupported_major_is_refused() {
let dir = crate::subprocess::ScratchDir::new("manifest_ver").expect("scratch");
let p = dir.join("c.toml");
std::fs::write(
&p,
"spec_version = \"2.0\"\n[codec]\nname = \"x\"\ncmd = \"python3\"\ndeclared_lossless = true\n",
)
.expect("write");
match load_codec_manifest(&p) {
Err(ManifestError::UnsupportedVersion(v)) => assert_eq!(v, "2.0"),
other => panic!("expected UnsupportedVersion, got {other:?}"),
}
}
}