Skip to main content

snomed_ecl_engine/store/
identifiers.rs

1use super::{sha256, IndexSource, Section};
2use anyhow::{bail, ensure, Result};
3use serde::{Deserialize, Serialize};
4use std::io::{BufWriter, Write};
5use std::path::Path;
6use std::sync::OnceLock;
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct Identifier {
10    pub scheme: u64,
11    pub code: String,
12    pub referenced_component: u64,
13    pub module: u64,
14    pub effective_time: u32,
15    pub active: bool,
16}
17#[derive(Clone, Debug, Default, Serialize, Deserialize)]
18pub struct IdentifierIndex {
19    pub rows: Vec<Identifier>,
20}
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct IdentifierManifest {
23    pub bytes: u64,
24    pub sha256: String,
25    pub rows: usize,
26}
27impl IdentifierIndex {
28    pub fn build(mut rows: Vec<Identifier>) -> Result<Self> {
29        rows.sort_unstable_by(|a, b| (a.scheme, &a.code).cmp(&(b.scheme, &b.code)));
30        let result = Self { rows };
31        result.validate()?;
32        Ok(result)
33    }
34    pub fn validate(&self) -> Result<()> {
35        ensure!(
36            self.rows
37                .windows(2)
38                .all(|w| (w[0].scheme, &w[0].code) < (w[1].scheme, &w[1].code)),
39            "Duplicate or unsorted alternate identifiers"
40        );
41        for row in &self.rows {
42            ensure!(
43                !row.code.is_empty() && !row.code.chars().any(|c| c.is_ascii_control()),
44                "Invalid alternate identifier code"
45            );
46            ensure!(
47                [row.scheme, row.module, row.referenced_component]
48                    .iter()
49                    .all(|id| (100000..1_000_000_000_000_000_000).contains(id)),
50                "Invalid identifier SCTID"
51            );
52            ensure!(
53                super::members::valid_time(row.effective_time),
54                "Invalid identifier effective time"
55            );
56        }
57        Ok(())
58    }
59    pub fn lookup(&self, scheme: u64, code: &str) -> Option<u64> {
60        let pos = self
61            .rows
62            .binary_search_by(|r| (r.scheme, r.code.as_str()).cmp(&(scheme, code)))
63            .ok()?;
64        let row = &self.rows[pos];
65        (row.active && matches!((row.referenced_component / 10) % 100, 0 | 10))
66            .then_some(row.referenced_component)
67    }
68    pub fn write(&self, directory: &Path) -> Result<IdentifierManifest> {
69        self.validate()?;
70        let path = directory.join("identifiers.json");
71        let mut file = BufWriter::new(std::fs::File::create_new(&path)?);
72        serde_json::to_writer(&mut file, self)?;
73        file.flush()?;
74        file.get_ref().sync_all()?;
75        Ok(IdentifierManifest {
76            bytes: path.metadata()?.len(),
77            sha256: sha256(&path)?,
78            rows: self.rows.len(),
79        })
80    }
81    pub(super) fn open(section: &Section, manifest: &IdentifierManifest) -> Result<Self> {
82        section.verify()?;
83        let index: Self = serde_json::from_reader(std::io::BufReader::new(section.reader()?))?;
84        index.validate()?;
85        ensure!(
86            index.rows.len() == manifest.rows,
87            "Identifier count differs from manifest"
88        );
89        Ok(index)
90    }
91}
92#[derive(Debug, Default)]
93pub struct IdentifierStore {
94    source: Option<(Section, IdentifierManifest)>,
95    loaded: OnceLock<std::result::Result<IdentifierIndex, String>>,
96}
97impl IdentifierStore {
98    pub fn loaded(index: IdentifierIndex) -> Result<Self> {
99        index.validate()?;
100        Ok(Self {
101            source: None,
102            loaded: OnceLock::from(Ok(index)),
103        })
104    }
105    pub(super) fn lazy(source: &IndexSource, manifest: IdentifierManifest) -> Result<Self> {
106        Ok(Self {
107            source: Some((source.section("identifiers.json")?, manifest)),
108            loaded: OnceLock::new(),
109        })
110    }
111    pub fn get(&self) -> Result<Option<&IdentifierIndex>> {
112        if self.source.is_none() && self.loaded.get().is_none() {
113            return Ok(None);
114        }
115        match self.loaded.get_or_init(|| {
116            let (path, manifest) = self.source.as_ref().unwrap();
117            IdentifierIndex::open(path, manifest).map_err(|e| e.to_string())
118        }) {
119            Ok(index) => Ok(Some(index)),
120            Err(e) => bail!("Identifier index: {e}"),
121        }
122    }
123}