anp 0.7.0

Rust SDK for Agent Network Protocol (ANP)
Documentation
use regex::Regex;

use super::errors::{HandleValidationError, WbaUriParseError};
use super::models::ParsedWbaUri;

pub fn validate_local_part(local_part: &str) -> bool {
    let normalized = local_part.to_ascii_lowercase();
    if normalized.is_empty() || normalized.len() > 63 {
        return false;
    }
    if normalized.starts_with('-') || normalized.ends_with('-') {
        return false;
    }
    if normalized.contains("--") {
        return false;
    }
    normalized
        .chars()
        .all(|value| value.is_ascii_lowercase() || value.is_ascii_digit() || value == '-')
}

pub fn validate_handle(handle: &str) -> Result<(String, String), HandleValidationError> {
    let normalized = handle.trim().to_ascii_lowercase();
    if normalized.is_empty() {
        return Err(HandleValidationError {
            message: "Handle must not be empty".to_string(),
            status_code: 400,
        });
    }
    let dot_index = normalized.find('.').ok_or(HandleValidationError {
        message: format!("Handle must contain at least one dot: '{}'", handle),
        status_code: 400,
    })?;
    let local_part = normalized[..dot_index].to_string();
    let domain = normalized[dot_index + 1..].to_string();
    if local_part.is_empty() {
        return Err(HandleValidationError {
            message: format!("Handle local-part is empty: '{}'", handle),
            status_code: 400,
        });
    }
    if domain.is_empty() {
        return Err(HandleValidationError {
            message: format!("Handle domain is empty: '{}'", handle),
            status_code: 400,
        });
    }
    if !validate_local_part(&local_part) {
        return Err(HandleValidationError {
            message: format!(
                "Invalid local-part '{}': must be 1-63 chars of a-z, 0-9, hyphen; must start/end with alnum; no consecutive hyphens",
                local_part
            ),
            status_code: 400,
        });
    }
    if !is_valid_domain(&domain) {
        return Err(HandleValidationError {
            message: format!("Invalid domain '{}'", domain),
            status_code: 400,
        });
    }
    Ok((local_part, domain))
}

pub fn normalize_handle(handle: &str) -> Result<String, HandleValidationError> {
    let (local_part, domain) = validate_handle(handle)?;
    Ok(format!("{}.{}", local_part, domain))
}

pub fn parse_wba_uri(uri: &str) -> Result<ParsedWbaUri, WbaUriParseError> {
    if !uri.starts_with("wba://") {
        return Err(WbaUriParseError {
            message: format!("URI must start with 'wba://': '{}'", uri),
            status_code: 400,
        });
    }
    let handle_part = &uri[6..];
    if handle_part.is_empty() {
        return Err(WbaUriParseError {
            message: format!("URI contains no handle after 'wba://': '{}'", uri),
            status_code: 400,
        });
    }
    let (local_part, domain) = validate_handle(handle_part).map_err(|err| WbaUriParseError {
        message: format!("Invalid handle in URI '{}': {}", uri, err.message),
        status_code: 400,
    })?;
    Ok(ParsedWbaUri {
        local_part: local_part.clone(),
        domain: domain.clone(),
        handle: format!("{}.{}", local_part, domain),
        original_uri: uri.to_string(),
    })
}

pub fn build_resolution_url(local_part: &str, domain: &str) -> String {
    format!("https://{}/.well-known/handle/{}", domain, local_part)
}

pub fn build_wba_uri(local_part: &str, domain: &str) -> String {
    format!("wba://{}.{}", local_part, domain)
}

fn is_valid_domain(domain: &str) -> bool {
    let label_re = Regex::new(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
        .expect("domain regex must compile");
    let labels = domain.split('.').collect::<Vec<&str>>();
    if labels.len() < 2 {
        return false;
    }
    labels.iter().all(|label| label_re.is_match(label))
}