cadmpeg-ir 0.6.2

CAD data model, validation, diffing, and codec API for cadmpeg.
// SPDX-License-Identifier: Apache-2.0
use super::identities;
use super::typed::{IdentityMap, RewriteIdentities};
use crate::ids::PointId;
use cadmpeg_core::decode::DecodeContext;
use cadmpeg_core::CodecError;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use std::collections::BTreeMap;

#[derive(Clone, Serialize)]
struct Text(String);

#[derive(Clone, Serialize)]
enum Variant {
    Unit,
    Newtype(PointId),
    Tuple(PointId, String),
    Struct { reference: PointId, text: String },
}

#[derive(Clone, Serialize)]
struct Entity {
    id: PointId,
    label: String,
    optional: Option<PointId>,
    absent: Option<PointId>,
    tuple: (PointId, Text),
    variants: Vec<Variant>,
    references: BTreeMap<PointId, PointId>,
    extensions: BTreeMap<String, String>,
}

rewrite_record!(Text, []; (text));
rewrite_enum!(Variant, []; {
    Unit,
    Newtype(identity),
    Tuple(identity, text),
    Struct {reference, text},
});
rewrite_record!(Entity, []; {id, label, optional, absent, tuple, variants, references, extensions});

fn id(key: &str) -> PointId {
    PointId::mint(format!("test:model:point#{key}")).unwrap()
}

#[test]
fn generic_unknown_links_share_the_typed_identity_rewrite_boundary() {
    let record = crate::NativeUnknownRecord {
        id: crate::ids::UnknownId::mint("test:model:unknown#source").unwrap(),
        links: vec![crate::ids::Identity::new("test:model:point#a").unwrap()],
    };
    let rewritten = serde_json::to_value(
        identities(
            &cadmpeg_test_support::service_decode_context(),
            record.clone(),
            |source| Ok(source.replace("test:model:", "test:occurrence:")),
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(
        rewritten,
        serde_json::json!({
            "id": "test:occurrence:unknown#source",
            "links": ["test:occurrence:point#a"]
        })
    );
    assert_eq!(record.links[0].as_str(), "test:model:point#a");
}

#[test]
fn rewriting_distinguishes_identities_from_identical_text_in_every_container() {
    let source = id("a").to_string();
    let entity = Entity {
        id: id("a"),
        label: source.clone(),
        optional: Some(id("a")),
        absent: None,
        tuple: (id("a"), Text(source.clone())),
        variants: vec![
            Variant::Unit,
            Variant::Newtype(id("a")),
            Variant::Tuple(id("a"), source.clone()),
            Variant::Struct {
                reference: id("a"),
                text: source.clone(),
            },
        ],
        references: BTreeMap::from([(id("a"), id("b"))]),
        extensions: BTreeMap::from([(source.clone(), source.clone())]),
    };
    let original = serde_json::to_value(&entity).unwrap();
    let calls = std::cell::Cell::new(0);
    let rewritten = serde_json::to_value(
        identities(
            &cadmpeg_test_support::service_decode_context(),
            entity.clone(),
            |source| {
                calls.set(calls.get() + 1);
                Ok(source.replace("test:model:", "test:occurrence:"))
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(calls.get(), 2);
    assert_eq!(
        rewritten,
        serde_json::json!({
            "id": "test:occurrence:point#a", "label": source,
            "optional": "test:occurrence:point#a", "absent": null,
            "tuple": ["test:occurrence:point#a", source],
            "variants": ["Unit", {"Newtype": "test:occurrence:point#a"},
                {"Tuple": ["test:occurrence:point#a", source]},
                {"Struct": {"reference": "test:occurrence:point#a", "text": source}}],
            "references": {"test:occurrence:point#a": "test:occurrence:point#b"},
            "extensions": {"test:model:point#a": "test:model:point#a"}
        })
    );
    assert_eq!(serde_json::to_value(&entity).unwrap(), original);
}

#[test]
fn colliding_map_keys_and_invalid_targets_fail_without_changing_the_source() {
    let source = BTreeMap::from([(id("a"), 1), (id("b"), 2)]);
    let before = serde_json::to_value(&source).unwrap();
    let error = identities(
        &cadmpeg_test_support::service_decode_context(),
        source.clone(),
        |_| Ok("test:occurrence:point#same".into()),
    )
    .unwrap_err();
    assert!(error.to_string().contains("test:model:point#b"), "{error}");
    assert!(error.to_string().contains("collides"), "{error}");
    let error = identities(
        &cadmpeg_test_support::service_decode_context(),
        source.clone(),
        |_| Ok(String::new()),
    )
    .unwrap_err();
    assert!(error.to_string().contains("invalid identity"), "{error}");
    assert_eq!(serde_json::to_value(&source).unwrap(), before);
}

#[derive(Debug, Clone)]
struct SwallowsElementErrors([PointId; 2]);

impl Serialize for SwallowsElementErrors {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut sequence = serializer.serialize_seq(Some(2))?;
        for id in &self.0 {
            sequence.serialize_element(id).ok();
        }
        sequence.end()
    }
}

impl RewriteIdentities for SwallowsElementErrors {
    fn visit_identity_references(
        &self,
        ctx: &DecodeContext<'_>,
        visitor: &mut dyn FnMut(&str) -> Result<(), CodecError>,
    ) -> Result<(), CodecError> {
        self.0.visit_identity_references(ctx, visitor)
    }
    fn rewrite_identities<F: FnMut(&str) -> Result<String, CodecError>>(
        self,
        ctx: &DecodeContext<'_>,
        map: &mut IdentityMap<'_, F>,
    ) -> Result<Self, CodecError> {
        for identity in &self.0 {
            identity.clone().rewrite_identities(ctx, map).ok();
        }
        Ok(self)
    }
}

#[test]
fn a_serializer_cannot_swallow_a_rewrite_collision() {
    let source = SwallowsElementErrors([id("a"), id("b")]);
    let error = identities(
        &cadmpeg_test_support::service_decode_context(),
        source.clone(),
        |_| Ok("test:occurrence:point#same".into()),
    )
    .unwrap_err();
    assert!(error.to_string().contains("collides"), "{error}");
    assert_eq!(
        serde_json::to_value(&source).unwrap(),
        serde_json::json!(["test:model:point#a", "test:model:point#b"])
    );
}

#[test]
fn a_field_walk_cannot_swallow_a_callback_resource_refusal() {
    let original = cadmpeg_core::decode::ResourceLimit {
        dimension: cadmpeg_core::decode::ResourceDimension::RetainedBytes,
        reason: cadmpeg_core::decode::ResourceFailure::AllocationFailed,
        limit: 4096,
        used: 128,
        additional: 256,
        operation: "test identity callback allocation",
    };
    let source = SwallowsElementErrors([id("a"), id("b")]);
    let calls = std::cell::Cell::new(0);
    let error = identities(
        &cadmpeg_test_support::service_decode_context(),
        source,
        |_| {
            calls.set(calls.get() + 1);
            Err(CodecError::ResourceLimit(original))
        },
    )
    .unwrap_err();
    assert!(matches!(error, CodecError::ResourceLimit(limit) if limit == original));
    assert_eq!(calls.get(), 1);
}

#[derive(Clone, Serialize)]
struct NativeLink {
    id: PointId,
    label: String,
    native_ref: Option<String>,
    geometry_ref: Option<String>,
    endpoint_refs: Vec<String>,
}
rewrite_record!(NativeLink, []; {id, label, native_ref, geometry_ref, endpoint_refs});

#[test]
fn full_fidelity_reference_rewrites_without_rewriting_identical_display_text() {
    let ctx = cadmpeg_test_support::service_decode_context();
    let source = NativeLink {
        id: id("owner"),
        label: "test:native:record#1".into(),
        native_ref: Some("test:native:record#1".into()),
        geometry_ref: Some("test:native:curve#1".into()),
        endpoint_refs: vec!["test:native:point#1".into()],
    };
    let rewritten = identities(&ctx, source.clone(), |source| {
        Ok(source.replace("test:", "scoped:"))
    })
    .unwrap();
    assert_eq!(rewritten.label, "test:native:record#1");
    assert_eq!(
        rewritten.native_ref.as_deref(),
        Some("scoped:native:record#1")
    );
    assert_eq!(
        rewritten.geometry_ref.as_deref(),
        Some("scoped:native:curve#1")
    );
    assert_eq!(rewritten.endpoint_refs, ["scoped:native:point#1"]);
    let mut wire = serde_json::to_value(source).unwrap();
    let mut map = IdentityMap::new(|source: &str| Ok(source.replace("test:", "scoped:")));
    NativeLink::rewrite_native_value(&ctx, &mut wire, &mut map).unwrap();
    map.finish().unwrap();
    assert_eq!(wire["native_ref"], "scoped:native:record#1");
    assert_eq!(wire["label"], "test:native:record#1");
    assert_eq!(wire["geometry_ref"], "scoped:native:curve#1");
    assert_eq!(
        wire["endpoint_refs"],
        serde_json::json!(["scoped:native:point#1"])
    );
}