Skip to main content

nexara_core/
validation.rs

1use crate::error::{NexaraError, NexaraResult};
2use crate::tool::{ToolCapability, ToolDescriptor};
3
4pub fn validate_tool_descriptor(descriptor: &ToolDescriptor) -> NexaraResult<()> {
5    validate_tool_name(&descriptor.name)?;
6    if descriptor.description.trim().is_empty() {
7        return Err(NexaraError::InvalidDescriptor(
8            "description must not be empty".to_string(),
9        ));
10    }
11    for scope in &descriptor.scopes {
12        validate_scope_name(scope)?;
13    }
14    for capability in &descriptor.capabilities {
15        validate_tool_capability(capability)?;
16    }
17    Ok(())
18}
19
20pub fn validate_tool_capability(capability: &ToolCapability) -> NexaraResult<()> {
21    validate_capability_id(&capability.id)?;
22    if capability.resource.trim().is_empty() {
23        return Err(NexaraError::InvalidDescriptor(
24            "capability resource must not be empty".to_string(),
25        ));
26    }
27    if capability.operation.trim().is_empty() {
28        return Err(NexaraError::InvalidDescriptor(
29            "capability operation must not be empty".to_string(),
30        ));
31    }
32    let mut segments = capability.id.split('.').collect::<Vec<_>>();
33    if segments.len() < 2 {
34        return Err(NexaraError::InvalidDescriptor(format!(
35            "capability id '{}' must include resource and operation",
36            capability.id
37        )));
38    }
39    let operation = segments.pop().unwrap_or_default();
40    if operation != capability.operation {
41        return Err(NexaraError::InvalidDescriptor(format!(
42            "capability id '{}' operation does not match '{}'",
43            capability.id, capability.operation
44        )));
45    }
46    if capability.resource_path.is_empty() {
47        return Err(NexaraError::InvalidDescriptor(format!(
48            "capability '{}' resource_path must not be empty",
49            capability.id
50        )));
51    }
52    if capability.resource_path.join(".") != segments.join(".") {
53        return Err(NexaraError::InvalidDescriptor(format!(
54            "capability '{}' resource_path must match id resource path",
55            capability.id
56        )));
57    }
58    if segments.len() > 1
59        && (capability
60            .display_name
61            .as_deref()
62            .unwrap_or_default()
63            .trim()
64            .is_empty()
65            || capability
66                .description
67                .as_deref()
68                .unwrap_or_default()
69                .trim()
70                .is_empty())
71    {
72        return Err(NexaraError::InvalidDescriptor(format!(
73            "capability '{}' requires display_name and description for deeper resource paths",
74            capability.id
75        )));
76    }
77    Ok(())
78}
79
80pub fn validate_capability_id(id: &str) -> NexaraResult<()> {
81    let trimmed = id.trim();
82    if trimmed.is_empty() {
83        return Err(NexaraError::InvalidDescriptor(
84            "capability id must not be empty".to_string(),
85        ));
86    }
87    if !trimmed.chars().all(|ch| {
88        ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-' || ch == '.'
89    }) {
90        return Err(NexaraError::InvalidDescriptor(format!(
91            "invalid capability id '{id}'"
92        )));
93    }
94    Ok(())
95}
96
97pub fn validate_tool_name(name: &str) -> NexaraResult<()> {
98    let trimmed = name.trim();
99    if trimmed.is_empty() {
100        return Err(NexaraError::InvalidDescriptor(
101            "tool name must not be empty".to_string(),
102        ));
103    }
104    if !trimmed
105        .chars()
106        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.')
107    {
108        return Err(NexaraError::InvalidDescriptor(format!(
109            "invalid tool name '{name}'"
110        )));
111    }
112    Ok(())
113}
114
115pub fn validate_scope_name(scope: &str) -> NexaraResult<()> {
116    let trimmed = scope.trim();
117    if trimmed.is_empty() {
118        return Err(NexaraError::InvalidDescriptor(
119            "scope must not be empty".to_string(),
120        ));
121    }
122    if !trimmed
123        .chars()
124        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == ':' || ch == '.')
125    {
126        return Err(NexaraError::InvalidDescriptor(format!(
127            "invalid scope '{scope}'"
128        )));
129    }
130    Ok(())
131}