Skip to main content

concept_graph/
identifiers.rs

1//! The alternate identifiers of an edition: a code of another scheme
2//! (`LOINC` `54486-6`) for a concept, from the RF2 identifier file, so an
3//! ECL `scheme#code` focus resolves to the concept.
4//!
5//! No spec governs the layout: our own design. Little-endian, a magic and
6//! version prefix, a count, then per identifier the scheme concept SCTID, the
7//! code, and the concept ordinal, sorted by scheme then code.
8
9use std::io::{self, Read, Write};
10
11use crate::ordinal::{Ordinal, to_usize};
12
13const MAGIC: &[u8; 8] = b"FTIDENT\0";
14const VERSION: u32 = 1;
15
16/// A failure while reading or writing the identifiers.
17#[derive(Debug, thiserror::Error)]
18pub enum IdentifiersError {
19    /// An I/O failure.
20    #[error("identifiers I/O failed")]
21    Io(#[from] io::Error),
22    /// The bytes do not start with the identifiers magic.
23    #[error("not an identifiers artifact")]
24    Magic,
25    /// The layout version is not the one this build reads.
26    #[error("identifiers layout version {found}, expected {expected}")]
27    Version {
28        /// The version found.
29        found: u32,
30        /// The version this build reads.
31        expected: u32,
32    },
33    /// More identifiers than the `u32` count addresses.
34    #[error("too many identifiers")]
35    TooMany(#[source] std::num::TryFromIntError),
36    /// A code is not UTF-8.
37    #[error("an alternate identifier is not UTF-8")]
38    Text(#[from] std::string::FromUtf8Error),
39}
40
41/// The alternate identifiers, sorted by scheme then code.
42#[derive(Debug, Clone, PartialEq, Eq, Default)]
43pub struct Identifiers {
44    entries: Vec<(u64, String, u32)>,
45}
46
47impl Identifiers {
48    /// Builds the table from `(scheme SCTID, code, concept)` entries.
49    #[must_use]
50    pub fn new(mut entries: Vec<(u64, String, Ordinal)>) -> Self {
51        entries.sort();
52        entries.dedup();
53        Self {
54            entries: entries
55                .into_iter()
56                .map(|(scheme, code, concept)| (scheme, code, concept.index()))
57                .collect(),
58        }
59    }
60
61    /// The heap bytes the table holds, its codes included
62    /// ([`crate::footprint`]).
63    #[must_use]
64    pub fn size_in_bytes(&self) -> usize {
65        self.entries
66            .iter()
67            .fold(crate::footprint::vector(&self.entries), |total, entry| {
68                total.saturating_add(entry.1.capacity())
69            })
70    }
71
72    /// The concept identified by `code` in `scheme`.
73    #[must_use]
74    pub fn lookup(&self, scheme: u64, code: &str) -> Option<Ordinal> {
75        self.entries
76            .binary_search_by(|(s, c, _)| (*s, c.as_str()).cmp(&(scheme, code)))
77            .ok()
78            .and_then(|i| self.entries.get(i))
79            .map(|(_, _, concept)| Ordinal::new(*concept))
80    }
81
82    /// The identifier schemes present, ascending.
83    #[must_use]
84    pub fn schemes(&self) -> Vec<u64> {
85        let mut schemes: Vec<u64> = self.entries.iter().map(|(s, _, _)| *s).collect();
86        schemes.dedup();
87        schemes
88    }
89
90    /// The number of identifiers.
91    #[must_use]
92    pub fn len(&self) -> usize {
93        self.entries.len()
94    }
95
96    /// Whether there are no identifiers.
97    #[must_use]
98    pub fn is_empty(&self) -> bool {
99        self.entries.is_empty()
100    }
101
102    /// Writes the layout.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`IdentifiersError::Io`] when writing fails.
107    pub fn write_to(&self, out: &mut impl Write) -> Result<(), IdentifiersError> {
108        out.write_all(MAGIC)?;
109        out.write_all(&VERSION.to_le_bytes())?;
110        let count = u32::try_from(self.entries.len()).map_err(IdentifiersError::TooMany)?;
111        out.write_all(&count.to_le_bytes())?;
112        for (scheme, code, concept) in &self.entries {
113            out.write_all(&scheme.to_le_bytes())?;
114            let len = u32::try_from(code.len()).map_err(IdentifiersError::TooMany)?;
115            out.write_all(&len.to_le_bytes())?;
116            out.write_all(code.as_bytes())?;
117            out.write_all(&concept.to_le_bytes())?;
118        }
119        Ok(())
120    }
121
122    /// Reads the layout.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`IdentifiersError`] for a truncated or foreign artifact.
127    pub fn read_from(input: &mut impl Read) -> Result<Self, IdentifiersError> {
128        let mut magic = [0_u8; 8];
129        input.read_exact(&mut magic)?;
130        if &magic != MAGIC {
131            return Err(IdentifiersError::Magic);
132        }
133        let version = read_u32(input)?;
134        if version != VERSION {
135            return Err(IdentifiersError::Version {
136                found: version,
137                expected: VERSION,
138            });
139        }
140        let count = read_u32(input)?;
141        let mut entries = Vec::with_capacity(to_usize(count));
142        for _ in 0..count {
143            let mut long = [0_u8; 8];
144            input.read_exact(&mut long)?;
145            let len = to_usize(read_u32(input)?);
146            let mut bytes = vec![0_u8; len];
147            input.read_exact(&mut bytes)?;
148            let concept = read_u32(input)?;
149            entries.push((u64::from_le_bytes(long), String::from_utf8(bytes)?, concept));
150        }
151        entries.sort();
152        Ok(Self { entries })
153    }
154}
155
156fn read_u32(input: &mut impl Read) -> Result<u32, IdentifiersError> {
157    let mut bytes = [0_u8; 4];
158    input.read_exact(&mut bytes)?;
159    Ok(u32::from_le_bytes(bytes))
160}
161
162#[cfg(test)]
163mod tests {
164    use super::{Identifiers, IdentifiersError};
165    use crate::ordinal::Ordinal;
166
167    #[test]
168    fn codes_resolve_per_scheme_and_the_layout_round_trips() {
169        let identifiers = Identifiers::new(vec![
170            (705_114_005, String::from("54486-6"), Ordinal::new(3)),
171            (705_114_005, String::from("1234-5"), Ordinal::new(4)),
172            (900, String::from("54486-6"), Ordinal::new(5)),
173        ]);
174        assert_eq!(
175            identifiers.lookup(705_114_005, "54486-6"),
176            Some(Ordinal::new(3))
177        );
178        assert_eq!(identifiers.lookup(900, "54486-6"), Some(Ordinal::new(5)));
179        assert_eq!(identifiers.lookup(705_114_005, "9999"), None);
180        assert_eq!(identifiers.schemes(), [900, 705_114_005]);
181        let mut bytes = Vec::new();
182        identifiers.write_to(&mut bytes).expect("writes");
183        assert_eq!(
184            Identifiers::read_from(&mut bytes.as_slice()).expect("reads"),
185            identifiers
186        );
187        assert!(matches!(
188            Identifiers::read_from(&mut b"XXXXXXXX\0\0\0\0".as_slice()),
189            Err(IdentifiersError::Magic)
190        ));
191    }
192}