singe-core 0.1.0-alpha.3

Shared utilities for Singe crates.
use std::{
    ffi::{CStr, CString, NulError},
    path::Path,
};

#[macro_export]
macro_rules! impl_enum_conversion {
    ($ty:ty, $from:ty, $into:ty $(,)?) => {
        const _: () = {
            impl From<$from> for $into {
                fn from(value: $from) -> Self {
                    let value = value as $ty;
                    <$into>::try_from(value).unwrap()
                }
            }

            impl From<$into> for $from {
                fn from(value: $into) -> Self {
                    let value = value.into();
                    unsafe { core::mem::transmute::<$ty, $from>(value) }
                }
            }
        };
    };

    ($from:ty, $into:ty $(,)?) => {
        impl_enum_conversion!(u32, $from, $into);
    };
}

#[macro_export]
macro_rules! assert_close {
    ($actual:expr, $expected:expr $(,)?) => {
        $crate::assert_close!($actual, $expected, 1.0e-5);
    };

    ($actual:expr, $expected:expr, $tolerance:expr $(,)?) => {{
        let actual = $actual;
        let expected = $expected;
        let tolerance = $tolerance as f64;

        assert_eq!(
            actual.len(),
            expected.len(),
            "assert_close length mismatch: actual len {}, expected len {}",
            actual.len(),
            expected.len(),
        );

        for (index, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
            let actual = *actual as f64;
            let expected = *expected as f64;
            let difference = (actual - expected).abs();

            assert!(
                difference <= tolerance,
                "assert_close failed at index {index}: actual {actual}, expected {expected}, difference {difference}, tolerance {tolerance}",
            );
        }
    }};

    ($actual:expr, $expected:expr, $tolerance:expr, $name:expr $(,)?) => {{
        let actual = $actual as f64;
        let expected = $expected as f64;
        let tolerance = $tolerance as f64;
        let difference = (actual - expected).abs();

        assert!(
            difference <= tolerance,
            "assert_close failed for {}: actual {actual}, expected {expected}, difference {difference}, tolerance {tolerance}",
            $name,
        );
    }};
}

pub fn path_to_cstring(path: &Path) -> Result<CString, NulError> {
    CString::new(path.as_os_str().to_string_lossy().as_bytes())
}

pub fn string_from_c_chars(buffer: &[i8]) -> String {
    if buffer.is_empty() {
        return String::new();
    }

    unsafe {
        CStr::from_ptr(buffer.as_ptr())
            .to_string_lossy()
            .into_owned()
    }
}

pub unsafe fn string_from_c_ptr(ptr: *const i8) -> String {
    if ptr.is_null() {
        return String::new();
    }

    unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }
}

pub fn copy_string_to_c_chars<const N: usize>(buffer: &mut [i8; N], value: &str) {
    let bytes = value.as_bytes();
    let len = bytes.len().min(N.saturating_sub(1));
    for (slot, byte) in buffer.iter_mut().zip(bytes.iter().copied()).take(len) {
        *slot = byte as i8;
    }
}