mecha10-core 0.8.1

Mecha10 core runtime and foundational types
Documentation
//! Parsing for a node's "wire identifier" - the single string form a node's `instance_id`/
//! `camera_id` get encoded into wherever a typed `NodeRef` isn't available to carry them as
//! separate fields (e.g. `mecha10-node-runner`'s CLI args, `mecha10-templates`' generated robot
//! binary, which can't depend on `mecha10-cli-core` where `NodeRef` itself lives).
//!
//! `NodeRef::wire_identifier()` (`mecha10-cli-core::types::project::targets`) is the encode
//! direction, for a typed `NodeRef`; [`split_wire_identifier`] here is the matching decode
//! direction, usable by any caller that only has the resulting string in hand. Kept in
//! `mecha10-core` (not `mecha10-cli-core`) specifically so it's one shared implementation every
//! caller depends on, rather than each hand-duplicating this exact parsing - which is what
//! previously let `mecha10-templates`' generated robot binary's own startup loop silently drift
//! out of sync with this logic (confirmed live: a `camera_id`-keyed node's `#camera_id` suffix
//! leaked straight through into a binary-download URL because that loop used a different,
//! incomplete splitting helper than the one its own lookup maps were built with).
//!
//! Note: the module that introduced this file was merged as a `refactor:` commit, which the
//! release pipeline's conventional-commit gate doesn't treat as releasable - so `mecha10-core`
//! was never republished to crates.io with this module present, even though
//! `main.rs.template` (a `*`-versioned consumer) started importing it immediately. Any project
//! generated or rebuilt against the crates.io-published `mecha10-core` in that gap failed with
//! `unresolved import mecha10_core::wire_identifier`. This comment (and the `fix:` commit that
//! carries it) exists to force that missing patch release.
//!
//! Encoding: `"{name}"` (plain), `"{name}::{instance_id}"`, `"{name}#{camera_id}"`, or
//! `"{name}::{instance_id}#{camera_id}"` - `::` and `#` are both disallowed in a bare node
//! identifier, so neither delimiter can collide with `name` itself.

/// Split a wire identifier into its base node name, optional `instance_id`, and optional
/// `camera_id`. The inverse of `NodeRef::wire_identifier()`.
pub fn split_wire_identifier(node_identifier: &str) -> (&str, Option<&str>, Option<&str>) {
    let (rest, camera_id) = match node_identifier.rsplit_once('#') {
        Some((rest, camera_id)) => (rest, Some(camera_id)),
        None => (node_identifier, None),
    };
    let (base, instance_id) = match rest.split_once("::") {
        Some((base, instance_id)) => (base, Some(instance_id)),
        None => (rest, None),
    };
    (base, instance_id, camera_id)
}

/// Strip a node identifier's `@scope/` namespace, if any (e.g. `"@mecha10/camera"` ->
/// `"camera"`). Does not strip `::instance_id`/`#camera_id` - callers that need the bare base
/// name alone should call [`split_wire_identifier`] first and pass its `.0` here.
pub fn short_name(name: &str) -> &str {
    name.rsplit('/').next().unwrap_or(name)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn split_wire_identifier_leaves_a_plain_identifier_untouched() {
        assert_eq!(
            split_wire_identifier("@mecha10/speaker"),
            ("@mecha10/speaker", None, None)
        );
    }

    #[test]
    fn split_wire_identifier_splits_an_encoded_instance_id() {
        assert_eq!(
            split_wire_identifier("@mecha10/camera::real"),
            ("@mecha10/camera", Some("real"), None)
        );
    }

    #[test]
    fn split_wire_identifier_splits_a_camera_id_with_no_instance_id() {
        assert_eq!(
            split_wire_identifier("@mecha10/video-streamer#camera"),
            ("@mecha10/video-streamer", None, Some("camera"))
        );
    }

    #[test]
    fn split_wire_identifier_splits_both_instance_id_and_camera_id() {
        assert_eq!(
            split_wire_identifier("@mecha10/video-streamer::sim-streamer#simulation_camera"),
            (
                "@mecha10/video-streamer",
                Some("sim-streamer"),
                Some("simulation_camera")
            )
        );
    }

    #[test]
    fn short_name_strips_the_scope_namespace() {
        assert_eq!(short_name("@mecha10/camera"), "camera");
    }

    #[test]
    fn short_name_leaves_an_unscoped_name_untouched() {
        assert_eq!(short_name("my-node"), "my-node");
    }
}