use std::num::NonZeroU32;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::ids::{
BodyId, CurveId, EdgeId, FaceId, OccurrenceId, PmiId, PointId, ProductDefinitionId, VertexId,
};
use crate::transform::Transform;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum PmiTarget {
Body {
body: BodyId,
},
Face {
face: FaceId,
},
Edge {
edge: EdgeId,
},
Vertex {
vertex: VertexId,
},
Point {
point: PointId,
},
Curve {
curve: CurveId,
},
Product {
product: ProductDefinitionId,
},
Occurrence {
occurrence: OccurrenceId,
},
ShapeAspect {
#[serde(deserialize_with = "deserialize_source_id")]
source_id: cadmpeg_core::text::NonBlankString,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PmiValue {
#[serde(deserialize_with = "deserialize_pmi_value")]
pub value: crate::scalar::FiniteReal,
pub quantity: PmiQuantity,
}
crate::units::named_field!(deserialize_pmi_value, crate::scalar::FiniteReal, "value");
impl PmiValue {
#[must_use]
pub const fn from_parts(value: crate::scalar::FiniteReal, quantity: PmiQuantity) -> Self {
Self { value, quantity }
}
pub fn new(value: f64, quantity: PmiQuantity) -> Option<Self> {
Some(Self::from_parts(
crate::scalar::FiniteReal::new(value)?,
quantity,
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(transparent)]
pub struct PmiMagnitude(PmiValue);
impl PmiMagnitude {
#[must_use]
pub fn from_parts(value: crate::scalar::NonNegativeReal, quantity: PmiQuantity) -> Self {
Self(PmiValue::from_parts(value.into(), quantity))
}
pub fn new(value: PmiValue) -> Option<Self> {
crate::scalar::NonNegativeReal::try_from(value.value)
.ok()
.map(|magnitude| Self::from_parts(magnitude, value.quantity))
}
pub const fn get(self) -> PmiValue {
self.0
}
}
impl<'de> Deserialize<'de> for PmiMagnitude {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self::new(crate::units::deserialize_named(deserializer, "magnitude")?)
.ok_or_else(|| serde::de::Error::custom("magnitude must be nonnegative"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum PmiQuantity {
Length,
Angle,
Ratio,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum DimensionKind {
Size,
Location,
Angular,
Diameter,
Radius,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum DatumTargetForm {
Point,
Line,
Rectangle,
Circle,
CircularCurve,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum GeometricToleranceKind {
Straightness,
Flatness,
Roundness,
Cylindricity,
Coaxiality,
LineProfile,
SurfaceProfile,
Angularity,
Perpendicularity,
Parallelism,
Position,
Concentricity,
Symmetry,
CircularRunout,
TotalRunout,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DatumReference {
pub datum: PmiId,
pub precedence: NonZeroU32,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_common_group"
)]
pub common_group: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modifiers: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "Vec<DatumReference>", into = "Vec<DatumReference>")]
pub struct DatumReferences(Vec<DatumReference>);
impl DatumReferences {
#[must_use]
pub fn as_slice(&self) -> &[DatumReference] {
&self.0
}
}
impl From<DatumReferences> for Vec<DatumReference> {
fn from(value: DatumReferences) -> Self {
value.0
}
}
impl TryFrom<Vec<DatumReference>> for DatumReferences {
type Error = String;
fn try_from(references: Vec<DatumReference>) -> Result<Self, Self::Error> {
let mut compartments = std::collections::BTreeMap::<_, (usize, Option<u32>)>::new();
let mut common_groups = std::collections::BTreeMap::new();
for reference in &references {
let (count, group) = compartments
.entry(reference.precedence)
.or_insert((0, reference.common_group));
if *group != reference.common_group {
return Err(
"references: a precedence compartment must use one common_group".into(),
);
}
*count += 1;
if let Some(group) = reference.common_group {
if common_groups
.insert(group, reference.precedence)
.is_some_and(|precedence| precedence != reference.precedence)
{
return Err("references: common_group spans precedence compartments".into());
}
}
}
if compartments
.values()
.any(|(count, group)| *count == 1 && group.is_some() || *count > 1 && group.is_none())
{
return Err("references: one datum must have no common_group and multiple datums must share a common_group".into());
}
Ok(Self(references))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LimitsAndFits {
pub form_variance: String,
pub zone_variance: String,
pub grade: String,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "form", rename_all = "snake_case", deny_unknown_fields)]
pub enum DimensionTolerance {
PlusMinus {
lower: PmiValue,
upper: PmiValue,
},
Fit {
fit: LimitsAndFits,
},
PlusMinusFit {
lower: PmiValue,
upper: PmiValue,
fit: LimitsAndFits,
},
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(try_from = "PmiDimensionWire", into = "PmiDimensionWire")]
pub struct PmiDimension {
kind: DimensionKind,
nominal: Option<PmiValue>,
tolerance: Option<DimensionTolerance>,
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct PmiDimensionWire {
dimension: DimensionKind,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_nominal"
)]
nominal: Option<PmiValue>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_tolerance"
)]
tolerance: Option<DimensionTolerance>,
}
impl PmiDimension {
pub fn new(
kind: DimensionKind,
nominal: Option<PmiValue>,
tolerance: Option<DimensionTolerance>,
) -> Result<Self, String> {
Self::validate(&kind, nominal, tolerance.as_ref())?;
Ok(Self {
kind,
nominal,
tolerance,
})
}
fn validate(
kind: &DimensionKind,
nominal: Option<PmiValue>,
tolerance: Option<&DimensionTolerance>,
) -> Result<(), String> {
let expected = match kind {
DimensionKind::Angular => Some(PmiQuantity::Angle),
DimensionKind::Size
| DimensionKind::Location
| DimensionKind::Diameter
| DimensionKind::Radius => Some(PmiQuantity::Length),
DimensionKind::Other(_) => nominal.map(|value| value.quantity),
};
if nominal.is_some_and(|value| Some(value.quantity) != expected) {
return Err("dimension nominal quantity disagrees with its kind".into());
}
if let Some(
DimensionTolerance::PlusMinus { lower, upper }
| DimensionTolerance::PlusMinusFit { lower, upper, .. },
) = tolerance
{
if lower.quantity != upper.quantity {
return Err("dimension tolerance quantities disagree".into());
}
if expected.is_some_and(|quantity| lower.quantity != quantity) {
return Err(
"dimension tolerance quantity disagrees with its kind or nominal".into(),
);
}
}
Ok(())
}
#[must_use]
pub fn kind(&self) -> &DimensionKind {
&self.kind
}
#[must_use]
pub fn nominal(&self) -> Option<&PmiValue> {
self.nominal.as_ref()
}
#[must_use]
pub fn tolerance(&self) -> Option<&DimensionTolerance> {
self.tolerance.as_ref()
}
pub fn set_kind(&mut self, kind: DimensionKind) -> Result<(), String> {
Self::validate(&kind, self.nominal, self.tolerance.as_ref())?;
self.kind = kind;
Ok(())
}
pub fn set_tolerance(&mut self, tolerance: Option<DimensionTolerance>) -> Result<(), String> {
Self::validate(&self.kind, self.nominal, tolerance.as_ref())?;
self.tolerance = tolerance;
Ok(())
}
pub fn merge_tolerance(&mut self, tolerance: DimensionTolerance) -> Result<bool, String> {
let compatible = matches!(
(self.tolerance.as_ref(), &tolerance),
(None, _)
| (
Some(DimensionTolerance::PlusMinus { .. }),
DimensionTolerance::Fit { .. }
)
| (
Some(DimensionTolerance::Fit { .. }),
DimensionTolerance::PlusMinus { .. }
)
);
if !compatible {
return Ok(false);
}
Self::validate(&self.kind, self.nominal, Some(&tolerance))?;
let merged = match (self.tolerance.take(), tolerance) {
(None, value) => value,
(
Some(DimensionTolerance::PlusMinus { lower, upper }),
DimensionTolerance::Fit { fit },
)
| (
Some(DimensionTolerance::Fit { fit }),
DimensionTolerance::PlusMinus { lower, upper },
) => DimensionTolerance::PlusMinusFit { lower, upper, fit },
(old, _) => {
self.tolerance = old;
return Ok(false);
}
};
self.tolerance = Some(merged);
Ok(true)
}
}
impl TryFrom<PmiDimensionWire> for PmiDimension {
type Error = String;
fn try_from(value: PmiDimensionWire) -> Result<Self, Self::Error> {
Self::new(value.dimension, value.nominal, value.tolerance)
}
}
#[cfg(feature = "schema")]
impl JsonSchema for PmiDimension {
fn schema_name() -> std::borrow::Cow<'static, str> {
"PmiDimension".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
PmiDimensionWire::json_schema(generator)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum PmiDefinition {
Datum {
identification: String,
},
DatumSystem {
references: DatumReferences,
},
DatumTarget {
form: DatumTargetForm,
identification: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
basis: Vec<PmiTarget>,
},
GeometricTolerance {
tolerance: GeometricToleranceKind,
magnitude: PmiMagnitude,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_defined_unit"
)]
defined_unit: Option<PmiValue>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_defined_area_unit"
)]
defined_area_unit: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_defined_area_second_unit"
)]
defined_area_second_unit: Option<PmiValue>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_datum_system"
)]
datum_system: Option<PmiId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
modifiers: Vec<String>,
},
Dimension(PmiDimension),
Presentation {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_text"
)]
text: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_placement"
)]
placement: Option<Transform>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
semantics: Vec<PmiId>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PmiAnnotation {
pub id: PmiId,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_name"
)]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub targets: Vec<PmiTarget>,
pub definition: PmiDefinition,
}
crate::units::named_field!(
deserialize_source_id,
cadmpeg_core::text::NonBlankString,
"source_id"
);
#[cfg(test)]
mod tests {
use cadmpeg_test_support::edit;
use std::num::NonZeroU32;
use super::{
DatumReference, DatumReferences, DimensionKind, DimensionTolerance, GeometricToleranceKind,
LimitsAndFits, PmiAnnotation, PmiDefinition, PmiDimension, PmiMagnitude, PmiQuantity,
PmiTarget, PmiValue,
};
use crate::document::CadIr;
use crate::ids::PmiId;
use crate::report::check::Check;
use crate::validate::validate_neutral;
#[test]
fn datum_system_references_resolve_with_precedence() {
let datum_id = PmiId::mint("test:model:pmi#datum-a").expect("valid identity");
let mut ir = CadIr::empty();
ir.model.pmi.push(PmiAnnotation {
id: datum_id.clone(),
name: Some("datum A".into()),
visible: None,
targets: vec![PmiTarget::ShapeAspect {
source_id: cadmpeg_core::text::NonBlankString::new("#10")
.expect("nonempty source identity"),
}],
definition: PmiDefinition::Datum {
identification: "A".into(),
},
});
ir.model.pmi.push(PmiAnnotation {
id: PmiId::mint("test:model:pmi#system").expect("valid identity"),
name: None,
visible: None,
targets: Vec::new(),
definition: PmiDefinition::DatumSystem {
references: vec![DatumReference {
datum: datum_id,
precedence: NonZeroU32::MIN,
common_group: None,
modifiers: Vec::new(),
}]
.try_into()
.expect("valid datum compartments"),
},
});
ir.finalize(&cadmpeg_test_support::service_decode_context())
.expect("fixture ordering is admitted");
assert!(validate_neutral(&ir, Vec::new())
.expect("resource allocation did not fail")
.is_ok());
}
#[test]
fn datum_reference_wire_rejects_zero_precedence() {
let error = serde_json::from_value::<DatumReference>(serde_json::json!({
"datum": "test:model:pmi#datum-a",
"precedence": 0
}))
.unwrap_err();
let error = error.to_string();
assert!(error.contains("nonzero"), "{error}");
}
#[test]
fn dimension_wire_nests_its_tolerance_and_refuses_the_deleted_flat_keys() {
let definition = PmiDefinition::Dimension(
PmiDimension::new(
DimensionKind::Size,
Some(PmiValue::new(12.0, PmiQuantity::Length).expect("finite value")),
Some(DimensionTolerance::PlusMinus {
lower: PmiValue::new(-0.1, PmiQuantity::Length).expect("finite value"),
upper: PmiValue::new(0.2, PmiQuantity::Length).expect("finite value"),
}),
)
.expect("compatible dimension"),
);
let value = serde_json::to_value(&definition).unwrap();
assert_eq!(value["kind"], "dimension");
assert_eq!(value["nominal"]["value"], 12.0);
assert_eq!(value["tolerance"]["form"], "plus_minus");
assert_eq!(value["tolerance"]["lower"]["value"], -0.1);
assert_eq!(value["tolerance"]["upper"]["value"], 0.2);
assert!(value.get("lower_deviation").is_none());
assert!(value.get("upper_deviation").is_none());
assert!(value.get("limits_and_fits").is_none());
assert_eq!(
serde_json::from_value::<PmiDefinition>(value).unwrap(),
definition
);
let error = serde_json::from_value::<PmiDefinition>(serde_json::json!({
"kind": "dimension",
"dimension": "size",
"lower_deviation": {"value": -0.1, "quantity": "length"}
}))
.unwrap_err()
.to_string();
assert!(error.contains("unknown field `lower_deviation`"), "{error}");
let error = serde_json::from_value::<DimensionTolerance>(serde_json::json!({
"form": "fit",
"fit": {
"form_variance": "H",
"zone_variance": "",
"grade": "7",
"source": "ISO 286"
},
"lower": {"value": -0.1, "quantity": "length"}
}))
.unwrap_err()
.to_string();
assert!(error.contains("unknown field `lower`"), "{error}");
}
#[test]
fn dimension_wire_carries_an_absent_nominal_and_a_combined_tolerance() {
let value = serde_json::json!({
"kind": "dimension",
"dimension": "diameter",
"tolerance": {
"form": "plus_minus",
"lower": {"value": -0.1, "quantity": "length"},
"upper": {"value": 0.2, "quantity": "length"}
}
});
let definition =
serde_json::from_value::<PmiDefinition>(value.clone()).expect("absent nominal");
assert!(matches!(
&definition,
PmiDefinition::Dimension(relation) if matches!(relation.kind(), DimensionKind::Diameter)
&& relation.nominal().is_none()
&& matches!(relation.tolerance(), Some(DimensionTolerance::PlusMinus { .. }))
));
assert_eq!(serde_json::to_value(&definition).expect("wire"), value);
let value = serde_json::json!({
"kind": "dimension",
"dimension": "size",
"nominal": {"value": 12.0, "quantity": "length"},
"tolerance": {
"form": "plus_minus_fit",
"lower": {"value": -0.1, "quantity": "length"},
"upper": {"value": 0.2, "quantity": "length"},
"fit": {
"form_variance": "H",
"zone_variance": "",
"grade": "7",
"source": "ISO 286"
}
}
});
let definition =
serde_json::from_value::<PmiDefinition>(value.clone()).expect("combined tolerance");
assert!(matches!(
&definition,
PmiDefinition::Dimension(relation) if matches!(relation.tolerance(), Some(DimensionTolerance::PlusMinusFit { .. }))
));
assert_eq!(serde_json::to_value(&definition).expect("wire"), value);
}
#[test]
fn dimension_relation_rejects_mixed_nominal_and_tolerance_quantities() {
let nominal = PmiValue::new(12.0, PmiQuantity::Length).expect("finite value");
let lower = PmiValue::new(-0.1, PmiQuantity::Angle).expect("finite value");
let upper = PmiValue::new(0.2, PmiQuantity::Length).expect("finite value");
assert!(PmiDimension::new(
DimensionKind::Size,
Some(nominal),
Some(DimensionTolerance::PlusMinus { lower, upper }),
)
.is_err());
let wire = serde_json::json!({
"kind": "dimension", "dimension": "size",
"nominal": {"value": 12.0, "quantity": "length"},
"tolerance": {"form": "plus_minus",
"lower": {"value": -0.1, "quantity": "angle"},
"upper": {"value": 0.2, "quantity": "length"}}
});
let error = serde_json::from_value::<PmiDefinition>(wire)
.expect_err("mixed dimension quantities")
.to_string();
assert!(error.contains("quantities disagree"), "{error}");
}
#[test]
fn dimension_relation_uses_kind_when_nominal_is_absent() {
let lower = PmiValue::new(-0.1, PmiQuantity::Length).expect("finite value");
let upper = PmiValue::new(0.2, PmiQuantity::Length).expect("finite value");
assert!(PmiDimension::new(
DimensionKind::Angular,
None,
Some(DimensionTolerance::PlusMinus { lower, upper }),
)
.is_err());
let wire = serde_json::json!({
"kind": "dimension", "dimension": "angular",
"tolerance": {"form": "plus_minus",
"lower": {"value": -0.1, "quantity": "length"},
"upper": {"value": 0.2, "quantity": "length"}}
});
let error = serde_json::from_value::<PmiDefinition>(wire)
.expect_err("angular dimension needs angle tolerance")
.to_string();
assert!(error.contains("quantity disagrees"), "{error}");
}
#[test]
fn dimension_tolerance_merge_moves_fit_and_preserves_rejection() {
let lower = PmiValue::new(-0.1, PmiQuantity::Length).expect("finite value");
let upper = PmiValue::new(0.2, PmiQuantity::Length).expect("finite value");
let fit = LimitsAndFits {
form_variance: "H".into(),
zone_variance: "7".into(),
grade: "IT7".into(),
source: "ISO 286".into(),
};
let mut dimension = PmiDimension::new(DimensionKind::Size, None, None).expect("dimension");
assert!(dimension
.merge_tolerance(DimensionTolerance::Fit { fit: fit.clone() })
.expect("fit"));
assert!(dimension
.merge_tolerance(DimensionTolerance::PlusMinus { lower, upper })
.expect("compatible deviations"));
assert!(matches!(
dimension.tolerance(),
Some(DimensionTolerance::PlusMinusFit { fit: merged, .. }) if merged == &fit
));
assert!(!dimension
.merge_tolerance(DimensionTolerance::Fit { fit: fit.clone() })
.expect("additional fit refused"));
assert!(matches!(
dimension.tolerance(),
Some(DimensionTolerance::PlusMinusFit { fit: merged, .. }) if merged == &fit
));
}
#[test]
fn dimension_relation_rejects_nominal_kind_mismatch_and_atomic_edits() {
let angle = PmiValue::new(1.0, PmiQuantity::Angle).expect("finite angle");
assert!(PmiDimension::new(DimensionKind::Size, Some(angle), None).is_err());
let wire = serde_json::json!({
"kind": "dimension", "dimension": "size",
"nominal": {"value": 1.0, "quantity": "angle"}
});
let error = serde_json::from_value::<PmiDefinition>(wire)
.expect_err("size nominal needs length")
.to_string();
assert!(error.contains("nominal quantity disagrees"), "{error}");
let length = PmiValue::new(12.0, PmiQuantity::Length).expect("finite length");
let mut relation = PmiDimension::new(DimensionKind::Size, Some(length), None)
.expect("compatible dimension");
let original = relation.clone();
assert!(relation.set_kind(DimensionKind::Angular).is_err());
assert_eq!(relation, original);
assert!(relation
.set_tolerance(Some(DimensionTolerance::PlusMinus {
lower: angle,
upper: angle,
}))
.is_err());
assert_eq!(relation, original);
}
#[test]
fn curve_target_resolves_against_the_curve_arena() {
let mut ir = crate::examples::unit_cube().expect("valid unit cube fixture");
let curve = ir.model.curves[0].id.clone();
ir.model.pmi.push(PmiAnnotation {
id: PmiId::mint("synthetic:model:pmi#curve-target").expect("valid identity"),
name: Some("curve target".into()),
visible: None,
targets: vec![PmiTarget::Curve { curve }],
definition: PmiDefinition::Dimension(
PmiDimension::new(
DimensionKind::Size,
Some(PmiValue::new(1.0, PmiQuantity::Length).expect("finite value")),
None,
)
.expect("compatible dimension"),
),
});
ir.finalize(&cadmpeg_test_support::service_decode_context())
.expect("fixture ordering is admitted");
assert!(validate_neutral(&ir, Vec::new())
.expect("resource allocation did not fail")
.is_ok());
}
#[test]
fn unresolved_semantic_reference_is_invalid() {
let mut ir = CadIr::empty();
ir.model.pmi.push(PmiAnnotation {
id: PmiId::mint("test:model:pmi#graphic").expect("valid identity"),
name: None,
visible: None,
targets: Vec::new(),
definition: PmiDefinition::Presentation {
text: None,
placement: None,
semantics: vec![PmiId::mint("test:model:pmi#missing").expect("valid identity")],
},
});
let report = validate_neutral(&ir, Vec::new()).expect("resource allocation did not fail");
assert!(report
.findings
.iter()
.any(|finding| finding.check == Check::Pmi));
}
#[test]
fn datum_references_are_type_checked_and_common_groups_are_explicit() {
let mut ir = CadIr::empty();
let dimension_id = PmiId::mint("test:model:pmi#dimension").expect("valid identity");
ir.model.pmi.push(PmiAnnotation {
id: dimension_id.clone(),
name: None,
visible: None,
targets: Vec::new(),
definition: PmiDefinition::Dimension(
PmiDimension::new(
DimensionKind::Size,
Some(PmiValue::new(1.0, PmiQuantity::Length).expect("finite value")),
None,
)
.expect("compatible dimension"),
),
});
ir.model.pmi.push(PmiAnnotation {
id: PmiId::mint("test:model:pmi#system").expect("valid identity"),
name: None,
visible: None,
targets: Vec::new(),
definition: PmiDefinition::DatumSystem {
references: vec![DatumReference {
datum: dimension_id.clone(),
precedence: NonZeroU32::MIN,
common_group: None,
modifiers: Vec::new(),
}]
.try_into()
.expect("valid datum compartments"),
},
});
ir.model.pmi.push(PmiAnnotation {
id: PmiId::mint("test:model:pmi#tolerance").expect("valid identity"),
name: None,
visible: None,
targets: Vec::new(),
definition: PmiDefinition::GeometricTolerance {
tolerance: GeometricToleranceKind::Position,
magnitude: PmiMagnitude::new(
PmiValue::new(0.1, PmiQuantity::Length).expect("finite value"),
)
.expect("nonnegative magnitude"),
defined_unit: None,
defined_area_unit: None,
defined_area_second_unit: None,
datum_system: Some(dimension_id),
modifiers: Vec::new(),
},
});
let findings = validate_neutral(&ir, Vec::new())
.expect("resource allocation did not fail")
.findings;
assert!(
findings
.iter()
.filter(|finding| finding.check == Check::Pmi)
.count()
>= 2
);
}
#[test]
fn pmi_magnitude_admission_keeps_signed_dimensions_and_zero_angles() {
let negative = PmiValue::new(-1.0, PmiQuantity::Length).expect("signed dimension");
assert_eq!(
PmiValue::from_parts(
crate::scalar::FiniteReal::new(-1.0).expect("finite negative"),
PmiQuantity::Length,
),
negative
);
assert!(PmiMagnitude::new(negative).is_none());
assert!(PmiValue::new(f64::INFINITY, PmiQuantity::Length).is_none());
let zero = PmiMagnitude::new(PmiValue::new(0.0, PmiQuantity::Angle).expect("zero angle"))
.expect("zero magnitude");
assert_eq!(
PmiMagnitude::from_parts(crate::scalar::NonNegativeReal::ZERO, PmiQuantity::Angle),
zero
);
let wire = serde_json::to_string(&zero).expect("serialize");
assert_eq!(wire, r#"{"value":0.0,"quantity":"angle"}"#);
assert_eq!(
serde_json::from_str::<PmiMagnitude>(&wire).expect("deserialize"),
zero
);
let error = serde_json::from_str::<PmiMagnitude>(r#"{"value":-1.0,"quantity":"length"}"#)
.expect_err("negative magnitude");
assert!(error.to_string().contains("magnitude"));
}
#[test]
fn source_identity_admission_preserves_nonempty_wire() {
let wire = serde_json::json!({"kind": "shape_aspect", "source_id": "#42"});
let value: PmiTarget = serde_json::from_value(wire.clone()).expect("nonempty source_id");
assert_eq!(serde_json::to_value(value).expect("serialize"), wire);
let error = serde_json::from_value::<PmiTarget>(
serde_json::json!({"kind": "shape_aspect", "source_id": ""}),
)
.expect_err("empty source_id");
assert!(error.to_string().contains("source_id"));
assert!(cadmpeg_core::text::NonBlankString::new("").is_none());
}
#[test]
fn datum_compartments_are_checked_at_construction_wire_and_replacement() {
let reference = |id: &str, precedence, common_group| DatumReference {
datum: PmiId::mint(format!("test:model:pmi#{id}")).expect("valid identity"),
precedence: NonZeroU32::new(precedence).expect("positive precedence"),
common_group,
modifiers: Vec::new(),
};
let valid = vec![
reference("c", 2, None),
reference("a", 1, Some(0)),
reference("b", 1, Some(0)),
];
let mut admitted = DatumReferences::try_from(valid.clone()).expect("valid compartments");
assert_eq!(admitted.as_slice(), valid);
let wire = serde_json::json!({"kind": "datum_system", "references": valid});
let definition: PmiDefinition = serde_json::from_value(wire.clone()).expect("valid wire");
assert_eq!(serde_json::to_value(definition).expect("serialize"), wire);
assert!(DatumReferences::try_from(Vec::new())
.expect("empty system")
.as_slice()
.is_empty());
for invalid in [
vec![reference("a", 1, Some(7))],
vec![reference("a", 1, None), reference("b", 1, None)],
vec![reference("a", 1, Some(7)), reference("b", 1, None)],
vec![reference("a", 1, Some(7)), reference("b", 1, Some(8))],
vec![
reference("a", 1, Some(7)),
reference("b", 1, Some(7)),
reference("c", 2, Some(7)),
reference("d", 2, Some(7)),
],
] {
assert!(DatumReferences::try_from(invalid.clone()).is_err());
let error = serde_json::from_value::<PmiDefinition>(
serde_json::json!({"kind": "datum_system", "references": invalid}),
)
.expect_err("invalid compartments");
assert!(error.to_string().contains("references"));
assert!(serde_json::from_value::<DatumReferences>(
serde_json::to_value(&invalid).expect("serialize")
)
.is_err());
assert!({
let replacement = invalid;
edit::replace(&mut admitted, |_| {
crate::pmi::DatumReferences::try_from(replacement)
})
}
.is_err());
assert_eq!(admitted.as_slice(), valid);
}
let replacement = vec![reference("z", 3, None)];
{
let replacement = replacement.clone();
edit::replace(&mut admitted, |_| {
crate::pmi::DatumReferences::try_from(replacement)
})
}
.expect("valid replacement");
assert_eq!(admitted.as_slice(), replacement);
}
#[test]
fn a_datum_precedence_of_zero_states_no_reference() {
let wire = serde_json::json!({
"datum": "test:model:pmi#0",
"precedence": 0
});
let error = serde_json::from_value::<super::DatumReference>(wire)
.expect_err("zero is not a precedence")
.to_string();
assert!(error.contains("nonzero"), "{error}");
let wire = serde_json::json!({
"datum": "test:model:pmi#0",
"precedence": 1
});
let reference =
serde_json::from_value::<super::DatumReference>(wire).expect("one is a precedence");
assert_eq!(reference.precedence.get(), 1);
}
}
cadmpeg_core::named_optional_field!(deserialize_common_group, u32, "common_group");
cadmpeg_core::named_optional_field!(deserialize_defined_unit, PmiValue, "defined_unit");
cadmpeg_core::named_optional_field!(deserialize_defined_area_unit, String, "defined_area_unit");
cadmpeg_core::named_optional_field!(
deserialize_defined_area_second_unit,
PmiValue,
"defined_area_second_unit"
);
cadmpeg_core::named_optional_field!(deserialize_datum_system, PmiId, "datum_system");
cadmpeg_core::named_optional_field!(deserialize_nominal, PmiValue, "nominal");
cadmpeg_core::named_optional_field!(deserialize_tolerance, DimensionTolerance, "tolerance");
cadmpeg_core::named_optional_field!(deserialize_text, String, "text");
cadmpeg_core::named_optional_field!(deserialize_placement, Transform, "placement");
cadmpeg_core::named_optional_field!(deserialize_name, String, "name");
cadmpeg_core::named_optional_field!(deserialize_visible, bool, "visible");
mod identity_rewrite;
mod serialization;