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