1use std::{error::Error, fmt, str::FromStr};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ValueError {
8 kind: ValueErrorKind,
9 value: String,
10}
11
12impl ValueError {
13 fn new(kind: ValueErrorKind, value: impl Into<String>) -> Self {
14 Self {
15 kind,
16 value: value.into(),
17 }
18 }
19
20 #[must_use]
22 pub const fn kind(&self) -> ValueErrorKind {
23 self.kind
24 }
25}
26
27impl fmt::Display for ValueError {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(formatter, "invalid {:?} value {:?}", self.kind, self.value)
30 }
31}
32
33impl Error for ValueError {}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum ValueErrorKind {
38 Guid,
40 Language,
42 Rational,
44 Decimal,
46 PositiveInteger,
48 EmptyReference,
50 CreationDate,
52 Uri,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
58pub struct Guid(String);
59
60impl Guid {
61 #[must_use]
63 pub fn as_str(&self) -> &str {
64 &self.0
65 }
66}
67
68impl FromStr for Guid {
69 type Err = ValueError;
70
71 fn from_str(value: &str) -> Result<Self, Self::Err> {
72 const HYPHENS: [usize; 4] = [8, 13, 18, 23];
73 let valid = value.len() == 36
74 && value.bytes().enumerate().all(|(index, byte)| {
75 if HYPHENS.contains(&index) {
76 byte == b'-'
77 } else {
78 byte.is_ascii_hexdigit()
79 }
80 });
81 valid
82 .then(|| Self(value.to_owned()))
83 .ok_or_else(|| ValueError::new(ValueErrorKind::Guid, value))
84 }
85}
86
87impl fmt::Display for Guid {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 formatter.write_str(&self.0)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95pub struct Language(String);
96
97impl Language {
98 #[must_use]
99 pub fn as_str(&self) -> &str {
100 &self.0
101 }
102}
103
104impl FromStr for Language {
105 type Err = ValueError;
106
107 fn from_str(value: &str) -> Result<Self, Self::Err> {
108 let value = collapse_whitespace(value);
109 is_language(&value)
110 .then(|| Self(value.clone()))
111 .ok_or_else(|| ValueError::new(ValueErrorKind::Language, value))
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
117pub struct DateTime(String);
118
119impl DateTime {
120 #[must_use]
121 pub fn as_str(&self) -> &str {
122 &self.0
123 }
124}
125
126impl FromStr for DateTime {
127 type Err = ValueError;
128
129 fn from_str(value: &str) -> Result<Self, Self::Err> {
130 let value = collapse_whitespace(value);
131 is_xs_datetime(&value)
132 .then(|| Self(value.clone()))
133 .ok_or_else(|| ValueError::new(ValueErrorKind::CreationDate, value))
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash)]
143pub struct AnyUri(String);
144
145impl AnyUri {
146 #[must_use]
147 pub fn as_str(&self) -> &str {
148 &self.0
149 }
150}
151
152impl FromStr for AnyUri {
153 type Err = ValueError;
154
155 fn from_str(value: &str) -> Result<Self, Self::Err> {
156 let value = collapse_whitespace(value);
157 value
158 .chars()
159 .all(is_xml_10_character)
160 .then(|| Self(value.clone()))
161 .ok_or_else(|| ValueError::new(ValueErrorKind::Uri, value))
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Hash)]
167pub struct MultiLanguageText {
168 language: Language,
169 text: String,
170}
171
172impl MultiLanguageText {
173 pub fn new(language: impl Into<String>, text: impl Into<String>) -> Result<Self, ValueError> {
175 let language = language.into().parse()?;
176 Ok(Self {
177 language,
178 text: text.into(),
179 })
180 }
181
182 #[must_use]
184 pub fn language(&self) -> &str {
185 self.language.as_str()
186 }
187
188 #[must_use]
190 pub fn text(&self) -> &str {
191 &self.text
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Hash)]
197pub struct Reference {
198 guid: Option<Guid>,
199 uri: Option<AnyUri>,
200}
201
202impl Reference {
203 #[must_use]
205 pub const fn new(guid: Option<Guid>, uri: Option<AnyUri>) -> Self {
206 Self { guid, uri }
207 }
208
209 pub fn identified(guid: Option<Guid>, uri: Option<AnyUri>) -> Result<Self, ValueError> {
211 if guid.is_none() && uri.is_none() {
212 return Err(ValueError::new(ValueErrorKind::EmptyReference, ""));
213 }
214 Ok(Self::new(guid, uri))
215 }
216
217 #[must_use]
218 pub const fn is_empty(&self) -> bool {
219 self.guid.is_none() && self.uri.is_none()
220 }
221
222 #[must_use]
224 pub const fn guid(&self) -> Option<&Guid> {
225 self.guid.as_ref()
226 }
227
228 #[must_use]
230 pub fn uri(&self) -> Option<&str> {
231 self.uri.as_ref().map(AnyUri::as_str)
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct Concept {
241 guid: Guid,
242 date_of_creation: DateTime,
243 names: Vec<MultiLanguageText>,
244 definition: MultiLanguageText,
245 references: Vec<Reference>,
246}
247
248impl Concept {
249 #[must_use]
251 pub fn new(
252 guid: Guid,
253 date_of_creation: DateTime,
254 first_name: MultiLanguageText,
255 definition: MultiLanguageText,
256 ) -> Self {
257 Self {
258 guid,
259 date_of_creation,
260 names: vec![first_name],
261 definition,
262 references: Vec::new(),
263 }
264 }
265
266 #[must_use]
267 pub const fn guid(&self) -> &Guid {
268 &self.guid
269 }
270
271 #[must_use]
272 pub fn date_of_creation(&self) -> &str {
273 self.date_of_creation.as_str()
274 }
275
276 #[must_use]
277 pub fn names(&self) -> &[MultiLanguageText] {
278 &self.names
279 }
280
281 #[must_use]
282 pub const fn definition(&self) -> &MultiLanguageText {
283 &self.definition
284 }
285
286 #[must_use]
287 pub fn references(&self) -> &[Reference] {
288 &self.references
289 }
290
291 pub fn add_name(&mut self, name: MultiLanguageText) {
292 self.names.push(name);
293 }
294
295 pub fn set_definition(&mut self, definition: MultiLanguageText) {
296 self.definition = definition;
297 }
298
299 pub fn add_reference(&mut self, reference: Reference) {
300 self.references.push(reference);
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Hash)]
306pub enum DataTypeName {
307 Boolean,
308 Integer,
309 Rational,
310 Real,
311 Complex,
312 String,
313 DateTime,
314 Other(String),
316}
317
318impl DataTypeName {
319 #[must_use]
321 pub fn as_str(&self) -> &str {
322 match self {
323 Self::Boolean => "BOOLEAN",
324 Self::Integer => "INTEGER",
325 Self::Rational => "RATIONAL",
326 Self::Real => "REAL",
327 Self::Complex => "COMPLEX",
328 Self::String => "STRING",
329 Self::DateTime => "DATETIME",
330 Self::Other(value) => value,
331 }
332 }
333}
334
335impl From<&str> for DataTypeName {
336 fn from(value: &str) -> Self {
337 match value {
338 "BOOLEAN" => Self::Boolean,
339 "INTEGER" => Self::Integer,
340 "RATIONAL" => Self::Rational,
341 "REAL" => Self::Real,
342 "COMPLEX" => Self::Complex,
343 "STRING" => Self::String,
344 "DATETIME" => Self::DateTime,
345 other => Self::Other(other.to_owned()),
346 }
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Hash)]
352pub enum Scale {
353 Linear,
354 Logarithmic,
355 Other(String),
356}
357
358impl From<&str> for Scale {
359 fn from(value: &str) -> Self {
360 match value {
361 "LINEAR" => Self::Linear,
362 "LOGARITHMIC" => Self::Logarithmic,
363 other => Self::Other(other.to_owned()),
364 }
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Hash)]
370pub enum Base {
371 One,
372 Two,
373 E,
374 Pi,
375 Ten,
376 Other(String),
377}
378
379impl From<&str> for Base {
380 fn from(value: &str) -> Self {
381 match value {
382 "ONE" => Self::One,
383 "TWO" => Self::Two,
384 "E" => Self::E,
385 "PI" => Self::Pi,
386 "TEN" => Self::Ten,
387 other => Self::Other(other.to_owned()),
388 }
389 }
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Hash)]
394pub struct Decimal(String);
395
396impl Decimal {
397 #[must_use]
398 pub fn as_str(&self) -> &str {
399 &self.0
400 }
401}
402
403impl FromStr for Decimal {
404 type Err = ValueError;
405
406 fn from_str(value: &str) -> Result<Self, Self::Err> {
407 let value = collapse_whitespace(value);
408 let unsigned = value.strip_prefix(['+', '-']).unwrap_or(&value);
409 let mut parts = unsigned.split('.');
410 let integer = parts.next().unwrap_or_default();
411 let fraction = parts.next();
412 let valid_integer = integer.bytes().all(|byte| byte.is_ascii_digit());
413 let valid_fraction =
414 fraction.is_none_or(|part| part.bytes().all(|byte| byte.is_ascii_digit()));
415 let has_digit = !integer.is_empty() || fraction.is_some_and(|part| !part.is_empty());
416 let valid = valid_integer && valid_fraction && has_digit && parts.next().is_none();
417 valid
418 .then(|| Self(value.clone()))
419 .ok_or_else(|| ValueError::new(ValueErrorKind::Decimal, value))
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Hash)]
425pub struct PositiveInteger(String);
426
427impl PositiveInteger {
428 #[must_use]
429 pub fn as_str(&self) -> &str {
430 &self.0
431 }
432}
433
434impl FromStr for PositiveInteger {
435 type Err = ValueError;
436
437 fn from_str(value: &str) -> Result<Self, Self::Err> {
438 let value = collapse_whitespace(value);
439 let digits = value.strip_prefix('+').unwrap_or(&value);
440 let valid = !digits.is_empty()
441 && digits.bytes().all(|byte| byte.is_ascii_digit())
442 && digits.bytes().any(|byte| byte != b'0');
443 valid
444 .then(|| Self(value.clone()))
445 .ok_or_else(|| ValueError::new(ValueErrorKind::PositiveInteger, value))
446 }
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Hash)]
451pub struct Rational(String);
452
453impl Rational {
454 #[must_use]
455 pub fn as_str(&self) -> &str {
456 &self.0
457 }
458}
459
460impl FromStr for Rational {
461 type Err = ValueError;
462
463 fn from_str(value: &str) -> Result<Self, Self::Err> {
464 let unsigned = value.strip_prefix(['+', '-']).unwrap_or(value);
465 let mut parts = unsigned.split('/');
466 let numerator = parts.next().unwrap_or_default();
467 let denominator = parts.next();
468 let valid_numerator =
469 !numerator.is_empty() && numerator.bytes().all(|b| b.is_ascii_digit());
470 let valid_denominator = denominator.is_none_or(|part| {
471 part.bytes()
472 .next()
473 .is_some_and(|first| matches!(first, b'1'..=b'9'))
474 && part.bytes().all(|b| b.is_ascii_digit())
475 });
476 let valid = valid_numerator && valid_denominator && parts.next().is_none();
477 valid
478 .then(|| Self(value.to_owned()))
479 .ok_or_else(|| ValueError::new(ValueErrorKind::Rational, value))
480 }
481}
482
483fn collapse_whitespace(value: &str) -> String {
484 let mut output = String::with_capacity(value.len());
485 let mut pending_space = false;
486 for character in value.chars() {
487 if matches!(character, ' ' | '\t' | '\r' | '\n') {
488 pending_space = !output.is_empty();
489 } else {
490 if pending_space {
491 output.push(' ');
492 pending_space = false;
493 }
494 output.push(character);
495 }
496 }
497 output
498}
499
500fn is_xml_10_character(value: char) -> bool {
501 matches!(value, '\u{9}' | '\u{A}' | '\u{D}')
502 || ('\u{20}'..='\u{D7FF}').contains(&value)
503 || ('\u{E000}'..='\u{FFFD}').contains(&value)
504 || ('\u{10000}'..='\u{10FFFF}').contains(&value)
505}
506
507fn is_language(value: &str) -> bool {
508 let mut parts = value.split('-');
509 let Some(first) = parts.next() else {
510 return false;
511 };
512 let first_valid =
513 (1..=8).contains(&first.len()) && first.bytes().all(|b| b.is_ascii_alphabetic());
514 first_valid
515 && parts.all(|part| {
516 (1..=8).contains(&part.len()) && part.bytes().all(|b| b.is_ascii_alphanumeric())
517 })
518}
519
520fn is_xs_datetime(value: &str) -> bool {
521 let Some((date, time_and_zone)) = value.split_once('T') else {
522 return false;
523 };
524 if time_and_zone.contains('T') || !valid_xs_date(date) {
525 return false;
526 }
527 let (time, zone) = split_timezone(time_and_zone);
528 valid_xs_time(time) && zone.is_none_or(valid_timezone)
529}
530
531fn valid_xs_date(value: &str) -> bool {
532 let unsigned = value.strip_prefix('-').unwrap_or(value);
533 let mut parts = unsigned.split('-');
534 let (Some(year), Some(month), Some(day)) = (parts.next(), parts.next(), parts.next()) else {
535 return false;
536 };
537 if parts.next().is_some()
538 || year.len() < 4
539 || (year.len() > 4 && year.starts_with('0'))
540 || !year.bytes().all(|b| b.is_ascii_digit())
541 || year.bytes().all(|b| b == b'0')
542 || month.len() != 2
543 || day.len() != 2
544 {
545 return false;
546 }
547 let (Ok(month), Ok(day)) = (month.parse::<u8>(), day.parse::<u8>()) else {
548 return false;
549 };
550 let year_mod_400 = year.bytes().fold(0_u16, |value, digit| {
551 (value * 10 + u16::from(digit - b'0')) % 400
552 });
553 let leap = year_mod_400 % 4 == 0 && (year_mod_400 % 100 != 0 || year_mod_400 == 0);
554 let max_day = match month {
555 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
556 4 | 6 | 9 | 11 => 30,
557 2 if leap => 29,
558 2 => 28,
559 _ => return false,
560 };
561 (1..=max_day).contains(&day)
562}
563
564fn valid_xs_time(value: &str) -> bool {
565 let mut parts = value.split(':');
566 let (Some(hour), Some(minute), Some(second)) = (parts.next(), parts.next(), parts.next())
567 else {
568 return false;
569 };
570 if parts.next().is_some() || hour.len() != 2 || minute.len() != 2 {
571 return false;
572 }
573 let (Ok(hour), Ok(minute)) = (hour.parse::<u8>(), minute.parse::<u8>()) else {
574 return false;
575 };
576 let mut second_parts = second.split('.');
577 let whole = second_parts.next().unwrap_or_default();
578 let fraction = second_parts.next();
579 let valid_second = whole.len() == 2
580 && whole.bytes().all(|b| b.is_ascii_digit())
581 && whole.parse::<u8>().is_ok_and(|v| v <= 59)
582 && fraction.is_none_or(|v| !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()))
583 && second_parts.next().is_none();
584 valid_second
585 && minute <= 59
586 && (hour <= 23
587 || (hour == 24
588 && minute == 0
589 && whole == "00"
590 && fraction.is_none_or(|value| value.bytes().all(|byte| byte == b'0'))))
591}
592
593fn split_timezone(value: &str) -> (&str, Option<&str>) {
594 if let Some(time) = value.strip_suffix('Z') {
595 return (time, Some("Z"));
596 }
597 if value.len() >= 6 {
598 let boundary = value.len() - 6;
599 if matches!(value.as_bytes()[boundary], b'+' | b'-') {
600 return (&value[..boundary], Some(&value[boundary..]));
601 }
602 }
603 (value, None)
604}
605
606fn valid_timezone(value: &str) -> bool {
607 if value == "Z" {
608 return true;
609 }
610 let bytes = value.as_bytes();
611 if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' {
612 return false;
613 }
614 let (Ok(hour), Ok(minute)) = (value[1..3].parse::<u8>(), value[4..6].parse::<u8>()) else {
615 return false;
616 };
617 hour <= 14 && minute <= 59 && (hour != 14 || minute == 0)
618}