Skip to main content

concept_graph/
identifiers.rs

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