1use super::*;
2
3#[derive(Clone, Copy, Debug, Eq, IntoStaticStr, PartialEq)]
5#[strum(serialize_all = "snake_case", const_into_str)]
6pub enum McpValidationScope {
7 Form,
9 Field,
11 Element,
13 Filter,
15}
16
17impl McpValidationScope {
18 pub const fn as_str(self) -> &'static str {
19 self.into_str()
20 }
21}
22
23#[derive(Clone, Copy, Debug, Eq, IntoStaticStr, PartialEq)]
25#[strum(serialize_all = "snake_case", const_into_str)]
26pub enum McpValidationTarget {
27 Default,
29 Full,
31 Unwrapped,
33}
34
35impl McpValidationTarget {
36 pub const fn as_str(self) -> &'static str {
37 self.into_str()
38 }
39}
40
41#[derive(Clone, Copy, Debug, Eq, IntoStaticStr, PartialEq)]
43#[strum(serialize_all = "snake_case", const_into_str)]
44pub enum McpValidationTypeArgMode {
45 None,
47 Infer,
49 Explicit,
51}
52
53impl McpValidationTypeArgMode {
54 pub const fn as_str(self) -> &'static str {
55 self.into_str()
56 }
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub struct McpValidationParam {
62 name: &'static str,
63 literal: Option<&'static str>,
64 expr: Option<&'static str>,
65}
66
67impl McpValidationParam {
68 pub const fn literal(name: &'static str, literal: &'static str) -> Self {
70 Self {
71 name,
72 literal: Some(literal),
73 expr: None,
74 }
75 }
76
77 pub const fn expr(name: &'static str, expr: &'static str) -> Self {
79 Self {
80 name,
81 literal: None,
82 expr: Some(expr),
83 }
84 }
85
86 pub const fn name(self) -> &'static str {
88 self.name
89 }
90
91 pub const fn literal_value(self) -> Option<&'static str> {
93 self.literal
94 }
95
96 pub const fn expr_value(self) -> Option<&'static str> {
98 self.expr
99 }
100
101 pub fn to_value(self) -> Value {
102 let mut object = Map::new();
103 object.insert("name".to_string(), Value::String(self.name.to_string()));
104 if let Some(literal) = self.literal {
105 object.insert("value".to_string(), Value::String(literal.to_string()));
106 }
107 if let Some(expr) = self.expr {
108 object.insert("expr".to_string(), Value::String(expr.to_string()));
109 }
110 Value::Object(object)
111 }
112}
113
114pub const MCP_VALIDATION_PARAMS_NONE: &[McpValidationParam] = &[];
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub struct McpValidationRule {
120 scope: McpValidationScope,
121 validator: &'static str,
122 path: &'static str,
123 label: Option<&'static str>,
124 target: Option<McpValidationTarget>,
125 type_arg_mode: McpValidationTypeArgMode,
126 params: &'static [McpValidationParam],
127}
128
129impl McpValidationRule {
130 pub const fn new(
132 scope: McpValidationScope,
133 validator: &'static str,
134 path: &'static str,
135 label: Option<&'static str>,
136 type_arg_mode: McpValidationTypeArgMode,
137 params: &'static [McpValidationParam],
138 ) -> Self {
139 Self {
140 scope,
141 validator,
142 path,
143 label,
144 target: None,
145 type_arg_mode,
146 params,
147 }
148 }
149
150 pub const fn with_target(mut self, target: McpValidationTarget) -> Self {
152 self.target = Some(target);
153 self
154 }
155
156 pub const fn scope(self) -> McpValidationScope {
158 self.scope
159 }
160
161 pub const fn validator(self) -> &'static str {
163 self.validator
164 }
165
166 pub const fn path(self) -> &'static str {
168 self.path
169 }
170
171 pub const fn label(self) -> Option<&'static str> {
173 self.label
174 }
175
176 pub const fn target(self) -> Option<McpValidationTarget> {
178 self.target
179 }
180
181 pub const fn type_arg_mode(self) -> McpValidationTypeArgMode {
183 self.type_arg_mode
184 }
185
186 pub const fn params(self) -> &'static [McpValidationParam] {
188 self.params
189 }
190
191 pub fn to_value(self) -> Value {
192 let mut object = Map::new();
193 object.insert(
194 "scope".to_string(),
195 Value::String(self.scope.as_str().to_string()),
196 );
197 object.insert(
198 "validator".to_string(),
199 Value::String(self.validator.to_string()),
200 );
201 object.insert("path".to_string(), Value::String(self.path.to_string()));
202 if let Some(label) = self.label {
203 object.insert("label".to_string(), Value::String(label.to_string()));
204 }
205 if let Some(target) = self.target {
206 object.insert(
207 "target".to_string(),
208 Value::String(target.as_str().to_string()),
209 );
210 }
211 object.insert(
212 "type_arg_mode".to_string(),
213 Value::String(self.type_arg_mode.as_str().to_string()),
214 );
215 if !self.params.is_empty() {
216 object.insert(
217 "params".to_string(),
218 Value::Array(self.params.iter().map(|param| param.to_value()).collect()),
219 );
220 }
221 Value::Object(object)
222 }
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct McpValidationIssue {
228 field: Option<String>,
229 filter: Option<String>,
230 scope: McpValidationScope,
231 validator: Option<String>,
232 path: Option<String>,
233 label: Option<String>,
234 target: Option<McpValidationTarget>,
235 element_index: Option<usize>,
236 message: String,
237 params: Vec<Value>,
238}
239
240impl McpValidationIssue {
241 pub fn form(message: impl Into<String>) -> Self {
243 Self::custom(McpValidationScope::Form, message)
244 }
245
246 pub fn custom(scope: McpValidationScope, message: impl Into<String>) -> Self {
248 Self {
249 field: None,
250 filter: None,
251 scope,
252 validator: None,
253 path: None,
254 label: None,
255 target: None,
256 element_index: None,
257 message: message.into(),
258 params: Vec::new(),
259 }
260 }
261
262 pub fn required(field: impl AsRef<str>) -> Self {
264 let field = field.as_ref();
265 Self::custom(
266 McpValidationScope::Field,
267 format!("missing required field `{field}`"),
268 )
269 .with_field(field)
270 .with_validator("required")
271 }
272
273 pub fn for_rule(
275 field: impl AsRef<str>,
276 rule: McpValidationRule,
277 message: impl Into<String>,
278 ) -> Self {
279 Self::custom(rule.scope(), message)
280 .with_field(field)
281 .with_rule(rule)
282 }
283
284 pub fn for_filter_rule(
286 filter: impl Into<String>,
287 rule: McpValidationRule,
288 message: impl Into<String>,
289 ) -> Self {
290 Self::custom(rule.scope(), message)
291 .with_filter(filter)
292 .with_rule(rule)
293 }
294
295 fn with_rule(mut self, rule: McpValidationRule) -> Self {
296 self.validator = Some(rule.validator().to_string());
297 self.path = Some(rule.path().to_string());
298 self.label = rule.label().map(str::to_string);
299 self.target = rule.target();
300 self.params = rule.params().iter().map(|param| param.to_value()).collect();
301 self
302 }
303
304 pub fn with_element_index(mut self, element_index: usize) -> Self {
306 self.element_index = Some(element_index);
307 self
308 }
309
310 pub fn with_field(mut self, field: impl AsRef<str>) -> Self {
312 self.field = Some(field.as_ref().to_string());
313 self
314 }
315
316 pub fn with_filter(mut self, filter: impl Into<String>) -> Self {
318 self.filter = Some(filter.into());
319 self
320 }
321
322 pub fn with_validator(mut self, validator: impl Into<String>) -> Self {
324 self.validator = Some(validator.into());
325 self
326 }
327
328 pub fn with_label(mut self, label: impl Into<String>) -> Self {
330 self.label = Some(label.into());
331 self
332 }
333
334 pub fn field(&self) -> Option<&str> {
336 self.field.as_deref()
337 }
338
339 pub fn filter(&self) -> Option<&str> {
341 self.filter.as_deref()
342 }
343
344 pub fn message(&self) -> &str {
346 &self.message
347 }
348
349 pub fn to_value(&self) -> Value {
351 let mut object = Map::new();
352 object.insert(
353 "scope".to_string(),
354 Value::String(self.scope.as_str().to_string()),
355 );
356 object.insert("message".to_string(), Value::String(self.message.clone()));
357 if let Some(field) = &self.field {
358 object.insert("field".to_string(), Value::String(field.clone()));
359 }
360 if let Some(filter) = &self.filter {
361 object.insert("filter".to_string(), Value::String(filter.clone()));
362 }
363 if let Some(validator) = &self.validator {
364 object.insert("validator".to_string(), Value::String(validator.clone()));
365 }
366 if let Some(path) = &self.path {
367 object.insert("path".to_string(), Value::String(path.clone()));
368 }
369 if let Some(label) = &self.label {
370 object.insert("label".to_string(), Value::String(label.clone()));
371 }
372 if let Some(target) = self.target {
373 object.insert(
374 "target".to_string(),
375 Value::String(target.as_str().to_string()),
376 );
377 }
378 if let Some(element_index) = self.element_index {
379 object.insert(
380 "element_index".to_string(),
381 Value::Number((element_index as u64).into()),
382 );
383 }
384 if !self.params.is_empty() {
385 object.insert("params".to_string(), Value::Array(self.params.clone()));
386 }
387 Value::Object(object)
388 }
389}
390
391impl From<&koruma_core::ValidationIssue> for McpValidationIssue {
397 fn from(issue: &koruma_core::ValidationIssue) -> Self {
398 use koruma_core::ValidationIssueScope;
399
400 let scope = match issue.scope() {
401 ValidationIssueScope::Form => McpValidationScope::Form,
402 ValidationIssueScope::Field => McpValidationScope::Field,
403 ValidationIssueScope::Element => McpValidationScope::Element,
404 };
405 let mut result = Self::custom(scope, issue.message());
406 result.field = issue.field_name_str().map(str::to_owned);
407 result.validator = issue.validator().map(str::to_owned);
408 result.label = issue.label().map(str::to_owned);
409 result.element_index = issue.element_index();
410 result.params = issue.params().iter().map(koruma_param_value).collect();
411 result
412 }
413}
414
415fn koruma_param_value(param: &koruma_core::ValidatorParam) -> Value {
416 use koruma_core::ValidatorParamValue;
417
418 let value = match param.value() {
419 ValidatorParamValue::Bool(value) => json!(value),
420 ValidatorParamValue::I64(value) => json!(value),
421 ValidatorParamValue::U64(value) => json!(value),
422 ValidatorParamValue::F64(value) if value.is_finite() => json!(value),
423 ValidatorParamValue::F64(value) => json!({ "kind": "f64", "value": value.to_string() }),
424 ValidatorParamValue::String(value) => json!(value),
425 ValidatorParamValue::None => Value::Null,
426 ValidatorParamValue::Opaque { type_name } => {
427 json!({ "kind": "opaque", "type_name": type_name })
428 },
429 };
430 json!({ "name": param.name(), "value": value })
431}
432
433pub fn koruma_validation_error<Error>(error: &Error) -> McpToolError
438where
439 Error: koruma_core::ValidationIssues + ?Sized,
440{
441 let mut issues: Vec<_> = error
442 .issues()
443 .iter()
444 .map(McpValidationIssue::from)
445 .collect();
446 if issues.is_empty() {
447 issues.push(McpValidationIssue::form("domain validation failed"));
448 }
449 validation_issues_error(issues)
450}
451
452pub fn validate_koruma<Input>(input: &Input) -> Result<(), McpToolError>
461where
462 Input: koruma_core::ValidateExt + ?Sized,
463 Input::Error: koruma_core::ValidationIssues,
464{
465 input
466 .validate()
467 .map_err(|error| koruma_validation_error(&error))
468}
469
470pub fn validation_issues_error(issues: Vec<McpValidationIssue>) -> McpToolError {
472 let message = issues
473 .iter()
474 .map(McpValidationIssue::message)
475 .collect::<Vec<_>>()
476 .join("; ");
477 McpToolError::validation_structured_details(
478 message,
479 issues.into_iter().map(|issue| issue.to_value()),
480 )
481}