nu_plugin_nw_ulid 0.2.0

Production-grade ULID (Universally Unique Lexicographically Sortable Identifier) utilities plugin for Nushell with cryptographically secure operations, enterprise-grade security, and streaming support
Documentation
//! Core ULID engine providing all ULID operations for the plugin.

use std::str::FromStr;

use nu_protocol::{Record, Span, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;

/// Length of a ULID string in Crockford Base32 encoding.
pub const ULID_STRING_LENGTH: usize = 26;

/// Maximum number of ULIDs in a single bulk generation request.
pub const MAX_BULK_GENERATION: usize = 10_000;

/// Valid characters for Crockford Base32 encoding used by ULIDs.
pub const CROCKFORD_BASE32_CHARSET: &str = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

/// Nanoseconds per millisecond, used for timestamp conversions.
pub const NANOS_PER_MILLI: u64 = 1_000_000;

/// Milliseconds per second, used for timestamp conversions.
pub const MS_PER_SECOND: u64 = 1_000;

/// Number of Crockford Base32 characters encoding the timestamp portion of a ULID.
pub const ULID_TIMESTAMP_CHARS: usize = 10;

/// Number of Crockford Base32 characters encoding the randomness portion of a ULID.
pub const ULID_RANDOMNESS_CHARS: usize = 16;

/// Bitmask for the 80-bit randomness component of a ULID.
const ULID_RANDOMNESS_MASK: u128 = 0xFFFF_FFFF_FFFF_FFFF_FFFF;

/// Core ULID engine providing all ULID operations for the plugin.
pub struct UlidEngine;

/// Parsed components of a ULID.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UlidComponents {
    /// The original ULID string.
    pub ulid: String,
    /// Millisecond timestamp extracted from the ULID.
    pub timestamp_ms: u64,
    /// Hexadecimal representation of the randomness component.
    pub randomness_hex: String,
    /// Whether the ULID passed validation.
    pub valid: bool,
}

impl UlidEngine {
    /// Generates a single ULID.
    pub fn generate() -> Result<Ulid, UlidError> {
        Ok(Ulid::new())
    }

    /// Generates a ULID with a specific timestamp.
    pub fn generate_with_timestamp(timestamp_ms: u64) -> Result<Ulid, UlidError> {
        let ulid = Ulid::from_parts(timestamp_ms, rand::random::<u128>() & ULID_RANDOMNESS_MASK);
        Ok(ulid)
    }

    /// Generates multiple ULIDs efficiently.
    pub fn generate_bulk(count: usize) -> Result<Vec<Ulid>, UlidError> {
        if count == 0 {
            return Ok(Vec::new());
        }

        if count > MAX_BULK_GENERATION {
            return Err(UlidError::InvalidInput {
                message: "Bulk generation limited to 10,000 ULIDs per request for performance"
                    .to_string(),
            });
        }

        let mut result = Vec::with_capacity(count);
        for _ in 0..count {
            result.push(Ulid::new());
        }
        Ok(result)
    }

    /// Parses a ULID string into components.
    pub fn parse(ulid_str: &str) -> Result<UlidComponents, UlidError> {
        match Ulid::from_str(ulid_str) {
            Ok(ulid) => {
                let components = UlidComponents {
                    ulid: ulid_str.to_string(),
                    timestamp_ms: ulid.timestamp_ms(),
                    randomness_hex: format!("{:x}", ulid.random()),
                    valid: true,
                };
                Ok(components)
            }
            Err(e) => Err(UlidError::InvalidFormat {
                input: ulid_str.to_string(),
                reason: format!("Parse error: {}", e),
            }),
        }
    }

    /// Returns `true` if the string is a valid ULID.
    #[must_use]
    pub fn validate(ulid_str: &str) -> bool {
        Ulid::from_str(ulid_str).is_ok()
    }

    /// Extracts the timestamp from a ULID.
    pub fn extract_timestamp(ulid_str: &str) -> Result<u64, UlidError> {
        match Ulid::from_str(ulid_str) {
            Ok(ulid) => Ok(ulid.timestamp_ms()),
            Err(e) => Err(UlidError::InvalidFormat {
                input: ulid_str.to_string(),
                reason: format!("Cannot extract timestamp: {}", e),
            }),
        }
    }

    /// Extracts the randomness component from a ULID.
    pub fn extract_randomness(ulid_str: &str) -> Result<u128, UlidError> {
        match Ulid::from_str(ulid_str) {
            Ok(ulid) => Ok(ulid.random()),
            Err(e) => Err(UlidError::InvalidFormat {
                input: ulid_str.to_string(),
                reason: format!("Cannot extract randomness: {}", e),
            }),
        }
    }

    /// Converts a ULID to its native 16-byte binary representation.
    pub fn to_bytes(ulid: &Ulid) -> Vec<u8> {
        ulid.to_bytes().to_vec()
    }

    /// Converts `UlidComponents` to a Nushell `Value`.
    pub fn components_to_value(components: &UlidComponents, span: Span) -> Value {
        let mut record = Record::new();

        record.push("ulid", Value::string(components.ulid.clone(), span));

        let mut timestamp_record = Record::new();
        timestamp_record.push("ms", Value::int(components.timestamp_ms as i64, span));

        // Convert timestamp to ISO8601 format
        let timestamp_secs = components.timestamp_ms / MS_PER_SECOND;
        let timestamp_nanos = (components.timestamp_ms % MS_PER_SECOND) * NANOS_PER_MILLI;

        if let Some(datetime) =
            chrono::DateTime::from_timestamp(timestamp_secs as i64, timestamp_nanos as u32)
        {
            timestamp_record.push(
                "iso8601",
                Value::string(datetime.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), span),
            );
            timestamp_record.push("unix", Value::int(timestamp_secs as i64, span));
        }

        record.push("timestamp", Value::record(timestamp_record, span));

        let mut randomness_record = Record::new();
        randomness_record.push(
            "hex",
            Value::string(components.randomness_hex.clone(), span),
        );

        record.push("randomness", Value::record(randomness_record, span));

        record.push("valid", Value::bool(components.valid, span));

        Value::record(record, span)
    }
}

/// Errors produced by ULID operations.
#[derive(Debug, Clone)]
pub enum UlidError {
    /// The input string is not a valid ULID.
    InvalidFormat {
        /// The input that failed validation.
        input: String,
        /// Human-readable reason for the failure.
        reason: String,
    },
    /// A general input validation error.
    InvalidInput {
        /// Description of the problem.
        message: String,
    },
    /// The timestamp exceeds the maximum representable value.
    TimestampOutOfRange {
        /// The provided timestamp.
        timestamp: u64,
        /// The maximum allowed timestamp.
        max_timestamp: u64,
    },
    /// ULID generation failed.
    GenerationError {
        /// Human-readable reason for the failure.
        reason: String,
    },
}

impl std::fmt::Display for UlidError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            UlidError::InvalidFormat { input, reason } => {
                write!(f, "Invalid ULID format '{}': {}", input, reason)
            }
            UlidError::InvalidInput { message } => {
                write!(f, "Invalid input: {}", message)
            }
            UlidError::TimestampOutOfRange {
                timestamp,
                max_timestamp,
            } => {
                write!(
                    f,
                    "Timestamp {} is out of range (max: {})",
                    timestamp, max_timestamp
                )
            }
            UlidError::GenerationError { reason } => {
                write!(f, "ULID generation error: {}", reason)
            }
        }
    }
}

impl std::error::Error for UlidError {}

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

    #[test]
    fn test_ulid_generation() {
        let ulid = UlidEngine::generate().unwrap();
        assert_eq!(ulid.to_string().len(), ULID_STRING_LENGTH);
    }

    #[test]
    fn test_ulid_validation() {
        // Valid ULID
        assert!(UlidEngine::validate("01AN4Z07BY79KA1307SR9X4MV3"));

        // Invalid ULIDs
        assert!(!UlidEngine::validate("invalid"));
        assert!(!UlidEngine::validate("01AN4Z07BY79KA1307SR9X4MV")); // Too short
        assert!(!UlidEngine::validate("01AN4Z07BY79KA1307SR9X4MV34")); // Too long
    }

    #[test]
    fn test_ulid_parsing() {
        let ulid_str = "01AN4Z07BY79KA1307SR9X4MV3";
        let components = UlidEngine::parse(ulid_str).unwrap();

        assert_eq!(components.ulid, ulid_str);
        assert!(components.valid);
        // The actual timestamp for this ULID
        assert_eq!(components.timestamp_ms, 1465824320894);
    }

    #[test]
    fn test_bulk_generation() {
        let ulids = UlidEngine::generate_bulk(10).unwrap();
        assert_eq!(ulids.len(), 10);

        // All should be unique
        let unique_count = ulids
            .iter()
            .map(|u| u.to_string())
            .collect::<std::collections::HashSet<_>>()
            .len();
        assert_eq!(unique_count, 10);
    }

    #[test]
    fn test_timestamp_extraction() {
        let ulid_str = "01AN4Z07BY79KA1307SR9X4MV3";
        let timestamp = UlidEngine::extract_timestamp(ulid_str).unwrap();
        assert_eq!(timestamp, 1465824320894);
    }

    #[test]
    fn test_bulk_generation_limit() {
        let result = UlidEngine::generate_bulk(10_001);
        assert!(result.is_err());

        if let Err(UlidError::InvalidInput { message }) = result {
            assert!(message.contains("10,000"));
        }
    }
}