1#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct NonEmptyStringList(Vec<String>);
14
15impl NonEmptyStringList {
16 fn single(s: String) -> Self {
19 Self(vec![s])
20 }
21
22 #[must_use]
25 pub fn try_from_vec(v: Vec<String>) -> Option<Self> {
26 if v.is_empty() { None } else { Some(Self(v)) }
27 }
28
29 #[must_use]
30 pub fn as_slice(&self) -> &[String] {
31 &self.0
32 }
33
34 pub fn iter(&self) -> std::slice::Iter<'_, String> {
36 self.0.iter()
37 }
38
39 #[must_use]
40 pub fn len(&self) -> usize {
41 self.0.len()
42 }
43
44 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 false
48 }
49}
50
51impl Default for NonEmptyStringList {
52 fn default() -> Self {
55 Self::single(String::new())
56 }
57}
58
59impl<'a> IntoIterator for &'a NonEmptyStringList {
60 type Item = &'a String;
61 type IntoIter = std::slice::Iter<'a, String>;
62
63 fn into_iter(self) -> Self::IntoIter {
64 self.0.iter()
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum ApFamily {
72 Ap203,
73 #[default]
74 Ap214,
75 Ap242,
76 Other,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum Stage {
86 Is,
88 Dis,
90 Cd,
92 #[default]
94 Unknown,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Default)]
112pub struct SchemaId {
113 pub family: ApFamily,
114 pub edition: Option<u8>,
117 pub stage: Stage,
118 pub raw: Option<NonEmptyStringList>,
119}
120
121impl SchemaId {
122 #[must_use]
124 pub fn synthetic(family: ApFamily, edition: Option<u8>, stage: Stage) -> Self {
125 Self {
126 family,
127 edition,
128 stage,
129 raw: None,
130 }
131 }
132
133 #[must_use]
135 pub fn is_recognized(&self) -> bool {
136 self.family != ApFamily::Other
137 }
138
139 #[must_use]
141 pub fn raw(&self) -> Option<&NonEmptyStringList> {
142 self.raw.as_ref()
143 }
144}
145
146impl std::fmt::Display for SchemaId {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 let fam = match self.family {
149 ApFamily::Ap203 => "AP203",
150 ApFamily::Ap214 => "AP214",
151 ApFamily::Ap242 => "AP242",
152 ApFamily::Other => return write!(f, "unrecognized schema"),
153 };
154 let stage = match self.stage {
155 Stage::Is => "IS",
156 Stage::Dis => "DIS",
157 Stage::Cd => "CD",
158 Stage::Unknown => "?",
159 };
160 match self.edition {
161 Some(e) => write!(f, "{fam} ed{e} ({stage})"),
162 None => write!(f, "{fam} ed? ({stage})"),
163 }
164 }
165}
166
167#[must_use]
173pub fn identify_schema(file_schema: &[String]) -> SchemaId {
174 let Some(raw) = NonEmptyStringList::try_from_vec(file_schema.to_vec()) else {
175 return SchemaId::default();
177 };
178 let (family, edition, stage) = classify(file_schema);
179 SchemaId {
180 family,
181 edition,
182 stage,
183 raw: Some(raw),
184 }
185}
186
187fn classify(file_schema: &[String]) -> (ApFamily, Option<u8>, Stage) {
190 for s in file_schema {
191 if let Some(hit) = classify_one(&s.to_uppercase()) {
192 return hit;
193 }
194 }
195 (ApFamily::Other, None, Stage::Unknown)
196}
197
198fn classify_one(upper: &str) -> Option<(ApFamily, Option<u8>, Stage)> {
202 if let Some((part, version)) = parse_token(upper) {
203 match part {
204 214 => {
205 use std::cmp::Ordering;
206 let (ed, stage) = match version.cmp(&0) {
207 Ordering::Less => (None, Stage::Cd),
208 Ordering::Equal => (None, Stage::Dis),
209 Ordering::Greater => (u8::try_from(version).ok(), Stage::Is),
210 };
211 return Some((ApFamily::Ap214, ed, stage));
212 }
213 403 | 203 => {
214 let (ed, stage) = if version >= 1 {
215 (u8::try_from(version).ok(), Stage::Is)
216 } else {
217 (None, Stage::Unknown)
218 };
219 return Some((ApFamily::Ap203, ed, stage));
220 }
221 442 => {
222 let (ed, stage) = match version {
226 1 => (Some(1), Stage::Is),
227 2 => (Some(2), Stage::Dis),
228 3 => (Some(2), Stage::Is),
229 4 => (Some(3), Stage::Is),
230 _ => (None, Stage::Unknown),
231 };
232 return Some((ApFamily::Ap242, ed, stage));
233 }
234 _ => {} }
236 }
237 if upper.contains("AUTOMOTIVE_DESIGN_CC1") || upper.contains("AUTOMOTIVE_DESIGN_CC2") {
239 return Some((ApFamily::Ap214, None, Stage::Cd));
240 }
241 if upper.contains("AUTOMOTIVE_DESIGN") {
242 return Some((ApFamily::Ap214, None, Stage::Unknown));
243 }
244 if upper.contains("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING") {
245 return Some((ApFamily::Ap242, None, Stage::Is));
246 }
247 if upper.contains("AP203_CONFIGURATION_CONTROLLED") {
248 return Some((ApFamily::Ap203, None, Stage::Is));
250 }
251 if upper.contains("CONFIG_CONTROL_DESIGN")
252 || upper.contains("CONFIGURATION_CONTROLLED_3D_DESIGN")
253 {
254 return Some((ApFamily::Ap203, Some(1), Stage::Is));
256 }
257 None
258}
259
260fn parse_token(upper: &str) -> Option<(i64, i64)> {
267 let start = upper.find('{')?;
268 let rel_end = upper[start..].find('}')?;
269 let inner = &upper[start + 1..start + rel_end];
270 let toks: Vec<&str> = inner.split_whitespace().collect();
271 let pos = toks.iter().position(|t| *t == "10303")?;
272 let part: i64 = toks.get(pos + 1)?.parse().ok()?;
273 let version: i64 = toks.get(pos + 2)?.parse().ok()?;
274 Some((part, version))
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn id(input: &str) -> SchemaId {
282 identify_schema(&[input.into()])
283 }
284
285 #[test]
287 fn ap214_is_editions() {
288 assert_eq!(
289 (
290 id("AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }").family,
291 id("AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }").edition,
292 id("AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }").stage
293 ),
294 (ApFamily::Ap214, Some(1), Stage::Is),
295 );
296 let e2 = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 2 1 1 }");
297 assert_eq!(
298 (e2.family, e2.edition, e2.stage),
299 (ApFamily::Ap214, Some(2), Stage::Is)
300 );
301 let e3 = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }");
302 assert_eq!(
303 (e3.family, e3.edition, e3.stage),
304 (ApFamily::Ap214, Some(3), Stage::Is)
305 );
306 }
307
308 #[test]
309 fn ap214_cd_dis() {
310 let cc2 = id("AUTOMOTIVE_DESIGN_CC2 { 1 2 10303 214 -1 1 5 4 }");
311 assert_eq!(
312 (cc2.family, cc2.edition, cc2.stage),
313 (ApFamily::Ap214, None, Stage::Cd)
314 );
315 let cc1 = id("AUTOMOTIVE_DESIGN_CC1 { 1 2 10303 214 -1 1 3 2 }");
316 assert_eq!(
317 (cc1.family, cc1.edition, cc1.stage),
318 (ApFamily::Ap214, None, Stage::Cd)
319 );
320 let dis = id("AUTOMOTIVE_DESIGN { 1 2 10303 214 0 1 1 1 }");
321 assert_eq!(
322 (dis.family, dis.edition, dis.stage),
323 (ApFamily::Ap214, None, Stage::Dis)
324 );
325 }
326
327 #[test]
328 fn ap214_bare_name_and_whitespace_variants() {
329 let bare = id("AUTOMOTIVE_DESIGN");
330 assert_eq!(
331 (bare.family, bare.edition, bare.stage),
332 (ApFamily::Ap214, None, Stage::Unknown)
333 );
334 let cc2_bare = id("AUTOMOTIVE_DESIGN_CC2");
335 assert_eq!(
336 (cc2_bare.family, cc2_bare.stage),
337 (ApFamily::Ap214, Stage::Cd)
338 );
339 let nospace = id("AUTOMOTIVE_DESIGN {1 0 10303 214 3 1 1}");
341 assert_eq!(
342 (nospace.family, nospace.edition),
343 (ApFamily::Ap214, Some(3))
344 );
345 let seven = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 1 }");
346 assert_eq!((seven.family, seven.edition), (ApFamily::Ap214, Some(3)));
347 }
348
349 #[test]
351 fn ap242_version_to_edition_nonlinear() {
352 let cases = [
353 (1, Some(1), Stage::Is),
354 (2, Some(2), Stage::Dis),
355 (3, Some(2), Stage::Is),
356 (4, Some(3), Stage::Is),
357 ];
358 for (v, ed, st) in cases {
359 let s = id(&format!(
360 "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF {{ 1 0 10303 442 {v} 1 4 }}"
361 ));
362 assert_eq!(
363 (s.family, s.edition, s.stage),
364 (ApFamily::Ap242, ed, st),
365 "version {v}"
366 );
367 }
368 }
369
370 #[test]
371 fn ap242_unknown_version_falls_back_by_name() {
372 let s = id("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 5 1 4 }");
374 assert_eq!(
375 (s.family, s.edition, s.stage),
376 (ApFamily::Ap242, None, Stage::Unknown)
377 );
378 }
379
380 #[test]
381 fn ap242_trailing_dot_and_no_space() {
382 let s = id("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF. {1 0 10303 442 1 1 4 }");
384 assert_eq!(
385 (s.family, s.edition, s.stage),
386 (ApFamily::Ap242, Some(1), Stage::Is)
387 );
388 }
389
390 #[test]
392 fn ap203_ed1_short_form() {
393 let s = id("CONFIG_CONTROL_DESIGN");
394 assert_eq!(
395 (s.family, s.edition, s.stage),
396 (ApFamily::Ap203, Some(1), Stage::Is)
397 );
398 }
399
400 #[test]
401 fn ap203_ed2_long_form_and_part_variant() {
402 let e2 = id(
403 "AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF { 1 0 10303 403 2 1 2 }",
404 );
405 assert_eq!(
406 (e2.family, e2.edition, e2.stage),
407 (ApFamily::Ap203, Some(2), Stage::Is)
408 );
409 let p203 = id(
411 "AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF { 1 0 10303 203 3 1 4 }",
412 );
413 assert_eq!(p203.family, ApFamily::Ap203);
414 }
415
416 #[test]
417 fn ap203_two_element_list_first_matches() {
418 let s = identify_schema(&[
419 "CONFIG_CONTROL_DESIGN".into(),
420 "SHAPE_APPEARANCE_LAYER_MIM".into(),
421 ]);
422 assert_eq!(s.family, ApFamily::Ap203);
423 assert_eq!(s.raw().map(NonEmptyStringList::len), Some(2));
424 }
425
426 #[test]
428 fn unrecognized_is_other_with_raw() {
429 let s = id("SOMETHING_ELSE");
430 assert_eq!(s.family, ApFamily::Other);
431 assert!(!s.is_recognized());
432 assert_eq!(
433 s.raw().map(|r| r.as_slice()[0].as_str()),
434 Some("SOMETHING_ELSE")
435 );
436 }
437
438 #[test]
439 fn empty_list_falls_back_to_default() {
440 assert_eq!(identify_schema(&[]), SchemaId::default());
442 }
443
444 #[test]
445 fn raw_is_always_preserved_on_read() {
446 let s = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }");
447 assert_eq!(
448 s.raw().map(|r| r.as_slice()[0].as_str()),
449 Some("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }")
450 );
451 }
452
453 #[test]
454 fn display_formats() {
455 assert_eq!(
456 id("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }")
457 .to_string(),
458 "AP242 ed2 (IS)"
459 );
460 assert_eq!(id("AUTOMOTIVE_DESIGN").to_string(), "AP214 ed? (?)");
461 assert_eq!(id("SOMETHING_ELSE").to_string(), "unrecognized schema");
462 }
463}