roplat 0.2.0

roplat: just a robot operation system
Documentation
//! IPC 端点身份:URI 解析与 Schema 指纹
//!
//! 两个进程独立编译、无法共享 Rust 类型系统,因此用「URI + namespace + schema 哈希」
//! 三段式身份标识。双方在编译期各自计算出相同的 `SchemaId` 后,在握手时比对。
//!
//! URI 形式:
//! ```text
//! roplat-ipc://<namespace>/<endpoint_name>?msg=<schema_id>&v=<msg_version>
//! ```

use super::transport::{IpcError, IpcResult};
use serde::{Deserialize, Serialize};
use std::fmt;

/// 端点角色
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
    /// Publisher endpoint.
    Publisher,
    /// Subscriber endpoint.
    Subscriber,
}

impl Role {
    /// Returns role string representation.
    pub fn as_str(&self) -> &'static str {
        match self {
            Role::Publisher => "publisher",
            Role::Subscriber => "subscriber",
        }
    }

    /// 对端角色(握手时用于对比 peer 是否合法)
    pub fn counterpart(&self) -> Role {
        match self {
            Role::Publisher => Role::Subscriber,
            Role::Subscriber => Role::Publisher,
        }
    }
}

/// 消息类型的结构指纹
///
/// 由 `#[roplat_msg]` 宏在编译期计算,内容为:
/// `sha256(字段名列表 || 字段类型列表 || repr 布局 || 字节序)` 的前 12 个十六进制字符。
///
/// MVP 阶段暂由用户手动声明;后续由宏自动生成 `const SCHEMA_ID: SchemaId`。
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SchemaId(pub String);

impl SchemaId {
    /// 运行时构造
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    /// 返回底层字符串视图。
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for SchemaId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl From<&str> for SchemaId {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

/// 端点 URI
///
/// 形如 `roplat-ipc://<namespace>/<endpoint_name>?msg=<schema_id>&v=<msg_version>`。
/// 解析失败返回 `IpcError::InvalidUri`。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EndpointUri {
    /// Namespace.
    pub namespace: String,
    /// Endpoint name.
    pub name: String,
    /// Message schema fingerprint.
    pub schema_id: SchemaId,
    /// Message version.
    pub msg_version: u16,
}

impl EndpointUri {
    /// Constructs a new endpoint URI object.
    pub fn new(
        namespace: impl Into<String>,
        name: impl Into<String>,
        schema_id: SchemaId,
        msg_version: u16,
    ) -> Self {
        Self {
            namespace: namespace.into(),
            name: name.into(),
            schema_id,
            msg_version,
        }
    }

    /// 解析形如 `roplat-ipc://ns/name?msg=xxx&v=1` 的字符串
    pub fn parse(s: &str) -> IpcResult<Self> {
        let rest = s
            .strip_prefix("roplat-ipc://")
            .ok_or_else(|| IpcError::InvalidUri(format!("missing scheme: {s}")))?;

        // 分离 path 与 query
        let (path, query) = match rest.split_once('?') {
            Some((p, q)) => (p, Some(q)),
            None => (rest, None),
        };

        let (namespace, name) = path
            .split_once('/')
            .ok_or_else(|| IpcError::InvalidUri(format!("missing namespace/name: {s}")))?;

        if namespace.is_empty() || name.is_empty() {
            return Err(IpcError::InvalidUri(format!(
                "empty namespace or name: {s}"
            )));
        }

        let (mut schema_id, mut msg_version) = (None, None);
        if let Some(q) = query {
            for pair in q.split('&') {
                let (k, v) = pair
                    .split_once('=')
                    .ok_or_else(|| IpcError::InvalidUri(format!("bad query pair: {pair}")))?;
                match k {
                    "msg" => schema_id = Some(SchemaId::from(v)),
                    "v" => {
                        msg_version = Some(
                            v.parse::<u16>()
                                .map_err(|e| IpcError::InvalidUri(format!("bad version: {e}")))?,
                        )
                    }
                    _ => {}
                }
            }
        }

        Ok(Self {
            namespace: namespace.to_string(),
            name: name.to_string(),
            schema_id: schema_id
                .ok_or_else(|| IpcError::InvalidUri(format!("missing msg=: {s}")))?,
            msg_version: msg_version
                .ok_or_else(|| IpcError::InvalidUri(format!("missing v=: {s}")))?,
        })
    }

    /// 在 rendezvous 目录下的相对路径 `<namespace>/<name>.rdv`
    pub fn rendezvous_relpath(&self) -> String {
        format!("{}/{}.rdv", self.namespace, self.name)
    }
}

impl fmt::Display for EndpointUri {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "roplat-ipc://{}/{}?msg={}&v={}",
            self.namespace, self.name, self.schema_id, self.msg_version
        )
    }
}

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

    #[test]
    fn parse_roundtrip() {
        let s = "roplat-ipc://default/sensor?msg=a4f1c2&v=2";
        let uri = EndpointUri::parse(s).unwrap();
        assert_eq!(uri.namespace, "default");
        assert_eq!(uri.name, "sensor");
        assert_eq!(uri.schema_id.as_str(), "a4f1c2");
        assert_eq!(uri.msg_version, 2);
        assert_eq!(uri.to_string(), s);
    }

    #[test]
    fn parse_missing_scheme() {
        assert!(EndpointUri::parse("default/sensor?msg=x&v=1").is_err());
    }

    #[test]
    fn parse_missing_msg() {
        assert!(EndpointUri::parse("roplat-ipc://ns/n?v=1").is_err());
    }
}