1use super::config::BehaviorModelConfig;
8use super::llm_client::LlmClient;
9use super::mutation_analyzer::{ValidationIssue, ValidationIssueType, ValidationSeverity};
10use super::types::LlmGenerationRequest;
11use mockforge_foundation::Result;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ValidationErrorExample {
19 pub field: Option<String>,
21 pub error_type: String,
23 pub message: String,
25 pub response: Value,
27 pub status_code: u16,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RequestContext {
34 pub method: String,
36 pub path: String,
38 pub request_body: Option<Value>,
40 #[serde(default)]
42 pub query_params: HashMap<String, String>,
43 #[serde(default)]
45 pub headers: HashMap<String, String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ValidationErrorResponse {
51 pub status_code: u16,
53 pub body: Value,
55 pub format: ErrorFormat,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61#[serde(rename_all = "lowercase")]
62pub enum ErrorFormat {
63 FieldLevel,
65 ObjectLevel,
67 Custom,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct FieldError {
74 pub field: String,
76 pub message: String,
78 pub code: Option<String>,
80 pub rejected_value: Option<Value>,
82}
83
84pub struct ValidationGenerator {
86 llm_client: Option<LlmClient>,
88 #[allow(dead_code)]
90 config: BehaviorModelConfig,
91 error_examples: Vec<ValidationErrorExample>,
93}
94
95impl ValidationGenerator {
96 pub fn new(config: BehaviorModelConfig) -> Self {
98 let llm_client = if config.llm_provider != "disabled" {
99 Some(LlmClient::new(config.clone()))
100 } else {
101 None
102 };
103
104 Self {
105 llm_client,
106 config,
107 error_examples: Vec::new(),
108 }
109 }
110
111 pub fn learn_from_example(&mut self, example: ValidationErrorExample) {
113 self.error_examples.push(example);
114 }
115
116 pub async fn generate_validation_error(
121 &self,
122 issue: &ValidationIssue,
123 context: &RequestContext,
124 ) -> Result<ValidationErrorResponse> {
125 let format = self.determine_error_format(issue);
127
128 let error_message = self.format_error_message(issue, context).await?;
130
131 let body = match format {
133 ErrorFormat::FieldLevel => {
134 self.build_field_level_error(issue, &error_message, context).await?
135 }
136 ErrorFormat::ObjectLevel => {
137 self.build_object_level_error(issue, &error_message, context).await?
138 }
139 ErrorFormat::Custom => self.build_custom_error(issue, &error_message, context).await?,
140 };
141
142 Ok(ValidationErrorResponse {
143 status_code: self.determine_status_code(issue),
144 body,
145 format,
146 })
147 }
148
149 pub async fn generate_field_error(
151 &self,
152 field: &str,
153 issue: &ValidationIssue,
154 context: &RequestContext,
155 ) -> Result<FieldError> {
156 let message = self.format_error_message(issue, context).await?;
157
158 let rejected_value =
160 context.request_body.as_ref().and_then(|body| body.get(field)).cloned();
161
162 Ok(FieldError {
163 field: field.to_string(),
164 message,
165 code: Some(self.generate_error_code(issue)),
166 rejected_value,
167 })
168 }
169
170 async fn format_error_message(
172 &self,
173 issue: &ValidationIssue,
174 _context: &RequestContext,
175 ) -> Result<String> {
176 if let Some(similar_example) = self.find_similar_example(issue, &self.error_examples) {
178 return Ok(similar_example.message.clone());
180 }
181
182 if let Some(ref _llm_client) = self.llm_client {
184 return self.generate_message_with_llm(issue).await;
185 }
186
187 Ok(self.generate_template_message(issue))
189 }
190
191 fn determine_error_format(&self, issue: &ValidationIssue) -> ErrorFormat {
195 if issue.field.is_some() {
197 return ErrorFormat::FieldLevel;
198 }
199
200 ErrorFormat::ObjectLevel
202 }
203
204 async fn build_field_level_error(
206 &self,
207 issue: &ValidationIssue,
208 message: &str,
209 _context: &RequestContext,
210 ) -> Result<Value> {
211 let field = issue.field.as_deref().unwrap_or("unknown");
212
213 Ok(serde_json::json!({
215 "error": {
216 "type": "validation_error",
217 "message": "Validation failed",
218 "fields": {
219 field: {
220 "message": message,
221 "code": self.generate_error_code(issue),
222 "type": format!("{:?}", issue.issue_type).to_lowercase()
223 }
224 }
225 }
226 }))
227 }
228
229 async fn build_object_level_error(
231 &self,
232 issue: &ValidationIssue,
233 message: &str,
234 _context: &RequestContext,
235 ) -> Result<Value> {
236 Ok(serde_json::json!({
237 "error": {
238 "type": "validation_error",
239 "message": message,
240 "code": self.generate_error_code(issue)
241 }
242 }))
243 }
244
245 async fn build_custom_error(
247 &self,
248 issue: &ValidationIssue,
249 message: &str,
250 context: &RequestContext,
251 ) -> Result<Value> {
252 if let Some(ref _llm_client) = self.llm_client {
254 return self.generate_custom_format_with_llm(issue, message, context).await;
255 }
256
257 self.build_object_level_error(issue, message, context).await
259 }
260
261 fn determine_status_code(&self, issue: &ValidationIssue) -> u16 {
263 match issue.severity {
264 ValidationSeverity::Critical | ValidationSeverity::Error => 400,
265 ValidationSeverity::Warning => 422,
266 ValidationSeverity::Info => 200, }
268 }
269
270 fn generate_error_code(&self, issue: &ValidationIssue) -> String {
272 match issue.issue_type {
273 ValidationIssueType::Required => "REQUIRED_FIELD".to_string(),
274 ValidationIssueType::Format => "INVALID_FORMAT".to_string(),
275 ValidationIssueType::MinLength => "MIN_LENGTH".to_string(),
276 ValidationIssueType::MaxLength => "MAX_LENGTH".to_string(),
277 ValidationIssueType::Pattern => "INVALID_PATTERN".to_string(),
278 ValidationIssueType::Range => "OUT_OF_RANGE".to_string(),
279 ValidationIssueType::Type => "INVALID_TYPE".to_string(),
280 ValidationIssueType::Custom => "VALIDATION_ERROR".to_string(),
281 }
282 }
283
284 fn find_similar_example<'a>(
286 &self,
287 issue: &ValidationIssue,
288 examples: &'a [ValidationErrorExample],
289 ) -> Option<&'a ValidationErrorExample> {
290 examples.iter().find(|ex| {
291 if let Some(ref field) = issue.field {
293 if let Some(ref ex_field) = ex.field {
294 if field == ex_field {
295 return true;
296 }
297 }
298 }
299
300 ex.error_type == format!("{:?}", issue.issue_type)
302 })
303 }
304
305 async fn generate_message_with_llm(&self, issue: &ValidationIssue) -> Result<String> {
307 let llm_client = self
308 .llm_client
309 .as_ref()
310 .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
311
312 let issue_type_str = format!("{:?}", issue.issue_type);
313 let field_str =
314 issue.field.as_ref().map(|f| format!(" for field '{}'", f)).unwrap_or_default();
315
316 let system_prompt = "You are an API error message generator. Generate clear, helpful validation error messages.";
317 let user_prompt = format!(
318 "Generate a validation error message{} for a {} error. \
319 The error should be clear, helpful, and professional. \
320 Return only the error message text, no additional formatting.",
321 field_str, issue_type_str
322 );
323
324 let request = LlmGenerationRequest {
325 system_prompt: system_prompt.to_string(),
326 user_prompt,
327 temperature: 0.3, max_tokens: 100,
329 schema: None,
330 };
331
332 let response = llm_client.generate(&request).await?;
333
334 if let Some(text) = response.as_str() {
336 Ok(text.trim().to_string())
337 } else if let Some(message) = response.get("message").and_then(|m| m.as_str()) {
338 Ok(message.to_string())
339 } else {
340 Ok(self.generate_template_message(issue))
341 }
342 }
343
344 fn generate_template_message(&self, issue: &ValidationIssue) -> String {
346 let field_str = issue.field.as_ref().map(|f| format!("Field '{}' ", f)).unwrap_or_default();
347
348 match issue.issue_type {
349 ValidationIssueType::Required => {
350 format!("{}is required", field_str)
351 }
352 ValidationIssueType::Format => {
353 format!("{}has an invalid format", field_str)
354 }
355 ValidationIssueType::MinLength => {
356 format!("{}is too short", field_str)
357 }
358 ValidationIssueType::MaxLength => {
359 format!("{}is too long", field_str)
360 }
361 ValidationIssueType::Pattern => {
362 format!("{}does not match the required pattern", field_str)
363 }
364 ValidationIssueType::Range => {
365 format!("{}is out of range", field_str)
366 }
367 ValidationIssueType::Type => {
368 format!("{}has an invalid type", field_str)
369 }
370 ValidationIssueType::Custom => issue.error_message.clone(),
371 }
372 }
373
374 async fn generate_custom_format_with_llm(
376 &self,
377 issue: &ValidationIssue,
378 message: &str,
379 context: &RequestContext,
380 ) -> Result<Value> {
381 let llm_client = self
382 .llm_client
383 .as_ref()
384 .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
385
386 let system_prompt = "You are an API error response generator. Generate realistic error responses in JSON format.";
387 let user_prompt = format!(
388 "Generate a validation error response for:\n\
389 Method: {}\n\
390 Path: {}\n\
391 Error: {:?}\n\
392 Message: {}\n\n\
393 Return a JSON object with error details. Use a realistic API error format.",
394 context.method, context.path, issue.issue_type, message
395 );
396
397 let request = LlmGenerationRequest {
398 system_prompt: system_prompt.to_string(),
399 user_prompt,
400 temperature: 0.5,
401 max_tokens: 300,
402 schema: None,
403 };
404
405 let response = llm_client.generate(&request).await?;
406
407 if let Some(obj) = response.as_object() {
409 Ok(Value::Object(obj.clone()))
410 } else {
411 Ok(serde_json::json!({
412 "error": {
413 "message": message,
414 "type": format!("{:?}", issue.issue_type)
415 }
416 }))
417 }
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use serde_json::json;
425
426 #[tokio::test]
427 async fn test_generate_template_message() {
428 let config = BehaviorModelConfig::default();
429 let generator = ValidationGenerator::new(config);
430
431 let issue = ValidationIssue {
432 field: Some("email".to_string()),
433 issue_type: ValidationIssueType::Required,
434 severity: ValidationSeverity::Error,
435 context: json!({}),
436 error_message: "".to_string(),
437 };
438
439 let message = generator.generate_template_message(&issue);
440 assert!(message.contains("email"));
441 assert!(message.contains("required"));
442 }
443
444 #[tokio::test]
445 async fn test_determine_error_format() {
446 let config = BehaviorModelConfig::default();
447 let generator = ValidationGenerator::new(config);
448
449 let field_issue = ValidationIssue {
450 field: Some("email".to_string()),
451 issue_type: ValidationIssueType::Required,
452 severity: ValidationSeverity::Error,
453 context: json!({}),
454 error_message: "".to_string(),
455 };
456
457 assert_eq!(generator.determine_error_format(&field_issue), ErrorFormat::FieldLevel);
458
459 let object_issue = ValidationIssue {
460 field: None,
461 issue_type: ValidationIssueType::Required,
462 severity: ValidationSeverity::Error,
463 context: json!({}),
464 error_message: "".to_string(),
465 };
466
467 assert_eq!(generator.determine_error_format(&object_issue), ErrorFormat::ObjectLevel);
468 }
469
470 #[tokio::test]
471 async fn test_generate_error_code() {
472 let config = BehaviorModelConfig::default();
473 let generator = ValidationGenerator::new(config);
474
475 let issue = ValidationIssue {
476 field: Some("email".to_string()),
477 issue_type: ValidationIssueType::Format,
478 severity: ValidationSeverity::Error,
479 context: json!({}),
480 error_message: "".to_string(),
481 };
482
483 let code = generator.generate_error_code(&issue);
484 assert_eq!(code, "INVALID_FORMAT");
485 }
486}