nexara_core/
validation.rs1use 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}