n5i-apps 0.12.0-dev.1

Utils for working with n5i apps
// SPDX-FileCopyrightText: 2024-2026 The n5i Project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use serde::{Deserialize, Serialize};

pub use n5i::utils::{is_false, true_default};

/// A type that can be serialized into a string, but can also be various other types
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(untagged)]
pub enum StringLike {
    String(String),
    Int(i64),
    Bool(bool),
    Float(f64),
}

impl From<StringLike> for String {
    fn from(s: StringLike) -> Self {
        match s {
            StringLike::String(s) => s,
            StringLike::Int(i) => i.to_string(),
            StringLike::Bool(b) => b.to_string(),
            StringLike::Float(f) => f.to_string(),
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(untagged)]
pub enum StringOrNumber {
    String(String),
    Int(i64),
    Float(f64),
}

#[must_use]
pub fn is_valid_name(s: &str) -> bool {
    #[inline]
    const fn alnum(c: u8) -> bool {
        c.is_ascii_lowercase() || c.is_ascii_digit()
    }

    let b = s.as_bytes();
    let n = b.len();

    if n == 0 {
        return false;
    }

    if !alnum(b[0]) {
        return false;
    }

    let mut prev_dash = false;

    for &c in &b[1..] {
        if c == b'-' {
            if prev_dash {
                return false;
            }
            prev_dash = true;
        } else if alnum(c) {
            prev_dash = false;
        } else {
            return false;
        }
    }

    !prev_dash
}

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

    #[test]
    fn test_is_false() {
        assert!(is_false(&false));
        assert!(!is_false(&true));
    }

    #[test]
    fn test_stringlike() {
        assert_eq!(
            Into::<String>::into(StringLike::String("Hello world".to_string())),
            "Hello world".to_string()
        );
        assert_eq!(
            Into::<String>::into(StringLike::Int(100)),
            "100".to_string()
        );
        assert_eq!(
            Into::<String>::into(StringLike::Bool(true)),
            "true".to_string()
        );
        assert_eq!(
            Into::<String>::into(StringLike::Float(1.23)),
            "1.23".to_string()
        );
    }

    #[test]
    fn test_is_valid_name() {
        assert!(is_valid_name("hello-world"));
        assert!(is_valid_name("hello"));
        assert!(is_valid_name("h"));
        assert!(is_valid_name("hello123"));
        assert!(!is_valid_name("-hello"));
        assert!(!is_valid_name("hello-"));
        assert!(!is_valid_name("hello_world"));
        assert!(!is_valid_name("Hello"));
        assert!(!is_valid_name(""));
        assert!(!is_valid_name("hello--world"));
        assert!(!is_valid_name("hello-wörld"));
    }
}