|
@@ -15,68 +15,69 @@ pub struct Cosmic {
|
|
|
impl FromStr for Cosmic {
|
|
impl FromStr for Cosmic {
|
|
|
type Err = anyhow::Error;
|
|
type Err = anyhow::Error;
|
|
|
|
|
|
|
|
- /// Parses a `Cosmic` instance from a semicolon-delimited string.
|
|
|
|
|
|
|
+ /// Parses COSMIC annotation fields from a semicolon-delimited string.
|
|
|
///
|
|
///
|
|
|
- /// # Expected Input Format
|
|
|
|
|
- /// The input string must follow the format:
|
|
|
|
|
|
|
+ /// # Accepted Input
|
|
|
///
|
|
///
|
|
|
- /// ```text
|
|
|
|
|
- /// <field1>;<field2>;CNT=<number>
|
|
|
|
|
- /// ```
|
|
|
|
|
|
|
+ /// The input is expected to be a `;`-separated list of `key=value` fields.
|
|
|
|
|
+ /// The `CNT` field may appear **anywhere** in the string.
|
|
|
///
|
|
///
|
|
|
- /// - The input must contain exactly three parts, separated by semicolons (`;`).
|
|
|
|
|
- /// - The third part must be of the form `CNT=<number>`, where `<number>` can be parsed as a `u64`.
|
|
|
|
|
- /// - If the first part contains the word `"MISSING"`, parsing will fail.
|
|
|
|
|
|
|
+ /// Examples of valid inputs:
|
|
|
///
|
|
///
|
|
|
- /// Generated with echtvar encode json:
|
|
|
|
|
- /// ```json
|
|
|
|
|
- /// [{"field":"GENOME_SCREEN_SAMPLE_COUNT", "alias": "CNT"}]
|
|
|
|
|
|
|
+ /// ```text
|
|
|
|
|
+ /// CNT=188
|
|
|
|
|
+ /// foo=bar;CNT=42
|
|
|
|
|
+ /// X=1;Y=2;CNT=7;Z=3
|
|
|
/// ```
|
|
/// ```
|
|
|
///
|
|
///
|
|
|
- /// # Examples
|
|
|
|
|
|
|
+ /// # Missing Data
|
|
|
///
|
|
///
|
|
|
- /// ```
|
|
|
|
|
- /// use your_crate::Cosmic;
|
|
|
|
|
- /// use std::str::FromStr;
|
|
|
|
|
|
|
+ /// - If the string contains the literal `"MISSING"`, parsing fails.
|
|
|
|
|
+ /// - If no `CNT` field is present, parsing fails.
|
|
|
///
|
|
///
|
|
|
- /// let input = "ID1;info;CNT=42";
|
|
|
|
|
- /// let cosmic = Cosmic::from_str(input).unwrap();
|
|
|
|
|
- /// assert_eq!(cosmic.cosmic_cnt, 42);
|
|
|
|
|
- /// ```
|
|
|
|
|
|
|
+ /// Handling of missing COSMIC annotation (i.e. mapping to `Option<Cosmic>`)
|
|
|
|
|
+ /// is expected to be performed by the caller.
|
|
|
///
|
|
///
|
|
|
/// # Errors
|
|
/// # Errors
|
|
|
///
|
|
///
|
|
|
- /// - Returns an error if the string does not contain exactly three semicolon-separated parts.
|
|
|
|
|
- /// - Returns an error if `"MISSING"` is found in the first part.
|
|
|
|
|
- /// - Returns an error if the third part is not in `key=value` format.
|
|
|
|
|
- /// - Returns an error if the value is not a valid `u64`.
|
|
|
|
|
|
|
+ /// Returns an error if:
|
|
|
|
|
+ /// - The input string is empty.
|
|
|
|
|
+ /// - A `CNT` field is present but cannot be parsed as `u64`.
|
|
|
|
|
+ /// - No `CNT` field is found.
|
|
|
fn from_str(s: &str) -> anyhow::Result<Self> {
|
|
fn from_str(s: &str) -> anyhow::Result<Self> {
|
|
|
let s = s.trim();
|
|
let s = s.trim();
|
|
|
- let vs: Vec<&str> = s.split(";").map(str::trim).collect();
|
|
|
|
|
- if vs.len() != 3 {
|
|
|
|
|
- return Err(anyhow::anyhow!(
|
|
|
|
|
- "Expected 3 semicolon-separated parts in Cosmic string, got {}: {s}",
|
|
|
|
|
- vs.len()
|
|
|
|
|
- ));
|
|
|
|
|
|
|
+ if s.is_empty() {
|
|
|
|
|
+ return Err(anyhow::anyhow!("Empty Cosmic string"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if vs[0].contains("MISSING") {
|
|
|
|
|
|
|
+ // Keep your existing semantics: MISSING => parsing fails
|
|
|
|
|
+ if s.contains("MISSING") {
|
|
|
return Err(anyhow::anyhow!("MISSING values in Cosmic results: {s}"));
|
|
return Err(anyhow::anyhow!("MISSING values in Cosmic results: {s}"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- let v: Vec<&str> = vs[2].split("=").map(str::trim).collect();
|
|
|
|
|
|
|
+ // Scan semicolon-delimited key=value fields and look for CNT
|
|
|
|
|
+ let mut cnt: Option<u64> = None;
|
|
|
|
|
|
|
|
- if v.len() != 2 {
|
|
|
|
|
- return Err(anyhow::anyhow!(
|
|
|
|
|
- "Expected key=value format in third field: {}",
|
|
|
|
|
- vs[2]
|
|
|
|
|
- ));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for part in s.split(';').map(str::trim).filter(|p| !p.is_empty()) {
|
|
|
|
|
+ let (k, v) = match part.split_once('=') {
|
|
|
|
|
+ Some(kv) => kv,
|
|
|
|
|
+ None => continue, // ignore non key=value tokens if any
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- let count = v[1]
|
|
|
|
|
- .parse::<u64>()
|
|
|
|
|
- .map_err(|e| anyhow::anyhow!("Failed to parse COSMIC CNT from '{}': {}", v[1], e))?;
|
|
|
|
|
|
|
+ let key = k.trim();
|
|
|
|
|
+ let val = v.trim();
|
|
|
|
|
+
|
|
|
|
|
+ if key == "CNT" {
|
|
|
|
|
+ let parsed = val
|
|
|
|
|
+ .parse::<u64>()
|
|
|
|
|
+ .map_err(|e| anyhow::anyhow!("Failed to parse COSMIC CNT from '{val}': {e}"))?;
|
|
|
|
|
+ cnt = Some(parsed);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- Ok(Cosmic { cosmic_cnt: count })
|
|
|
|
|
|
|
+ let cosmic_cnt =
|
|
|
|
|
+ cnt.ok_or_else(|| anyhow::anyhow!("Missing CNT field in Cosmic string: {s}"))?;
|
|
|
|
|
+ Ok(Cosmic { cosmic_cnt })
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|