use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum ReleaseError {
#[error("cannot read {path}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("the release under {root} has no {name}")]
Missing {
root: PathBuf,
name: &'static str,
},
#[error("cannot parse {path}")]
Csv {
path: PathBuf,
#[source]
source: csv::Error,
},
#[error("{path} has no column `{column}`")]
Column {
path: PathBuf,
column: &'static str,
},
#[error("{path}: `{code}` is not a LOINC code")]
Code {
path: PathBuf,
code: String,
},
}
pub const TERMS: &str = "Loinc.csv";
pub const PARTS: &str = "Part.csv";
pub const HIERARCHY: &str = "ComponentHierarchyBySystem.csv";
pub const LINKS: &str = "LoincPartLink_Primary.csv";
pub const ANSWER_LISTS: &str = "AnswerList.csv";
pub const ANSWER_LINKS: &str = "LoincAnswerListLink.csv";
pub const VARIANT_SUFFIX: &str = "LinguisticVariant.csv";
#[derive(Debug, Clone)]
pub struct Release {
root: PathBuf,
files: Vec<PathBuf>,
}
impl Release {
pub fn open(root: &Path) -> Result<Self, ReleaseError> {
let mut files = Vec::new();
walk(root, &mut files)?;
files.sort();
let release = Self {
root: root.to_path_buf(),
files,
};
release.file(TERMS)?;
Ok(release)
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
pub fn file(&self, name: &'static str) -> Result<&Path, ReleaseError> {
self.optional(name).ok_or_else(|| ReleaseError::Missing {
root: self.root.clone(),
name,
})
}
#[must_use]
pub fn optional(&self, name: &str) -> Option<&Path> {
self.files
.iter()
.filter(|p| {
p.file_name()
.and_then(|f| f.to_str())
.is_some_and(|f| f.eq_ignore_ascii_case(name))
})
.min_by_key(|p| p.components().count())
.map(PathBuf::as_path)
}
pub fn variants(&self) -> impl Iterator<Item = &Path> {
self.files
.iter()
.filter(|p| {
p.file_name()
.and_then(|f| f.to_str())
.is_some_and(|f| f.ends_with(VARIANT_SUFFIX))
})
.map(PathBuf::as_path)
}
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), ReleaseError> {
let entries = std::fs::read_dir(dir).map_err(|source| ReleaseError::Io {
path: dir.to_path_buf(),
source,
})?;
for entry in entries {
let path = entry
.map_err(|source| ReleaseError::Io {
path: dir.to_path_buf(),
source,
})?
.path();
if path.is_dir() {
walk(&path, out)?;
} else {
out.push(path);
}
}
Ok(())
}
pub(crate) struct Table {
pub(crate) path: PathBuf,
pub(crate) reader: csv::Reader<std::fs::File>,
header: csv::StringRecord,
}
impl Table {
pub(crate) fn open(path: &Path) -> Result<Self, ReleaseError> {
let mut reader = csv::ReaderBuilder::new()
.flexible(true)
.from_path(path)
.map_err(|source| ReleaseError::Csv {
path: path.to_path_buf(),
source,
})?;
let header = reader
.headers()
.map_err(|source| ReleaseError::Csv {
path: path.to_path_buf(),
source,
})?
.clone();
Ok(Self {
path: path.to_path_buf(),
reader,
header,
})
}
pub(crate) fn column(&self, column: &'static str) -> Result<usize, ReleaseError> {
self.header
.iter()
.position(|h| h.eq_ignore_ascii_case(column))
.ok_or_else(|| ReleaseError::Column {
path: self.path.clone(),
column,
})
}
pub(crate) fn columns(&self) -> Vec<String> {
self.header.iter().map(str::to_owned).collect()
}
}
pub(crate) fn field(record: &csv::StringRecord, index: usize) -> &str {
record.get(index).map(str::trim).unwrap_or_default()
}
pub(crate) fn csv_at(path: &Path, source: csv::Error) -> ReleaseError {
ReleaseError::Csv {
path: path.to_path_buf(),
source,
}
}