#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cadmpeg_ir::ids::{EdgeId, FaceId, VertexId};
pub mod identity;
macro_rules! native_record {
(
$(#[doc = $record_doc:literal])*
$name:ident, $wire:ident, $kind:literal,
#[doc = $index_doc:literal]
record_index,
$(#[doc = $entity_doc:literal])*
$entity:ident: $entity_ty:ty,
$(
$(#[doc = $field_doc:literal])*
$field:ident: $field_ty:ty $([$($wire_attr:meta),*])?,
)*
) => {
$(#[doc = $record_doc])*
#[derive(Debug, Clone, PartialEq)]
pub struct $name {
pub source_namespace: identity::NativeRecordNamespace,
#[doc = $index_doc]
pub record_index: u32,
$(#[doc = $entity_doc])*
pub $entity: $entity_ty,
$(
$(#[doc = $field_doc])*
pub $field: $field_ty,
)*
}
impl $name {
#[must_use]
pub fn id(&self) -> String {
self.source_namespace.id($kind, self.record_index)
}
}
mod $wire {
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::Deserialize;
$(#[doc = $record_doc])*
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub(super) struct Wire {
#[cfg_attr(feature = "schema", schemars(with = "cadmpeg_ir::ids::Identity"))]
pub(super) id: String,
$(#[doc = $entity_doc])*
pub(super) $entity: $entity_ty,
#[doc = $index_doc]
pub(super) record_index: u32,
$(
$(#[doc = $field_doc])*
$($(#[$wire_attr])*)?
pub(super) $field: $field_ty,
)*
}
}
impl Serialize for $name {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct Wire<'a> {
id: identity::NativeRecordIdentity<'a>,
$entity: &'a $entity_ty,
record_index: u32,
$(
$($(#[$wire_attr])*)?
$field: &'a $field_ty,
)*
}
Wire {
id: self.source_namespace.serialized_id($kind, self.record_index),
$entity: &self.$entity,
record_index: self.record_index,
$($field: &self.$field,)*
}.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = $wire::Wire::deserialize(deserializer)?;
let source_namespace = identity::NativeRecordNamespace::from_wire(&wire.id, wire.record_index, $kind)
.map_err(serde::de::Error::custom)?;
Ok(Self {
source_namespace,
record_index: wire.record_index,
$entity: wire.$entity,
$($field: wire.$field,)*
})
}
}
impl cadmpeg_ir::schema::rewrite::typed::RewriteIdentities for $name {
fn visit_identity_references(&self, ctx: &cadmpeg_core::decode::DecodeContext<'_>, visitor: &mut dyn FnMut(&str) -> Result<(), cadmpeg_core::CodecError>) -> Result<(), cadmpeg_core::CodecError> {
let _depth = ctx.enter_nested("walk ASM native references")?;
ctx.charge_work(1, "walk ASM native references")?;
self.source_namespace.visit(ctx, $kind, self.record_index, visitor)?;
cadmpeg_ir::schema::rewrite::typed::RewriteIdentities::visit_identity_references(&self.$entity, ctx, visitor)?;
$(cadmpeg_ir::schema::rewrite::typed::RewriteIdentities::visit_identity_references(&self.$field, ctx, visitor)?;)*
Ok(())
}
fn rewrite_identities<F: FnMut(&str) -> Result<String, cadmpeg_core::CodecError>>(mut self, ctx: &cadmpeg_core::decode::DecodeContext<'_>, map: &mut cadmpeg_ir::schema::rewrite::typed::IdentityMap<'_, F>) -> Result<Self, cadmpeg_core::CodecError> {
let _depth = ctx.enter_nested("rewrite ASM native fields")?;
ctx.charge_work(1, "rewrite ASM native fields")?;
self.source_namespace = self.source_namespace.rewrite(ctx, $kind, self.record_index, map)?;
self.$entity = cadmpeg_ir::schema::rewrite::typed::RewriteIdentities::rewrite_identities(self.$entity, ctx, map)?;
$(self.$field = cadmpeg_ir::schema::rewrite::typed::RewriteIdentities::rewrite_identities(self.$field, ctx, map)?;)*
Ok(self)
}
}
#[cfg(feature = "schema")]
impl JsonSchema for $name {
fn schema_name() -> std::borrow::Cow<'static, str> {
stringify!($name).into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
$wire::Wire::json_schema(generator)
}
}
};
}
native_record! {
EdgeContinuity, edge_continuity, "edge-continuity",
record_index,
edge: cadmpeg_ir::ids::EdgeId,
sense: cadmpeg_ir::topology::Sense,
continuity: String,
}
native_record! {
EdgeOwnership, edge_ownership, "edge-ownership",
record_index,
edge: cadmpeg_ir::ids::EdgeId,
owner_coedge: Option<cadmpeg_ir::ids::CoedgeId> [serde(default, skip_serializing_if = "Option::is_none")],
}
native_record! {
VertexOwnership, vertex_ownership, "vertex-ownership",
record_index,
vertex: cadmpeg_ir::ids::VertexId,
owning_edge: cadmpeg_ir::ids::EdgeId,
endpoint_index: crate::brep::records::EndpointSlot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "u8", into = "u8")]
pub enum EndpointSlot {
Start,
End,
}
impl EndpointSlot {
#[must_use]
pub const fn code(self) -> u8 {
match self {
Self::Start => 0,
Self::End => 1,
}
}
}
impl From<EndpointSlot> for u8 {
fn from(value: EndpointSlot) -> Self {
value.code()
}
}
impl TryFrom<u8> for EndpointSlot {
type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Start),
1 => Ok(Self::End),
_ => Err("endpoint_index must be 0 or 1"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum FaceContainment {
In,
Out,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FaceSidedness {
pub source_namespace: identity::NativeRecordNamespace,
pub record_index: u32,
pub face: FaceId,
pub native_sense: cadmpeg_ir::topology::Sense,
pub carrier_flipped: bool,
pub containment: Option<FaceContainment>,
}
impl Serialize for FaceSidedness {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct Wire<'a> {
id: identity::NativeRecordIdentity<'a>,
face: &'a FaceId,
record_index: u32,
native_sense: cadmpeg_ir::topology::Sense,
normalized_sense: cadmpeg_ir::topology::Sense,
#[serde(skip_serializing_if = "Option::is_none")]
containment: Option<FaceContainment>,
}
Wire {
id: self
.source_namespace
.serialized_id("face-sidedness", self.record_index),
face: &self.face,
record_index: self.record_index,
native_sense: self.native_sense,
normalized_sense: self.normalized_sense(),
containment: self.containment,
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for FaceSidedness {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
FaceSidednessWire::deserialize(deserializer)
.and_then(|w| FaceSidedness::try_from(w).map_err(serde::de::Error::custom))
}
}
#[cfg(feature = "schema")]
impl JsonSchema for FaceSidedness {
fn schema_name() -> std::borrow::Cow<'static, str> {
"FaceSidedness".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
FaceSidednessWire::json_schema(generator)
}
}
impl FaceSidedness {
fn normalized_sense(&self) -> cadmpeg_ir::topology::Sense {
use cadmpeg_ir::topology::Sense;
match (self.native_sense, self.carrier_flipped) {
(Sense::Forward, true) => Sense::Reversed,
(Sense::Reversed, true) => Sense::Forward,
(sense, false) => sense,
}
}
#[must_use]
pub fn id(&self) -> String {
self.source_namespace
.id("face-sidedness", self.record_index)
}
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct FaceSidednessWire {
#[cfg_attr(feature = "schema", schemars(with = "cadmpeg_ir::ids::Identity"))]
id: String,
face: FaceId,
record_index: u32,
native_sense: cadmpeg_ir::topology::Sense,
normalized_sense: cadmpeg_ir::topology::Sense,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_containment"
)]
containment: Option<FaceContainment>,
}
impl From<FaceSidedness> for FaceSidednessWire {
fn from(value: FaceSidedness) -> Self {
let normalized_sense = value.normalized_sense();
Self {
id: value.id(),
face: value.face,
record_index: value.record_index,
native_sense: value.native_sense,
normalized_sense,
containment: value.containment,
}
}
}
impl TryFrom<FaceSidednessWire> for FaceSidedness {
type Error = String;
fn try_from(value: FaceSidednessWire) -> Result<Self, Self::Error> {
let source_namespace = identity::NativeRecordNamespace::from_wire(
&value.id,
value.record_index,
"face-sidedness",
)?;
Ok(Self {
source_namespace,
record_index: value.record_index,
face: value.face,
native_sense: value.native_sense,
carrier_flipped: value.native_sense != value.normalized_sense,
containment: value.containment,
})
}
}
native_record! {
FaceNativeKey, face_native_key, "face-native-key",
record_index,
face: cadmpeg_ir::ids::FaceId,
asm_face_key: Option<u64> [serde(default, skip_serializing_if = "Option::is_none")],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case", tag = "slot", deny_unknown_fields)]
pub enum EvaluatedToleranceSlot {
Absent {},
Unset {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_trailing"
)]
trailing: Option<i64>,
},
Evaluated {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_trailing"
)]
trailing: Option<i64>,
},
}
impl EvaluatedToleranceSlot {
#[must_use]
pub fn trailing(self) -> Option<i64> {
match self {
Self::Absent {} => None,
Self::Unset { trailing } | Self::Evaluated { trailing } => trailing,
}
}
}
native_record! {
TolerantVertexTail, tolerant_vertex_tail, "tolerant-vertex-tail",
record_index,
vertex: cadmpeg_ir::ids::VertexId,
leading_tolerances: [cadmpeg_ir::scalar::FiniteReal; 2],
evaluated_slot: crate::brep::records::EvaluatedToleranceSlot,
}
native_record! {
TolerantEdgeTail, tolerant_edge_tail, "tolerant-edge-tail",
record_index,
edge: cadmpeg_ir::ids::EdgeId,
entity_revision: i64,
trailing_field: Option<i64>,
}
native_record! {
TolerantCoedgeParameters, tolerant_coedge_parameters, "tolerant-coedge-parameters",
record_index,
coedge: cadmpeg_ir::ids::CoedgeId,
parameter_range: cadmpeg_ir::units::FiniteVector<2>,
extension: crate::brep::records::TolerantCoedgeExtension [serde(default)],
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case", tag = "layout", deny_unknown_fields)]
pub enum TolerantCoedgeExtension {
None {},
Reference {
#[serde(deserialize_with = "cadmpeg_core::absent_key::nullable")]
target: Option<i64>,
},
Empty {
#[serde(deserialize_with = "cadmpeg_core::absent_key::nullable")]
target: Option<i64>,
},
EmbeddedCurve {
#[serde(deserialize_with = "cadmpeg_core::absent_key::nullable")]
target: Option<i64>,
curve_reversed: bool,
payload_token_count: u32,
#[serde(deserialize_with = "cadmpeg_core::absent_key::nullable")]
parameter_range: Option<cadmpeg_ir::units::FiniteVector<2>>,
},
}
impl Default for TolerantCoedgeExtension {
fn default() -> Self {
Self::None {}
}
}
native_record! {
MeshSurfaceSentinel, mesh_surface_sentinel, "mesh-surface-sentinel",
record_index,
surface: cadmpeg_ir::ids::SurfaceId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum WireSide {
In,
Out,
}
native_record! {
WireTopology, wire_topology, "wire-topology",
record_index,
shell: cadmpeg_ir::ids::ShellId,
members: crate::brep::records::WireMembers [serde(flatten)],
side: crate::brep::records::WireSide,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "WireMembersWire")]
pub enum WireMembers {
Edges(Vec<EdgeId>),
Vertex(VertexId),
}
impl WireMembers {
#[must_use]
pub fn edges(&self) -> &[EdgeId] {
match self {
Self::Edges(edges) => edges,
Self::Vertex(_) => &[],
}
}
#[must_use]
pub fn free_vertex(&self) -> Option<&VertexId> {
match self {
Self::Edges(_) => None,
Self::Vertex(vertex) => Some(vertex),
}
}
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct WireMembersWire {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
edges: Vec<EdgeId>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_free_vertex"
)]
free_vertex: Option<VertexId>,
}
impl Serialize for WireMembers {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
match self {
Self::Edges(edges) if !edges.is_empty() => map.serialize_entry("edges", edges)?,
Self::Edges(_) => {}
Self::Vertex(vertex) => map.serialize_entry("free_vertex", vertex)?,
}
map.end()
}
}
impl TryFrom<WireMembersWire> for WireMembers {
type Error = &'static str;
fn try_from(wire: WireMembersWire) -> Result<Self, Self::Error> {
match wire.free_vertex {
None => Ok(Self::Edges(wire.edges)),
Some(vertex) if wire.edges.is_empty() => Ok(Self::Vertex(vertex)),
Some(_) => Err("wire edges and free_vertex are mutually exclusive"),
}
}
}
native_record! {
BodyNativeKey, body_native_key, "body-native-key",
record_index,
body: cadmpeg_ir::ids::BodyId,
body_ordinal: u32,
source_brep: Option<String> [serde(default, skip_serializing_if = "Option::is_none")],
asm_body_key: Option<u64> [serde(default, skip_serializing_if = "Option::is_none")],
}
native_record! {
TransformHints, transform_hints, "transform-hints",
record_index,
body: cadmpeg_ir::ids::BodyId,
rotation: bool,
reflection: bool,
shear: bool,
}
#[cfg(test)]
mod tests {
use super::{EndpointSlot, TolerantCoedgeParameters, TolerantVertexTail, WireMembers};
use cadmpeg_ir::ids::{EdgeId, VertexId};
use serde::Deserialize;
#[test]
fn tolerant_vertex_tail_json_keeps_sentinel_and_refuses_nonfinite_value() {
use serde_value::Value;
let json = serde_json::json!({
"id": "f3d:asm:tolerant-vertex-tail#1",
"record_index": 1,
"vertex": "f3d:brep:entity#1",
"leading_tolerances": [-1.0, 0.03],
"evaluated_slot": {"slot": "unset", "trailing": 0},
});
let tail: TolerantVertexTail = serde_json::from_value(json.clone()).unwrap();
assert_eq!(serde_json::to_value(&tail).unwrap(), json);
let mut wire = serde_value::to_value(&tail).unwrap();
let Value::Map(ref mut members) = wire else {
panic!("native record wire must be a map");
};
members.insert(
Value::String("leading_tolerances".into()),
Value::Seq(vec![Value::F64(f64::INFINITY), Value::F64(-1.0)]),
);
let error = TolerantVertexTail::deserialize(wire).unwrap_err();
assert!(error.to_string().contains("FiniteReal must be finite"));
}
#[test]
fn tolerant_coedge_interval_json_refuses_nonfinite_endpoints() {
use serde_value::Value;
let json = serde_json::json!({
"id": "f3d:asm:tolerant-coedge-parameters#1",
"record_index": 1,
"coedge": "f3d:brep:entity#1",
"parameter_range": [-1.0, 2.0],
"extension": {"layout": "none"},
});
let parameters: TolerantCoedgeParameters = serde_json::from_value(json.clone()).unwrap();
assert_eq!(serde_json::to_value(¶meters).unwrap(), json);
let mut wire = serde_value::to_value(¶meters).unwrap();
let Value::Map(ref mut members) = wire else {
panic!("native record wire must be a map");
};
members.insert(
Value::String("parameter_range".into()),
Value::Seq(vec![Value::F64(f64::INFINITY), Value::F64(2.0)]),
);
let error = TolerantCoedgeParameters::deserialize(wire).unwrap_err();
assert!(error.to_string().contains("finite"));
}
fn assert_empty_slot_refuses_extra_keys<T>(arena: &str, field: &str, record: &serde_json::Value)
where
T: serde::de::DeserializeOwned + serde::Serialize + std::fmt::Debug,
{
let mut document = serde_json::to_value(cadmpeg_ir::CadIr::empty()).unwrap();
document["native"] = serde_json::json!({"f3d": {arena: [record.clone()]}});
let admitted: cadmpeg_ir::CadIr = serde_json::from_value(document.clone()).unwrap();
let typed: Vec<T> = admitted
.native
.namespace("f3d")
.unwrap()
.arena_as(arena)
.unwrap();
assert_eq!(&serde_json::to_value(&typed[0]).unwrap(), record);
for extra in [
serde_json::Value::Null,
serde_json::json!(false),
serde_json::json!(0),
serde_json::json!(1.25),
serde_json::json!("extra"),
serde_json::json!([]),
serde_json::json!({}),
] {
let mut invalid = document.clone();
invalid["native"]["f3d"][arena][0][field]["zz_bogus"] = extra;
let admitted: cadmpeg_ir::CadIr = serde_json::from_value(invalid).unwrap();
let error = admitted
.native
.namespace("f3d")
.unwrap()
.arena_as::<T>(arena)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{error}");
assert!(error.contains(arena), "{error}");
assert!(error.contains(record["id"].as_str().unwrap()), "{error}");
}
}
#[test]
fn absent_vertex_tolerance_refuses_extra_keys_after_document_admission() {
assert_empty_slot_refuses_extra_keys::<super::TolerantVertexTail>(
"tolerant_vertex_tails",
"evaluated_slot",
&serde_json::json!({
"id": "f3d:asm:tolerant-vertex-tail#1",
"record_index": 1,
"vertex": "f3d:brep:entity#1",
"leading_tolerances": [-1.0, -1.0],
"evaluated_slot": {"slot": "absent"},
}),
);
}
#[test]
fn absent_coedge_extension_refuses_extra_keys_after_document_admission() {
assert_empty_slot_refuses_extra_keys::<super::TolerantCoedgeParameters>(
"tolerant_coedge_parameters",
"extension",
&serde_json::json!({
"id": "f3d:asm:tolerant-coedge-parameters#1",
"record_index": 1,
"coedge": "f3d:brep:entity#1",
"parameter_range": [0.0, 1.0],
"extension": {"layout": "none"},
}),
);
}
#[test]
fn tolerant_coedge_curve_sense_requires_the_current_wire_name() {
let extension = super::TolerantCoedgeExtension::EmbeddedCurve {
target: None,
curve_reversed: true,
payload_token_count: 0,
parameter_range: None,
};
let wire = serde_value::to_value(&extension).unwrap();
assert_eq!(
super::TolerantCoedgeExtension::deserialize(wire.clone()).unwrap(),
extension
);
let serde_value::Value::Map(mut fields) = wire else {
panic!("extension map")
};
let value = fields
.remove(&serde_value::Value::String("curve_reversed".into()))
.unwrap();
fields.insert(serde_value::Value::String("flag".into()), value);
let error = super::TolerantCoedgeExtension::deserialize(serde_value::Value::Map(fields))
.unwrap_err();
assert!(error.to_string().contains("curve_reversed"));
}
#[test]
fn body_native_key_requires_explicit_body_ordinal() {
let key = super::BodyNativeKey {
source_namespace: super::identity::NativeRecordNamespace::new(crate::asm_format!(
"f3d"
)),
record_index: 17,
body: cadmpeg_ir::ids::BodyId::mint("f3d:brep:entity#17").unwrap(),
body_ordinal: 0,
source_brep: Some("Body1.sab".into()),
asm_body_key: None,
};
let wire = serde_value::to_value(&key).unwrap();
assert_eq!(
super::BodyNativeKey::deserialize(wire.clone()).unwrap(),
key
);
let serde_value::Value::Map(mut fields) = wire else {
panic!("record map")
};
assert_eq!(
fields.remove(&serde_value::Value::String("body_ordinal".into())),
Some(serde_value::Value::U32(0))
);
let error = super::BodyNativeKey::deserialize(serde_value::Value::Map(fields)).unwrap_err();
assert!(error.to_string().contains("body_ordinal"));
}
#[test]
fn endpoint_slot_preserves_numeric_wire_and_rejects_other_indices() {
for (slot, code) in [(EndpointSlot::Start, 0), (EndpointSlot::End, 1)] {
let wire = serde_value::to_value(slot).expect("serialize endpoint");
assert_eq!(wire, serde_value::Value::U8(code));
assert_eq!(
EndpointSlot::deserialize(wire).expect("read endpoint"),
slot
);
}
for code in [2, u8::MAX] {
let error = EndpointSlot::deserialize(serde_value::Value::U8(code))
.expect_err("undefined endpoint");
assert!(error.to_string().contains("endpoint_index"));
}
}
#[test]
fn wire_members_preserve_flat_fields_and_reject_mixed_membership() {
use serde_value::Value;
let edge = EdgeId::mint("asm:test:edge#1").expect("edge id");
let vertex = VertexId::mint("asm:test:vertex#2").expect("vertex id");
let edge_value = serde_value::to_value(&edge).expect("edge wire");
let vertex_value = serde_value::to_value(&vertex).expect("vertex wire");
for (members, fields) in [
(WireMembers::Edges(Vec::new()), Vec::new()),
(
WireMembers::Edges(vec![edge]),
vec![(
Value::String("edges".into()),
Value::Seq(vec![edge_value.clone()]),
)],
),
(
WireMembers::Vertex(vertex),
vec![(Value::String("free_vertex".into()), vertex_value.clone())],
),
] {
let wire = Value::Map(fields.into_iter().collect());
assert_eq!(
serde_value::to_value(&members).expect("serialize members"),
wire
);
assert_eq!(
WireMembers::deserialize(wire).expect("read members"),
members
);
}
let mixed = Value::Map(
[
(Value::String("edges".into()), Value::Seq(vec![edge_value])),
(Value::String("free_vertex".into()), vertex_value),
]
.into_iter()
.collect(),
);
let error = WireMembers::deserialize(mixed).expect_err("mixed wire members");
assert!(error.to_string().contains("edges and free_vertex"));
}
#[test]
fn a_stated_null_free_vertex_is_refused() {
let error = serde_json::from_str::<WireMembers>("{\"free_vertex\":null}")
.expect_err("a stated null is not a second spelling of absence")
.to_string();
assert!(
error.contains("this key states a value or is left out; it does not state null"),
"{error}"
);
assert_eq!(
serde_json::from_str::<WireMembers>("{}").expect("an omitted free_vertex"),
WireMembers::Edges(Vec::new())
);
}
#[cfg(feature = "schema")]
#[test]
fn the_wire_member_schema_states_the_keys_the_writer_states() {
let schema =
serde_json::to_value(schemars::schema_for!(WireMembers)).expect("members schema");
let properties: std::collections::BTreeSet<String> = schema["properties"]
.as_object()
.expect("the members schema states properties")
.keys()
.cloned()
.collect();
let edge = EdgeId::mint("asm:test:edge#1").expect("edge id");
let vertex = VertexId::mint("asm:test:vertex#2").expect("vertex id");
let written: std::collections::BTreeSet<String> =
[WireMembers::Edges(vec![edge]), WireMembers::Vertex(vertex)]
.iter()
.flat_map(|members| {
serde_json::to_value(members)
.expect("serialize members")
.as_object()
.expect("members write a map")
.keys()
.cloned()
.collect::<Vec<_>>()
})
.collect();
assert_eq!(properties, written);
}
#[test]
fn a_stated_null_tolerant_coedge_target_is_the_native_null_reference() {
assert_eq!(
serde_json::from_str::<super::TolerantCoedgeExtension>(
"{\"layout\":\"reference\",\"target\":null}"
)
.expect("a stated null target"),
super::TolerantCoedgeExtension::Reference { target: None }
);
}
}
cadmpeg_core::named_optional_field!(deserialize_containment, FaceContainment, "containment");
cadmpeg_core::named_optional_field!(deserialize_trailing, i64, "trailing");
cadmpeg_core::named_optional_field!(deserialize_free_vertex, VertexId, "free_vertex");
mod identity_rewrite;