#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonEmptyStringList(Vec<String>);
impl NonEmptyStringList {
fn single(s: String) -> Self {
Self(vec![s])
}
#[must_use]
pub fn try_from_vec(v: Vec<String>) -> Option<Self> {
if v.is_empty() { None } else { Some(Self(v)) }
}
#[must_use]
pub fn as_slice(&self) -> &[String] {
&self.0
}
pub fn iter(&self) -> std::slice::Iter<'_, String> {
self.0.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
false
}
}
impl Default for NonEmptyStringList {
fn default() -> Self {
Self::single(String::new())
}
}
impl<'a> IntoIterator for &'a NonEmptyStringList {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ApFamily {
Ap203,
#[default]
Ap214,
Ap242,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Stage {
Is,
Dis,
Cd,
#[default]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SchemaId {
pub family: ApFamily,
pub edition: Option<u8>,
pub stage: Stage,
pub raw: Option<NonEmptyStringList>,
}
impl SchemaId {
#[must_use]
pub fn synthetic(family: ApFamily, edition: Option<u8>, stage: Stage) -> Self {
Self {
family,
edition,
stage,
raw: None,
}
}
#[must_use]
pub fn is_recognized(&self) -> bool {
self.family != ApFamily::Other
}
#[must_use]
pub fn raw(&self) -> Option<&NonEmptyStringList> {
self.raw.as_ref()
}
}
impl std::fmt::Display for SchemaId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fam = match self.family {
ApFamily::Ap203 => "AP203",
ApFamily::Ap214 => "AP214",
ApFamily::Ap242 => "AP242",
ApFamily::Other => return write!(f, "unrecognized schema"),
};
let stage = match self.stage {
Stage::Is => "IS",
Stage::Dis => "DIS",
Stage::Cd => "CD",
Stage::Unknown => "?",
};
match self.edition {
Some(e) => write!(f, "{fam} ed{e} ({stage})"),
None => write!(f, "{fam} ed? ({stage})"),
}
}
}
#[must_use]
pub fn identify_schema(file_schema: &[String]) -> SchemaId {
let Some(raw) = NonEmptyStringList::try_from_vec(file_schema.to_vec()) else {
return SchemaId::default();
};
let (family, edition, stage) = classify(file_schema);
SchemaId {
family,
edition,
stage,
raw: Some(raw),
}
}
fn classify(file_schema: &[String]) -> (ApFamily, Option<u8>, Stage) {
for s in file_schema {
if let Some(hit) = classify_one(&s.to_uppercase()) {
return hit;
}
}
(ApFamily::Other, None, Stage::Unknown)
}
fn classify_one(upper: &str) -> Option<(ApFamily, Option<u8>, Stage)> {
if let Some((part, version)) = parse_token(upper) {
match part {
214 => {
use std::cmp::Ordering;
let (ed, stage) = match version.cmp(&0) {
Ordering::Less => (None, Stage::Cd),
Ordering::Equal => (None, Stage::Dis),
Ordering::Greater => (u8::try_from(version).ok(), Stage::Is),
};
return Some((ApFamily::Ap214, ed, stage));
}
403 | 203 => {
let (ed, stage) = if version >= 1 {
(u8::try_from(version).ok(), Stage::Is)
} else {
(None, Stage::Unknown)
};
return Some((ApFamily::Ap203, ed, stage));
}
442 => {
let (ed, stage) = match version {
1 => (Some(1), Stage::Is),
2 => (Some(2), Stage::Dis),
3 => (Some(2), Stage::Is),
4 => (Some(3), Stage::Is),
_ => (None, Stage::Unknown),
};
return Some((ApFamily::Ap242, ed, stage));
}
_ => {} }
}
if upper.contains("AUTOMOTIVE_DESIGN_CC1") || upper.contains("AUTOMOTIVE_DESIGN_CC2") {
return Some((ApFamily::Ap214, None, Stage::Cd));
}
if upper.contains("AUTOMOTIVE_DESIGN") {
return Some((ApFamily::Ap214, None, Stage::Unknown));
}
if upper.contains("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING") {
return Some((ApFamily::Ap242, None, Stage::Is));
}
if upper.contains("AP203_CONFIGURATION_CONTROLLED") {
return Some((ApFamily::Ap203, None, Stage::Is));
}
if upper.contains("CONFIG_CONTROL_DESIGN")
|| upper.contains("CONFIGURATION_CONTROLLED_3D_DESIGN")
{
return Some((ApFamily::Ap203, Some(1), Stage::Is));
}
None
}
fn parse_token(upper: &str) -> Option<(i64, i64)> {
let start = upper.find('{')?;
let rel_end = upper[start..].find('}')?;
let inner = &upper[start + 1..start + rel_end];
let toks: Vec<&str> = inner.split_whitespace().collect();
let pos = toks.iter().position(|t| *t == "10303")?;
let part: i64 = toks.get(pos + 1)?.parse().ok()?;
let version: i64 = toks.get(pos + 2)?.parse().ok()?;
Some((part, version))
}
#[cfg(test)]
mod tests {
use super::*;
fn id(input: &str) -> SchemaId {
identify_schema(&[input.into()])
}
#[test]
fn ap214_is_editions() {
assert_eq!(
(
id("AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }").family,
id("AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }").edition,
id("AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }").stage
),
(ApFamily::Ap214, Some(1), Stage::Is),
);
let e2 = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 2 1 1 }");
assert_eq!(
(e2.family, e2.edition, e2.stage),
(ApFamily::Ap214, Some(2), Stage::Is)
);
let e3 = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }");
assert_eq!(
(e3.family, e3.edition, e3.stage),
(ApFamily::Ap214, Some(3), Stage::Is)
);
}
#[test]
fn ap214_cd_dis() {
let cc2 = id("AUTOMOTIVE_DESIGN_CC2 { 1 2 10303 214 -1 1 5 4 }");
assert_eq!(
(cc2.family, cc2.edition, cc2.stage),
(ApFamily::Ap214, None, Stage::Cd)
);
let cc1 = id("AUTOMOTIVE_DESIGN_CC1 { 1 2 10303 214 -1 1 3 2 }");
assert_eq!(
(cc1.family, cc1.edition, cc1.stage),
(ApFamily::Ap214, None, Stage::Cd)
);
let dis = id("AUTOMOTIVE_DESIGN { 1 2 10303 214 0 1 1 1 }");
assert_eq!(
(dis.family, dis.edition, dis.stage),
(ApFamily::Ap214, None, Stage::Dis)
);
}
#[test]
fn ap214_bare_name_and_whitespace_variants() {
let bare = id("AUTOMOTIVE_DESIGN");
assert_eq!(
(bare.family, bare.edition, bare.stage),
(ApFamily::Ap214, None, Stage::Unknown)
);
let cc2_bare = id("AUTOMOTIVE_DESIGN_CC2");
assert_eq!(
(cc2_bare.family, cc2_bare.stage),
(ApFamily::Ap214, Stage::Cd)
);
let nospace = id("AUTOMOTIVE_DESIGN {1 0 10303 214 3 1 1}");
assert_eq!(
(nospace.family, nospace.edition),
(ApFamily::Ap214, Some(3))
);
let seven = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 1 }");
assert_eq!((seven.family, seven.edition), (ApFamily::Ap214, Some(3)));
}
#[test]
fn ap242_version_to_edition_nonlinear() {
let cases = [
(1, Some(1), Stage::Is),
(2, Some(2), Stage::Dis),
(3, Some(2), Stage::Is),
(4, Some(3), Stage::Is),
];
for (v, ed, st) in cases {
let s = id(&format!(
"AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF {{ 1 0 10303 442 {v} 1 4 }}"
));
assert_eq!(
(s.family, s.edition, s.stage),
(ApFamily::Ap242, ed, st),
"version {v}"
);
}
}
#[test]
fn ap242_unknown_version_falls_back_by_name() {
let s = id("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 5 1 4 }");
assert_eq!(
(s.family, s.edition, s.stage),
(ApFamily::Ap242, None, Stage::Unknown)
);
}
#[test]
fn ap242_trailing_dot_and_no_space() {
let s = id("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF. {1 0 10303 442 1 1 4 }");
assert_eq!(
(s.family, s.edition, s.stage),
(ApFamily::Ap242, Some(1), Stage::Is)
);
}
#[test]
fn ap203_ed1_short_form() {
let s = id("CONFIG_CONTROL_DESIGN");
assert_eq!(
(s.family, s.edition, s.stage),
(ApFamily::Ap203, Some(1), Stage::Is)
);
}
#[test]
fn ap203_ed2_long_form_and_part_variant() {
let e2 = id(
"AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF { 1 0 10303 403 2 1 2 }",
);
assert_eq!(
(e2.family, e2.edition, e2.stage),
(ApFamily::Ap203, Some(2), Stage::Is)
);
let p203 = id(
"AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF { 1 0 10303 203 3 1 4 }",
);
assert_eq!(p203.family, ApFamily::Ap203);
}
#[test]
fn ap203_two_element_list_first_matches() {
let s = identify_schema(&[
"CONFIG_CONTROL_DESIGN".into(),
"SHAPE_APPEARANCE_LAYER_MIM".into(),
]);
assert_eq!(s.family, ApFamily::Ap203);
assert_eq!(s.raw().map(NonEmptyStringList::len), Some(2));
}
#[test]
fn unrecognized_is_other_with_raw() {
let s = id("SOMETHING_ELSE");
assert_eq!(s.family, ApFamily::Other);
assert!(!s.is_recognized());
assert_eq!(
s.raw().map(|r| r.as_slice()[0].as_str()),
Some("SOMETHING_ELSE")
);
}
#[test]
fn empty_list_falls_back_to_default() {
assert_eq!(identify_schema(&[]), SchemaId::default());
}
#[test]
fn raw_is_always_preserved_on_read() {
let s = id("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }");
assert_eq!(
s.raw().map(|r| r.as_slice()[0].as_str()),
Some("AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }")
);
}
#[test]
fn display_formats() {
assert_eq!(
id("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }")
.to_string(),
"AP242 ed2 (IS)"
);
assert_eq!(id("AUTOMOTIVE_DESIGN").to_string(), "AP214 ed? (?)");
assert_eq!(id("SOMETHING_ELSE").to_string(), "unrecognized schema");
}
}