Skip to main content

pleiades_types/
custom_bodies.rs

1//! Custom body identifiers: [`CustomBodyId`] and [`CustomDefinitionValidationError`].
2
3use core::fmt;
4
5/// A structured identifier for a custom body.
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8pub struct CustomBodyId {
9    /// A coarse namespace for the body source, such as `asteroid` or `hypothetical`.
10    pub catalog: String,
11    /// The designation within the namespace.
12    pub designation: String,
13}
14
15impl CustomBodyId {
16    /// Creates a new custom body identifier.
17    pub fn new(catalog: impl Into<String>, designation: impl Into<String>) -> Self {
18        Self {
19            catalog: catalog.into(),
20            designation: designation.into(),
21        }
22    }
23
24    /// Validates the custom body identifier fields.
25    ///
26    /// The catalog and designation must both be non-empty, must not contain
27    /// leading or trailing whitespace, and must not contain the `:` separator
28    /// used by the display representation.
29    pub fn validate(&self) -> Result<(), CustomDefinitionValidationError> {
30        validate_canonical_text("custom body id", "catalog", &self.catalog)?;
31        validate_canonical_text("custom body id", "designation", &self.designation)?;
32
33        if self.catalog.contains(':') {
34            return Err(CustomDefinitionValidationError::contains_separator(
35                "custom body id",
36                "catalog",
37                ':',
38            ));
39        }
40
41        if self.designation.contains(':') {
42            return Err(CustomDefinitionValidationError::contains_separator(
43                "custom body id",
44                "designation",
45                ':',
46            ));
47        }
48
49        Ok(())
50    }
51}
52
53impl fmt::Display for CustomBodyId {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{}:{}", self.catalog, self.designation)
56    }
57}
58
59/// Validation failure for a custom body, house system, or ayanamsa definition.
60#[derive(Clone, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub enum CustomDefinitionValidationError {
63    /// A required field was blank.
64    Blank {
65        /// The type of custom definition being validated.
66        subject: &'static str,
67        /// The invalid field name.
68        field: String,
69    },
70    /// A field carried leading or trailing whitespace.
71    Padded {
72        /// The type of custom definition being validated.
73        subject: &'static str,
74        /// The invalid field name.
75        field: String,
76    },
77    /// A field contained the `:` separator used by custom body identifiers.
78    ContainsSeparator {
79        /// The type of custom definition being validated.
80        subject: &'static str,
81        /// The invalid field name.
82        field: String,
83        /// The separator that was rejected.
84        separator: char,
85    },
86    /// A list of aliases contained a duplicate value.
87    DuplicateAlias {
88        /// The type of custom definition being validated.
89        subject: &'static str,
90        /// The duplicated alias.
91        alias: String,
92    },
93    /// A label collides with a built-in descriptor or alias.
94    ReservedLabel {
95        /// The type of custom definition being validated.
96        subject: &'static str,
97        /// The invalid field name.
98        field: String,
99        /// The reserved built-in label.
100        label: String,
101    },
102    /// A numeric field was not finite.
103    NonFinite {
104        /// The type of custom definition being validated.
105        subject: &'static str,
106        /// The invalid field name.
107        field: String,
108    },
109    /// Two optional fields must be supplied together.
110    IncompletePair {
111        /// The type of custom definition being validated.
112        subject: &'static str,
113        /// The first required field.
114        first: String,
115        /// The second required field.
116        second: String,
117    },
118}
119
120impl CustomDefinitionValidationError {
121    pub(crate) fn blank(subject: &'static str, field: impl Into<String>) -> Self {
122        Self::Blank {
123            subject,
124            field: field.into(),
125        }
126    }
127
128    pub(crate) fn padded(subject: &'static str, field: impl Into<String>) -> Self {
129        Self::Padded {
130            subject,
131            field: field.into(),
132        }
133    }
134
135    pub(crate) fn contains_separator(
136        subject: &'static str,
137        field: impl Into<String>,
138        separator: char,
139    ) -> Self {
140        Self::ContainsSeparator {
141            subject,
142            field: field.into(),
143            separator,
144        }
145    }
146
147    pub(crate) fn duplicate_alias(subject: &'static str, alias: impl Into<String>) -> Self {
148        Self::DuplicateAlias {
149            subject,
150            alias: alias.into(),
151        }
152    }
153
154    pub(crate) fn reserved_label(
155        subject: &'static str,
156        field: impl Into<String>,
157        label: impl Into<String>,
158    ) -> Self {
159        Self::ReservedLabel {
160            subject,
161            field: field.into(),
162            label: label.into(),
163        }
164    }
165
166    pub(crate) fn non_finite(subject: &'static str, field: impl Into<String>) -> Self {
167        Self::NonFinite {
168            subject,
169            field: field.into(),
170        }
171    }
172
173    pub(crate) fn incomplete_pair(
174        subject: &'static str,
175        first: impl Into<String>,
176        second: impl Into<String>,
177    ) -> Self {
178        Self::IncompletePair {
179            subject,
180            first: first.into(),
181            second: second.into(),
182        }
183    }
184
185    /// Returns a compact one-line rendering of the validation failure.
186    pub fn summary_line(&self) -> String {
187        match self {
188            Self::Blank { subject, field } => format!("{subject} {field} must not be blank"),
189            Self::Padded { subject, field } => {
190                format!("{subject} {field} must not have leading or trailing whitespace")
191            }
192            Self::ContainsSeparator {
193                subject,
194                field,
195                separator,
196            } => format!("{subject} {field} must not contain '{separator}'"),
197            Self::DuplicateAlias { subject, alias } => {
198                format!("{subject} aliases must be unique: duplicate {alias}")
199            }
200            Self::ReservedLabel {
201                subject,
202                field,
203                label,
204            } => {
205                format!("{subject} {field} must not match a built-in label: {label}")
206            }
207            Self::NonFinite { subject, field } => format!("{subject} {field} must be finite"),
208            Self::IncompletePair {
209                subject,
210                first,
211                second,
212            } => format!("{subject} requires both {first} and {second} when one is present"),
213        }
214    }
215}
216
217impl fmt::Display for CustomDefinitionValidationError {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.write_str(&self.summary_line())
220    }
221}
222
223impl std::error::Error for CustomDefinitionValidationError {}
224
225pub(crate) fn validate_canonical_text(
226    subject: &'static str,
227    field: impl Into<String>,
228    value: &str,
229) -> Result<(), CustomDefinitionValidationError> {
230    let field = field.into();
231    let trimmed = value.trim();
232
233    if trimmed.is_empty() {
234        return Err(CustomDefinitionValidationError::blank(subject, field));
235    }
236
237    if trimmed != value {
238        return Err(CustomDefinitionValidationError::padded(subject, field));
239    }
240
241    Ok(())
242}