1use std::fmt;
7
8use concept_graph::ordinal::Ordinal;
9
10#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12pub enum RecordError {
13 #[error("record truncated at byte {at}")]
15 Truncated {
16 at: usize,
18 },
19 #[error("string at byte {at} is not UTF-8")]
21 Utf8 {
22 at: usize,
24 #[source]
26 source: std::str::Utf8Error,
27 },
28 #[error("unknown property value tag {tag}")]
30 Tag {
31 tag: u8,
33 },
34 #[error("{remaining} trailing byte(s) after the record")]
36 Trailing {
37 remaining: usize,
39 },
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Concept {
45 pub code: String,
47 pub active: bool,
49 pub effective_time: Option<String>,
51 pub module: Option<Ordinal>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Designation {
58 pub id: Option<String>,
60 pub term: String,
62 pub language: String,
64 pub use_ordinal: u32,
66 pub active: bool,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum PropertyValue {
73 Concept(Ordinal),
75 Code(String),
77 String(String),
79 Integer(i64),
81 Boolean(bool),
83 Decimal(String),
85 DateTime(String),
87}
88
89impl fmt::Display for PropertyValue {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Self::Concept(o) => write!(f, "{o}"),
93 Self::Code(s) | Self::String(s) | Self::Decimal(s) | Self::DateTime(s) => {
94 f.write_str(s)
95 }
96 Self::Integer(i) => write!(f, "{i}"),
97 Self::Boolean(b) => write!(f, "{b}"),
98 }
99 }
100}
101
102struct Writer(Vec<u8>);
103
104impl Writer {
105 fn u8(&mut self, v: u8) {
106 self.0.push(v);
107 }
108 fn u32(&mut self, v: u32) {
109 self.0.extend_from_slice(&v.to_le_bytes());
110 }
111 fn i64(&mut self, v: i64) {
112 self.0.extend_from_slice(&v.to_le_bytes());
113 }
114 fn str(&mut self, s: &str) {
115 self.u32(u32::try_from(s.len()).unwrap_or(u32::MAX));
116 self.0.extend_from_slice(s.as_bytes());
117 }
118 fn opt_str(&mut self, s: Option<&str>) {
119 match s {
120 Some(s) => {
121 self.u8(1);
122 self.str(s);
123 }
124 None => self.u8(0),
125 }
126 }
127}
128
129struct Reader<'a> {
130 bytes: &'a [u8],
131 at: usize,
132}
133
134impl<'a> Reader<'a> {
135 fn take(&mut self, n: usize) -> Result<&'a [u8], RecordError> {
136 let end = self
137 .at
138 .checked_add(n)
139 .ok_or(RecordError::Truncated { at: self.at })?;
140 let slice = self
141 .bytes
142 .get(self.at..end)
143 .ok_or(RecordError::Truncated { at: self.at })?;
144 self.at = end;
145 Ok(slice)
146 }
147 fn u8(&mut self) -> Result<u8, RecordError> {
148 self.take(1)?
149 .first()
150 .copied()
151 .ok_or(RecordError::Truncated { at: self.at })
152 }
153 fn u32(&mut self) -> Result<u32, RecordError> {
154 let Ok(bytes) = <[u8; 4]>::try_from(self.take(4)?) else {
156 return Err(RecordError::Truncated { at: self.at });
157 };
158 Ok(u32::from_le_bytes(bytes))
159 }
160 fn i64(&mut self) -> Result<i64, RecordError> {
161 let Ok(bytes) = <[u8; 8]>::try_from(self.take(8)?) else {
162 return Err(RecordError::Truncated { at: self.at });
163 };
164 Ok(i64::from_le_bytes(bytes))
165 }
166 fn str(&mut self) -> Result<String, RecordError> {
167 let Ok(len) = usize::try_from(self.u32()?) else {
168 return Err(RecordError::Truncated { at: self.at });
169 };
170 let at = self.at;
171 let bytes = self.take(len)?;
172 std::str::from_utf8(bytes)
173 .map(str::to_owned)
174 .map_err(|source| RecordError::Utf8 { at, source })
175 }
176 fn opt_str(&mut self) -> Result<Option<String>, RecordError> {
177 match self.u8()? {
178 0 => Ok(None),
179 _ => self.str().map(Some),
180 }
181 }
182 fn finish(self) -> Result<(), RecordError> {
183 let remaining = self.bytes.len().saturating_sub(self.at);
184 if remaining == 0 {
185 Ok(())
186 } else {
187 Err(RecordError::Trailing { remaining })
188 }
189 }
190}
191
192impl Concept {
193 #[must_use]
195 pub fn encode(&self) -> Vec<u8> {
196 let mut w = Writer(Vec::new());
197 w.str(&self.code);
198 w.u8(u8::from(self.active));
199 w.opt_str(self.effective_time.as_deref());
200 match self.module {
201 Some(m) => {
202 w.u8(1);
203 w.u32(m.index());
204 }
205 None => w.u8(0),
206 }
207 w.0
208 }
209
210 pub fn decode(bytes: &[u8]) -> Result<Self, RecordError> {
216 let mut r = Reader { bytes, at: 0 };
217 let code = r.str()?;
218 let active = r.u8()? != 0;
219 let effective_time = r.opt_str()?;
220 let module = match r.u8()? {
221 0 => None,
222 _ => Some(Ordinal::new(r.u32()?)),
223 };
224 r.finish()?;
225 Ok(Self {
226 code,
227 active,
228 effective_time,
229 module,
230 })
231 }
232}
233
234impl Designation {
235 #[must_use]
237 pub fn encode(&self) -> Vec<u8> {
238 let mut w = Writer(Vec::new());
239 w.opt_str(self.id.as_deref());
240 w.str(&self.term);
241 w.str(&self.language);
242 w.u32(self.use_ordinal);
243 w.u8(u8::from(self.active));
244 w.0
245 }
246
247 pub fn decode(bytes: &[u8]) -> Result<Self, RecordError> {
253 let mut r = Reader { bytes, at: 0 };
254 let designation = Self {
255 id: r.opt_str()?,
256 term: r.str()?,
257 language: r.str()?,
258 use_ordinal: r.u32()?,
259 active: r.u8()? != 0,
260 };
261 r.finish()?;
262 Ok(designation)
263 }
264}
265
266impl PropertyValue {
267 fn encode_into(&self, w: &mut Writer) {
268 match self {
269 Self::Concept(o) => {
270 w.u8(0);
271 w.u32(o.index());
272 }
273 Self::Code(s) => {
274 w.u8(1);
275 w.str(s);
276 }
277 Self::String(s) => {
278 w.u8(2);
279 w.str(s);
280 }
281 Self::Integer(i) => {
282 w.u8(3);
283 w.i64(*i);
284 }
285 Self::Boolean(b) => {
286 w.u8(4);
287 w.u8(u8::from(*b));
288 }
289 Self::Decimal(s) => {
290 w.u8(5);
291 w.str(s);
292 }
293 Self::DateTime(s) => {
294 w.u8(6);
295 w.str(s);
296 }
297 }
298 }
299
300 fn decode_from(r: &mut Reader<'_>) -> Result<Self, RecordError> {
301 Ok(match r.u8()? {
302 0 => Self::Concept(Ordinal::new(r.u32()?)),
303 1 => Self::Code(r.str()?),
304 2 => Self::String(r.str()?),
305 3 => Self::Integer(r.i64()?),
306 4 => Self::Boolean(r.u8()? != 0),
307 5 => Self::Decimal(r.str()?),
308 6 => Self::DateTime(r.str()?),
309 tag => return Err(RecordError::Tag { tag }),
310 })
311 }
312
313 #[must_use]
315 pub fn encode_list(values: &[Self]) -> Vec<u8> {
316 let mut w = Writer(Vec::new());
317 w.u32(u32::try_from(values.len()).unwrap_or(u32::MAX));
318 for value in values {
319 value.encode_into(&mut w);
320 }
321 w.0
322 }
323
324 pub fn decode_list(bytes: &[u8]) -> Result<Vec<Self>, RecordError> {
330 let mut r = Reader { bytes, at: 0 };
331 let count = r.u32()?;
332 let mut values = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
333 for _ in 0..count {
334 values.push(Self::decode_from(&mut r)?);
335 }
336 r.finish()?;
337 Ok(values)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::{Concept, Designation, PropertyValue, RecordError};
344 use concept_graph::ordinal::Ordinal;
345
346 #[test]
347 fn records_round_trip() {
348 let concept = Concept {
349 code: "123456789".to_owned(),
350 active: true,
351 effective_time: Some("20260101".to_owned()),
352 module: Some(Ordinal::new(7)),
353 };
354 assert_eq!(Concept::decode(&concept.encode()), Ok(concept));
355 let designation = Designation {
356 id: None,
357 term: "Synthetisch kind".to_owned(),
358 language: "nl".to_owned(),
359 use_ordinal: 2,
360 active: false,
361 };
362 assert_eq!(Designation::decode(&designation.encode()), Ok(designation));
363 let values = vec![
364 PropertyValue::Concept(Ordinal::new(3)),
365 PropertyValue::Code("x".to_owned()),
366 PropertyValue::String("s".to_owned()),
367 PropertyValue::Integer(-42),
368 PropertyValue::Boolean(true),
369 PropertyValue::Decimal("2.50".to_owned()),
370 PropertyValue::DateTime("2026-01-01".to_owned()),
371 ];
372 assert_eq!(
373 PropertyValue::decode_list(&PropertyValue::encode_list(&values)),
374 Ok(values)
375 );
376 }
377
378 #[test]
379 fn damaged_records_are_refused() {
380 let bytes = Concept {
381 code: "1".to_owned(),
382 active: true,
383 effective_time: None,
384 module: None,
385 }
386 .encode();
387 assert!(matches!(
388 Concept::decode(&bytes[..3]),
389 Err(RecordError::Truncated { .. })
390 ));
391 let mut trailing = bytes.clone();
392 trailing.push(0);
393 assert!(matches!(
394 Concept::decode(&trailing),
395 Err(RecordError::Trailing { remaining: 1 })
396 ));
397 assert!(matches!(
398 PropertyValue::decode_list(&[1, 0, 0, 0, 9]),
399 Err(RecordError::Tag { tag: 9 })
400 ));
401 assert!(matches!(
402 Designation::decode(&[0, 1, 0, 0, 0, 0xff]),
403 Err(RecordError::Utf8 { .. })
404 ));
405 }
406}