use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::io;
use std::io::Read;
use std::path::Component;
use std::path::Path;
use flate2::read::GzDecoder;
use serde::Deserialize;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Expansions {
by_url: BTreeMap<String, BTreeSet<Member>>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Member {
system: Option<String>,
code: String,
}
impl Expansions {
pub fn read(archive: impl Read) -> Result<Self, ExpansionError> {
let mut expansions = Self::default();
expansions.add(archive)?;
Ok(expansions)
}
pub fn add(&mut self, archive: impl Read) -> Result<(), ExpansionError> {
let mut tarball = tar::Archive::new(GzDecoder::new(archive));
for entry in tarball.entries().map_err(unreadable)? {
let mut entry = entry.map_err(unreadable)?;
if !entry.header().entry_type().is_file() {
continue;
}
let path = entry.path().map_err(unreadable)?.into_owned();
if !resource_file(&path) {
continue;
}
let mut bytes = Vec::new();
entry.read_to_end(&mut bytes).map_err(unreadable)?;
let file = path.to_string_lossy().into_owned();
let resource: ValueSet = serde_json::from_slice(&bytes)
.map_err(|source| ExpansionError::Json { file, source })?;
if resource.resource_type.as_deref() != Some("ValueSet") {
continue;
}
let (Some(url), Some(expansion)) = (resource.url, resource.expansion) else {
continue;
};
let mut members = BTreeSet::new();
collect(&expansion.contains, &mut members);
if self.by_url.insert(url.clone(), members).is_some() {
return Err(ExpansionError::Twice { url });
}
}
Ok(())
}
#[must_use]
pub fn has(&self, url: &str) -> bool {
self.by_url.contains_key(url)
}
#[must_use]
pub fn contains(&self, url: &str, system: Option<&str>, code: &str) -> Option<bool> {
let members = self.by_url.get(url)?;
Some(members.iter().any(|member| {
member.code == code
&& system.is_none_or(|system| member.system.as_deref() == Some(system))
}))
}
pub fn urls(&self) -> impl Iterator<Item = &str> {
self.by_url.keys().map(String::as_str)
}
}
fn collect(contains: &[Entry], members: &mut BTreeSet<Member>) {
for entry in contains {
if let Some(code) = &entry.code {
members.insert(Member {
system: entry.system.clone(),
code: code.clone(),
});
}
collect(&entry.contains, members);
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct ValueSet {
resource_type: Option<String>,
url: Option<String>,
expansion: Option<Expansion>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct Expansion {
contains: Vec<Entry>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct Entry {
system: Option<String>,
code: Option<String>,
contains: Vec<Self>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ExpansionError {
#[error("the package archive cannot be read")]
Archive {
#[source]
source: io::Error,
},
#[error("{file} does not parse")]
Json {
file: String,
#[source]
source: serde_json::Error,
},
#[error("two expanded value sets carry the canonical URL {url}")]
Twice {
url: String,
},
}
fn resource_file(path: &Path) -> bool {
let mut parts = path.components();
let (Some(Component::Normal(folder)), Some(Component::Normal(file)), None) =
(parts.next(), parts.next(), parts.next())
else {
return false;
};
let Some(file) = file.to_str() else {
return false;
};
folder == "package"
&& file != "package.json"
&& !file.starts_with('.')
&& Path::new(file)
.extension()
.is_some_and(|extension| extension == "json")
}
fn unreadable(source: io::Error) -> ExpansionError {
ExpansionError::Archive { source }
}