snomed-ecl-engine 0.1.1

Compact SNOMED CT ECL engine and RF2 index builder
Documentation
use super::{sha256, IndexSource, Section};
use anyhow::{bail, ensure, Result};
use serde::{Deserialize, Serialize};
use std::io::{BufWriter, Write};
use std::path::Path;
use std::sync::OnceLock;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Identifier {
    pub scheme: u64,
    pub code: String,
    pub referenced_component: u64,
    pub module: u64,
    pub effective_time: u32,
    pub active: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct IdentifierIndex {
    pub rows: Vec<Identifier>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IdentifierManifest {
    pub bytes: u64,
    pub sha256: String,
    pub rows: usize,
}
impl IdentifierIndex {
    pub fn build(mut rows: Vec<Identifier>) -> Result<Self> {
        rows.sort_unstable_by(|a, b| (a.scheme, &a.code).cmp(&(b.scheme, &b.code)));
        let result = Self { rows };
        result.validate()?;
        Ok(result)
    }
    pub fn validate(&self) -> Result<()> {
        ensure!(
            self.rows
                .windows(2)
                .all(|w| (w[0].scheme, &w[0].code) < (w[1].scheme, &w[1].code)),
            "Duplicate or unsorted alternate identifiers"
        );
        for row in &self.rows {
            ensure!(
                !row.code.is_empty() && !row.code.chars().any(|c| c.is_ascii_control()),
                "Invalid alternate identifier code"
            );
            ensure!(
                [row.scheme, row.module, row.referenced_component]
                    .iter()
                    .all(|id| (100000..1_000_000_000_000_000_000).contains(id)),
                "Invalid identifier SCTID"
            );
            ensure!(
                super::members::valid_time(row.effective_time),
                "Invalid identifier effective time"
            );
        }
        Ok(())
    }
    pub fn lookup(&self, scheme: u64, code: &str) -> Option<u64> {
        let pos = self
            .rows
            .binary_search_by(|r| (r.scheme, r.code.as_str()).cmp(&(scheme, code)))
            .ok()?;
        let row = &self.rows[pos];
        (row.active && matches!((row.referenced_component / 10) % 100, 0 | 10))
            .then_some(row.referenced_component)
    }
    pub fn write(&self, directory: &Path) -> Result<IdentifierManifest> {
        self.validate()?;
        let path = directory.join("identifiers.json");
        let mut file = BufWriter::new(std::fs::File::create_new(&path)?);
        serde_json::to_writer(&mut file, self)?;
        file.flush()?;
        file.get_ref().sync_all()?;
        Ok(IdentifierManifest {
            bytes: path.metadata()?.len(),
            sha256: sha256(&path)?,
            rows: self.rows.len(),
        })
    }
    pub(super) fn open(section: &Section, manifest: &IdentifierManifest) -> Result<Self> {
        section.verify()?;
        let index: Self = serde_json::from_reader(std::io::BufReader::new(section.reader()?))?;
        index.validate()?;
        ensure!(
            index.rows.len() == manifest.rows,
            "Identifier count differs from manifest"
        );
        Ok(index)
    }
}
#[derive(Debug, Default)]
pub struct IdentifierStore {
    source: Option<(Section, IdentifierManifest)>,
    loaded: OnceLock<std::result::Result<IdentifierIndex, String>>,
}
impl IdentifierStore {
    pub fn loaded(index: IdentifierIndex) -> Result<Self> {
        index.validate()?;
        Ok(Self {
            source: None,
            loaded: OnceLock::from(Ok(index)),
        })
    }
    pub(super) fn lazy(source: &IndexSource, manifest: IdentifierManifest) -> Result<Self> {
        Ok(Self {
            source: Some((source.section("identifiers.json")?, manifest)),
            loaded: OnceLock::new(),
        })
    }
    pub fn get(&self) -> Result<Option<&IdentifierIndex>> {
        if self.source.is_none() && self.loaded.get().is_none() {
            return Ok(None);
        }
        match self.loaded.get_or_init(|| {
            let (path, manifest) = self.source.as_ref().unwrap();
            IdentifierIndex::open(path, manifest).map_err(|e| e.to_string())
        }) {
            Ok(index) => Ok(Some(index)),
            Err(e) => bail!("Identifier index: {e}"),
        }
    }
}