Skip to main content

snomed_ecl_engine/
config.rs

1//! Bind edition-specific names without changing the ECL parser.
2use anyhow::{ensure, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::path::Path;
6
7#[derive(Clone, Debug, Deserialize, Serialize)]
8#[serde(default, deny_unknown_fields)]
9pub struct QueryConfig {
10    #[serde(with = "id_map")]
11    pub identifier_schemes: BTreeMap<String, u64>,
12    #[serde(with = "id_map")]
13    pub dialects: BTreeMap<String, u64>,
14    pub member_language: String,
15}
16impl Default for QueryConfig {
17    fn default() -> Self {
18        Self {
19            member_language: "en".into(),
20            identifier_schemes: BTreeMap::new(),
21            dialects: BTreeMap::from([
22                ("da-dk".into(), 554461000005103),
23                ("en-au".into(), 32570271000036106),
24                ("en-ca".into(), 19491000087109),
25                ("en-gb".into(), 900000000000508004),
26                ("en-ie".into(), 21000220103),
27                ("en-nz".into(), 271000210107),
28                ("en-nz-x-pat".into(), 281000210109),
29                ("en-us".into(), 900000000000509007),
30                ("en-x-gmdn".into(), 608771002),
31                ("en-x-nhs-clinical".into(), 999001261000000100),
32                ("en-x-nhs-dmd".into(), 999000671000001103),
33                ("en-x-nhs-pharmacy".into(), 999000691000001104),
34                ("en-gb-x-drug".into(), 999000681000001101),
35                ("en-gb-x-ext".into(), 999001251000000103),
36                ("es".into(), 450828004),
37                ("es-uy".into(), 5641000179103),
38                ("et-ee".into(), 71000181105),
39                ("de".into(), 722130004),
40                ("fr".into(), 722131000),
41                ("fr-be".into(), 21000172104),
42                ("fr-ca".into(), 20581000087109),
43                ("ja".into(), 722129009),
44                ("mi".into(), 291000210106),
45                ("nl-be".into(), 31000172101),
46                ("nl-nl".into(), 31000146106),
47                ("nb-no".into(), 61000202103),
48                ("nn-no".into(), 91000202106),
49                ("sv-se".into(), 46011000052107),
50                ("zh".into(), 722128001),
51            ]),
52        }
53    }
54}
55impl QueryConfig {
56    pub fn fingerprint(&self) -> Result<String> {
57        use sha2::{Digest, Sha256};
58        Ok(format!("{:x}", Sha256::digest(serde_json::to_vec(self)?)))
59    }
60    pub fn read(path: &Path) -> Result<Self> {
61        let mut config: Self = serde_json::from_reader(std::fs::File::open(path)?)?;
62        config.normalise()?;
63        Ok(config)
64    }
65    pub fn normalise(&mut self) -> Result<()> {
66        ensure!(
67            self.member_language.len() == 2
68                && self
69                    .member_language
70                    .bytes()
71                    .all(|b| b.is_ascii_alphabetic()),
72            "Member language must be a two-letter language code"
73        );
74        self.member_language.make_ascii_lowercase();
75        for map in [&mut self.identifier_schemes, &mut self.dialects] {
76            let mut result = BTreeMap::new();
77            for (name, id) in map.iter() {
78                ensure!(valid_alias(name), "Invalid alias name");
79                ensure!(
80                    (100000..1_000_000_000_000_000_000).contains(id),
81                    "Invalid alias SCTID"
82                );
83                ensure!(
84                    result.insert(name.to_ascii_lowercase(), *id).is_none(),
85                    "Duplicate alias ignoring case"
86                );
87            }
88            *map = result;
89        }
90        Ok(())
91    }
92    pub fn member_language_code(&self) -> Option<[u8; 2]> {
93        self.member_language
94            .as_bytes()
95            .try_into()
96            .ok()
97            .filter(|code: &[u8; 2]| code.iter().all(u8::is_ascii_lowercase))
98    }
99}
100pub(crate) fn valid_alias(name: &str) -> bool {
101    name.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
102        && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
103}
104mod id_map {
105    use super::*;
106    pub fn serialize<S: serde::Serializer>(
107        map: &BTreeMap<String, u64>,
108        serializer: S,
109    ) -> Result<S::Ok, S::Error> {
110        map.iter()
111            .map(|(k, v)| (k, v.to_string()))
112            .collect::<BTreeMap<_, _>>()
113            .serialize(serializer)
114    }
115    pub fn deserialize<'de, D: serde::Deserializer<'de>>(
116        deserializer: D,
117    ) -> Result<BTreeMap<String, u64>, D::Error> {
118        let values = BTreeMap::<String, String>::deserialize(deserializer)?;
119        values
120            .into_iter()
121            .map(|(k, v)| {
122                if v.is_empty() || !v.bytes().all(|b| b.is_ascii_digit()) {
123                    return Err(serde::de::Error::custom("SCTIDs must be decimal strings"));
124                }
125                v.parse()
126                    .map(|id| (k, id))
127                    .map_err(serde::de::Error::custom)
128            })
129            .collect()
130    }
131}