1use core::fmt;
4
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8pub struct CustomBodyId {
9 pub catalog: String,
11 pub designation: String,
13}
14
15impl CustomBodyId {
16 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 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#[derive(Clone, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub enum CustomDefinitionValidationError {
63 Blank {
65 subject: &'static str,
67 field: String,
69 },
70 Padded {
72 subject: &'static str,
74 field: String,
76 },
77 ContainsSeparator {
79 subject: &'static str,
81 field: String,
83 separator: char,
85 },
86 DuplicateAlias {
88 subject: &'static str,
90 alias: String,
92 },
93 ReservedLabel {
95 subject: &'static str,
97 field: String,
99 label: String,
101 },
102 NonFinite {
104 subject: &'static str,
106 field: String,
108 },
109 IncompletePair {
111 subject: &'static str,
113 first: String,
115 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 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}