ocm-types 0.2.1

Types required to implement the OpenCloudMesh filesharing protocol
Documentation
// SPDX-FileCopyrightText: 2026 Matthias Kraus <info@opengeomesh.org>
//
// SPDX-License-Identifier: LGPL-3.0-or-later

use std::fmt::Display;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ShareType {
    #[default]
    User,
    Group,
    Federation,
}

/// The OCM Address identifies a user or group "at" an OCM Server.
/// The OCM Address contains a server specific Party identifier, a host
/// locating the OCM Server and an optional port. The OCM Address is not a
/// URI as it does not have scheme and the identifier may contain reserved
/// characters.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct OcmAddress {
    address: String,
    separator_index: usize
}

impl OcmAddress {
    /// Returns the OCM Server specific identifier of a Sending or Receiving Party
    pub fn get_identifier(&self) -> &str {
        self.address.split_at(self.separator_index).0
    }

    /// Returns the address of the OCM Server where the Party identified by the OCM
    /// Address is located. This can be used to Discover the OCM Server of the Party.
    pub fn get_server_url(&self) -> &str {
        self.address.split_at(self.separator_index+1).1
    }
}

impl TryFrom<&str> for OcmAddress {
    type Error = &'static str;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        value.to_string().try_into()
    }
}

impl TryFrom<String> for OcmAddress {
    type Error = &'static str;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        if value.is_empty() {
            Err("OCM Address may not be empty")?
        }

        let separator_index = value.rfind('@')
            .ok_or("Missing '@' separator in OCM Address")?;

        if separator_index == 0 {
            Err("UserId before the '@' character in OCM Address may not be empty")?
        }
        
        if separator_index == value.len() - 1 {
            Err("OCM Server FQDN after the '@' character in OCM Address may not be empty")?
        }

        let host = value.split_at(separator_index+1).1;

        if host.contains('/') {
            Err("OCM Address may not contain a path or scheme")?
        };
        Ok(Self {
            address: value,
            separator_index
        })
    }
}

impl Display for OcmAddress {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.address)?;
        Ok(())
    }
}

impl From<OcmAddress> for String {
    fn from(val: OcmAddress) -> Self {
        val.address
    }
}

impl AsRef<str> for OcmAddress {
    fn as_ref(&self) -> &str {
        &self.address
    }
}

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


    #[test]
    fn invalid_ocm_addresses() {
        OcmAddress::try_from("").expect_err("OCM Address may not be empty");
        OcmAddress::try_from("user@https://example.org").expect_err("Scheme is not allowed in server address part");
        OcmAddress::try_from("user@example.org/subfolder").expect_err("Scheme is not allowed in server address part");
        OcmAddress::try_from("@example.org").expect_err("Empty user Id must be rejected");
        OcmAddress::try_from("user@").expect_err("Empty Server FQDN must be rejected");
        OcmAddress::try_from("@").expect_err("Empty User Id and Empty Server FQDN must be rejected");
    }

    #[test]
    fn valid_ocm_addresses() {
        let address = OcmAddress::try_from("asdf@user@example.org").expect("asdf@user@example.org should be accepted");
        assert_eq!(("asdf@user", "example.org"), (address.get_identifier(), address.get_server_url()));
        
        let address = OcmAddress::try_from("🤡asdf@user@example.org:8080").expect("🤡asdf@user@example.org:8080 should be accepted");
        assert_eq!(("🤡asdf@user", "example.org:8080"), (address.get_identifier(), address.get_server_url()));
        
        let address = OcmAddress::try_from("asdf@user@127.0.0.1").expect("asdf@user@127.0.0.1 should be accepted");
        assert_eq!(("asdf@user", "127.0.0.1"), (address.get_identifier(), address.get_server_url()));
        
        let address = OcmAddress::try_from("asdf@user@[fe80::cc7f:2f7d:80f6:3876%en0]").expect("asdf@user@[fe80::cc7f:2f7d:80f6:3876%en0] should be accepted");
        assert_eq!(("asdf@user", "[fe80::cc7f:2f7d:80f6:3876%en0]"), (address.get_identifier(), address.get_server_url()));
        
    }
}