clipmem 0.4.0

macOS clipboard memory backed by SQLite and searchable from agent runtimes
Documentation
use super::*;

pub(super) fn parse_normalized_score(value: &str) -> Result<f64, LimitParseError> {
    let parsed = value
        .parse::<f64>()
        .map_err(|_| LimitParseError(format!("invalid floating-point value '{value}'")))?;

    if (0.0..=1.0).contains(&parsed) {
        Ok(parsed)
    } else {
        Err(LimitParseError(format!(
            "value must be between 0.0 and 1.0, got {parsed}"
        )))
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct LimitParseError(String);

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

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

pub(super) fn parse_bounded_limit(value: &str) -> Result<usize, LimitParseError> {
    let parsed = value
        .parse::<usize>()
        .map_err(|_| LimitParseError(format!("invalid integer value '{value}'")))?;

    if (1..=250).contains(&parsed) {
        Ok(parsed)
    } else {
        Err(LimitParseError(format!(
            "value must be between 1 and 250, got {parsed}"
        )))
    }
}

pub(super) fn parse_rfc3339_timestamp(value: &str) -> Result<String, LimitParseError> {
    OffsetDateTime::parse(value, &Rfc3339)
        .map_err(|_| LimitParseError(format!("invalid RFC3339 timestamp '{value}'")))
        .and_then(|timestamp| {
            timestamp
                .format(&Rfc3339)
                .map_err(|error| LimitParseError(format!("format timestamp '{value}': {error}")))
        })
}

pub(super) fn parse_nonnegative_bytes(value: &str) -> Result<usize, LimitParseError> {
    value
        .parse::<usize>()
        .map_err(|_| LimitParseError(format!("invalid integer value '{value}'")))
}

pub(super) fn parse_duration_value(value: &str) -> Result<DurationValue, LimitParseError> {
    let trimmed = value.trim();
    if trimmed.len() < 2 {
        return Err(LimitParseError(format!(
            "invalid duration '{value}'; expected <integer><unit> like 30d, 12h, or 15m"
        )));
    }

    let (amount, unit) = trimmed.split_at(trimmed.len() - 1);
    let amount = amount.parse::<u64>().map_err(|_| {
        LimitParseError(format!(
            "invalid duration '{value}'; expected an integer amount before the unit"
        ))
    })?;
    if amount == 0 {
        return Err(LimitParseError(
            "duration must be greater than zero".to_string(),
        ));
    }

    let seconds = match unit.to_ascii_lowercase().as_str() {
        "d" => amount.saturating_mul(24 * 60 * 60),
        "h" => amount.saturating_mul(60 * 60),
        "m" => amount.saturating_mul(60),
        _ => {
            return Err(LimitParseError(format!(
                "invalid duration unit '{unit}'; expected d, h, or m"
            )))
        }
    };

    if seconds == u64::MAX {
        return Err(LimitParseError(format!(
            "duration '{value}' exceeds supported range"
        )));
    }

    Ok(DurationValue::new(trimmed.to_string(), seconds))
}

pub(super) fn parse_retention_value(value: &str) -> Result<RetentionValue, LimitParseError> {
    if value.trim().eq_ignore_ascii_case("forever") {
        Ok(RetentionValue::Forever)
    } else {
        parse_duration_value(value).map(RetentionValue::Duration)
    }
}