open_eeg_codec_standard/
manifest.rs1use std::collections::BTreeMap;
9use std::fmt;
10use std::path::Path;
11use std::time::Duration;
12
13use serde::Deserialize;
14
15use crate::adapters_external::{
16 default_decode_args, default_encode_args, resolve_cmd, ExternalCodec, InputFormat, OutputFormat,
17};
18use crate::adapters_external::DEFAULT_TIMEOUT_SECS;
19use crate::subprocess::SampleDtype;
20
21fn default_spec_version() -> String {
23 crate::SPEC_VERSION.to_string()
24}
25
26fn default_timeout_secs() -> u64 {
28 DEFAULT_TIMEOUT_SECS
29}
30
31#[derive(Debug, Clone, Deserialize)]
33pub struct CodecManifest {
34 #[serde(default = "default_spec_version")]
36 pub spec_version: String,
37 pub codec: CodecSpec,
39}
40
41#[derive(Debug, Clone, Deserialize)]
43pub struct CodecSpec {
44 pub name: String,
46 pub cmd: String,
48 pub declared_lossless: bool,
50 #[serde(default)]
52 pub prefix_args: Vec<String>,
53 #[serde(default)]
55 pub sample_dtype: SampleDtype,
56 #[serde(default)]
58 pub input_format: InputFormat,
59 #[serde(default)]
61 pub output_format: OutputFormat,
62 #[serde(default = "default_timeout_secs")]
64 pub timeout_secs: u64,
65 pub encode_args: Option<Vec<String>>,
67 pub decode_args: Option<Vec<String>>,
69 #[serde(default)]
71 pub env: BTreeMap<String, String>,
72}
73
74#[derive(Debug)]
76pub enum ManifestError {
77 Io(std::io::Error),
79 Parse(toml::de::Error),
81 UnsupportedVersion(String),
83}
84
85impl fmt::Display for ManifestError {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 ManifestError::Io(e) => write!(f, "reading codec manifest: {e}"),
89 ManifestError::Parse(e) => write!(f, "parsing codec manifest: {e}"),
90 ManifestError::UnsupportedVersion(v) => write!(
91 f,
92 "codec manifest spec_version {v:?} has a major this grader (OpenECS {}) does not implement",
93 crate::SPEC_VERSION
94 ),
95 }
96 }
97}
98
99impl std::error::Error for ManifestError {}
100
101pub fn load_codec_manifest<P: AsRef<Path>>(path: P) -> Result<CodecManifest, ManifestError> {
106 let text = std::fs::read_to_string(path).map_err(ManifestError::Io)?;
107 let manifest: CodecManifest = toml::from_str(&text).map_err(ManifestError::Parse)?;
108 match crate::spec_major(&manifest.spec_version) {
111 Some(m) if m <= crate::SPEC_MAJOR => Ok(manifest),
112 _ => Err(ManifestError::UnsupportedVersion(manifest.spec_version)),
113 }
114}
115
116impl CodecManifest {
117 pub fn into_adapter(&self) -> Option<ExternalCodec> {
121 let c = &self.codec;
122 let cmd = resolve_cmd(&c.name, &c.cmd)?;
123 let encode = c.encode_args.clone().unwrap_or_else(default_encode_args);
124 let decode = c.decode_args.clone().unwrap_or_else(default_decode_args);
125 let env: Vec<(String, String)> =
126 c.env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
127 Some(
128 ExternalCodec::new(&c.name, cmd)
129 .with_prefix_args(c.prefix_args.clone())
130 .with_templates(encode, decode)
131 .with_env(env)
132 .with_declared_lossless(c.declared_lossless)
133 .with_sample_dtype(c.sample_dtype)
134 .with_formats(c.input_format, c.output_format)
135 .with_timeout(Duration::from_secs(c.timeout_secs)),
136 )
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::adapter::Codec; const MINIMAL: &str = r#"
146 [codec]
147 name = "gzip-ext"
148 cmd = "python3"
149 declared_lossless = true
150 "#;
151
152 const FULL: &str = r#"
153 spec_version = "1.0"
154 [codec]
155 name = "neural-lmq"
156 cmd = "/nonexistent/codec"
157 declared_lossless = false
158 prefix_args = ["-m", "neural_lmq.cli"]
159 sample_dtype = "i16"
160 input_format = "edf"
161 output_format = "ecs0"
162 timeout_secs = 1800
163 encode_args = ["enc", "{input}", "{output}"]
164 decode_args = ["dec", "{input}", "{output}"]
165 [codec.env]
166 CUDA_VISIBLE_DEVICES = "0"
167 "#;
168
169 #[test]
170 fn parses_minimal_with_defaults() {
171 let m: CodecManifest = toml::from_str(MINIMAL).expect("minimal parses");
172 assert_eq!(m.spec_version, crate::SPEC_VERSION); assert_eq!(m.codec.name, "gzip-ext");
174 assert!(m.codec.declared_lossless);
175 assert_eq!(m.codec.sample_dtype, SampleDtype::I32); assert_eq!(m.codec.input_format, InputFormat::Edf);
177 assert_eq!(m.codec.output_format, OutputFormat::Raw);
178 assert_eq!(m.codec.timeout_secs, DEFAULT_TIMEOUT_SECS);
179 assert!(m.codec.encode_args.is_none());
180 }
181
182 #[test]
183 fn parses_full() {
184 let m: CodecManifest = toml::from_str(FULL).expect("full parses");
185 assert_eq!(m.codec.sample_dtype, SampleDtype::I16);
186 assert_eq!(m.codec.output_format, OutputFormat::Ecs0);
187 assert_eq!(m.codec.timeout_secs, 1800);
188 assert_eq!(m.codec.prefix_args, vec!["-m", "neural_lmq.cli"]);
189 assert_eq!(m.codec.env.get("CUDA_VISIBLE_DEVICES").map(|s| s.as_str()), Some("0"));
190 }
191
192 #[test]
193 fn into_adapter_resolves_bare_command() {
194 let m: CodecManifest = toml::from_str(MINIMAL).expect("parses");
195 let codec = m.into_adapter().expect("bare 'python3' resolves");
196 assert_eq!(codec.name(), "gzip-ext");
197 assert!(codec.declared_lossless());
198 }
199
200 #[test]
201 fn into_adapter_none_when_path_missing() {
202 let m: CodecManifest = toml::from_str(FULL).expect("parses");
204 assert!(m.into_adapter().is_none());
205 }
206
207 #[test]
208 fn malformed_toml_errors() {
209 assert!(toml::from_str::<CodecManifest>("not = [valid").is_err());
210 assert!(toml::from_str::<CodecManifest>("spec_version = \"1.0\"").is_err());
212 }
213
214 #[test]
215 fn unsupported_major_is_refused() {
216 let dir = crate::subprocess::ScratchDir::new("manifest_ver").expect("scratch");
218 let p = dir.join("c.toml");
219 std::fs::write(
220 &p,
221 "spec_version = \"2.0\"\n[codec]\nname = \"x\"\ncmd = \"python3\"\ndeclared_lossless = true\n",
222 )
223 .expect("write");
224 match load_codec_manifest(&p) {
225 Err(ManifestError::UnsupportedVersion(v)) => assert_eq!(v, "2.0"),
226 other => panic!("expected UnsupportedVersion, got {other:?}"),
227 }
228 }
229}