nexara-core 0.1.1

Core types, policy, registry, broker, and audit schema for Nexara
Documentation
use crate::error::{NexaraError, NexaraResult};
use crate::tool::{ToolCapability, ToolDescriptor};

pub fn validate_tool_descriptor(descriptor: &ToolDescriptor) -> NexaraResult<()> {
    validate_tool_name(&descriptor.name)?;
    if descriptor.description.trim().is_empty() {
        return Err(NexaraError::InvalidDescriptor(
            "description must not be empty".to_string(),
        ));
    }
    for scope in &descriptor.scopes {
        validate_scope_name(scope)?;
    }
    for capability in &descriptor.capabilities {
        validate_tool_capability(capability)?;
    }
    Ok(())
}

pub fn validate_tool_capability(capability: &ToolCapability) -> NexaraResult<()> {
    validate_capability_id(&capability.id)?;
    if capability.resource.trim().is_empty() {
        return Err(NexaraError::InvalidDescriptor(
            "capability resource must not be empty".to_string(),
        ));
    }
    if capability.operation.trim().is_empty() {
        return Err(NexaraError::InvalidDescriptor(
            "capability operation must not be empty".to_string(),
        ));
    }
    let mut segments = capability.id.split('.').collect::<Vec<_>>();
    if segments.len() < 2 {
        return Err(NexaraError::InvalidDescriptor(format!(
            "capability id '{}' must include resource and operation",
            capability.id
        )));
    }
    let operation = segments.pop().unwrap_or_default();
    if operation != capability.operation {
        return Err(NexaraError::InvalidDescriptor(format!(
            "capability id '{}' operation does not match '{}'",
            capability.id, capability.operation
        )));
    }
    if capability.resource_path.is_empty() {
        return Err(NexaraError::InvalidDescriptor(format!(
            "capability '{}' resource_path must not be empty",
            capability.id
        )));
    }
    if capability.resource_path.join(".") != segments.join(".") {
        return Err(NexaraError::InvalidDescriptor(format!(
            "capability '{}' resource_path must match id resource path",
            capability.id
        )));
    }
    if segments.len() > 1
        && (capability
            .display_name
            .as_deref()
            .unwrap_or_default()
            .trim()
            .is_empty()
            || capability
                .description
                .as_deref()
                .unwrap_or_default()
                .trim()
                .is_empty())
    {
        return Err(NexaraError::InvalidDescriptor(format!(
            "capability '{}' requires display_name and description for deeper resource paths",
            capability.id
        )));
    }
    Ok(())
}

pub fn validate_capability_id(id: &str) -> NexaraResult<()> {
    let trimmed = id.trim();
    if trimmed.is_empty() {
        return Err(NexaraError::InvalidDescriptor(
            "capability id must not be empty".to_string(),
        ));
    }
    if !trimmed.chars().all(|ch| {
        ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-' || ch == '.'
    }) {
        return Err(NexaraError::InvalidDescriptor(format!(
            "invalid capability id '{id}'"
        )));
    }
    Ok(())
}

pub fn validate_tool_name(name: &str) -> NexaraResult<()> {
    let trimmed = name.trim();
    if trimmed.is_empty() {
        return Err(NexaraError::InvalidDescriptor(
            "tool name must not be empty".to_string(),
        ));
    }
    if !trimmed
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.')
    {
        return Err(NexaraError::InvalidDescriptor(format!(
            "invalid tool name '{name}'"
        )));
    }
    Ok(())
}

pub fn validate_scope_name(scope: &str) -> NexaraResult<()> {
    let trimmed = scope.trim();
    if trimmed.is_empty() {
        return Err(NexaraError::InvalidDescriptor(
            "scope must not be empty".to_string(),
        ));
    }
    if !trimmed
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == ':' || ch == '.')
    {
        return Err(NexaraError::InvalidDescriptor(format!(
            "invalid scope '{scope}'"
        )));
    }
    Ok(())
}