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 seed: None,
331 };
332
333 let response = llm_client.generate(&request).await?;
334
335 if let Some(text) = response.as_str() {
337 Ok(text.trim().to_string())
338 } else if let Some(message) = response.get("message").and_then(|m| m.as_str()) {
339 Ok(message.to_string())
340 } else {
341 Ok(self.generate_template_message(issue))
342 }
343 }
344
345 fn generate_template_message(&self, issue: &ValidationIssue) -> String {
347 let field_str = issue.field.as_ref().map(|f| format!("Field '{}' ", f)).unwrap_or_default();
348
349 match issue.issue_type {
350 ValidationIssueType::Required => {
351 format!("{}is required", field_str)
352 }
353 ValidationIssueType::Format => {
354 format!("{}has an invalid format", field_str)
355 }
356 ValidationIssueType::MinLength => {
357 format!("{}is too short", field_str)
358 }
359 ValidationIssueType::MaxLength => {
360 format!("{}is too long", field_str)
361 }
362 ValidationIssueType::Pattern => {
363 format!("{}does not match the required pattern", field_str)
364 }
365 ValidationIssueType::Range => {
366 format!("{}is out of range", field_str)
367 }
368 ValidationIssueType::Type => {
369 format!("{}has an invalid type", field_str)
370 }
371 ValidationIssueType::Custom => issue.error_message.clone(),
372 }
373 }
374
375 async fn generate_custom_format_with_llm(
377 &self,
378 issue: &ValidationIssue,
379 message: &str,
380 context: &RequestContext,
381 ) -> Result<Value> {
382 let llm_client = self
383 .llm_client
384 .as_ref()
385 .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
386
387 let system_prompt = "You are an API error response generator. Generate realistic error responses in JSON format.";
388 let user_prompt = format!(
389 "Generate a validation error response for:\n\
390 Method: {}\n\
391 Path: {}\n\
392 Error: {:?}\n\
393 Message: {}\n\n\
394 Return a JSON object with error details. Use a realistic API error format.",
395 context.method, context.path, issue.issue_type, message
396 );
397
398 let request = LlmGenerationRequest {
399 system_prompt: system_prompt.to_string(),
400 user_prompt,
401 temperature: 0.5,
402 max_tokens: 300,
403 schema: None,
404 seed: None,
405 };
406
407 let response = llm_client.generate(&request).await?;
408
409 if let Some(obj) = response.as_object() {
411 Ok(Value::Object(obj.clone()))
412 } else {
413 Ok(serde_json::json!({
414 "error": {
415 "message": message,
416 "type": format!("{:?}", issue.issue_type)
417 }
418 }))
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use serde_json::json;
427
428 #[tokio::test]
429 async fn test_generate_template_message() {
430 let config = BehaviorModelConfig::default();
431 let generator = ValidationGenerator::new(config);
432
433 let issue = ValidationIssue {
434 field: Some("email".to_string()),
435 issue_type: ValidationIssueType::Required,
436 severity: ValidationSeverity::Error,
437 context: json!({}),
438 error_message: "".to_string(),
439 };
440
441 let message = generator.generate_template_message(&issue);
442 assert!(message.contains("email"));
443 assert!(message.contains("required"));
444 }
445
446 #[tokio::test]
447 async fn test_determine_error_format() {
448 let config = BehaviorModelConfig::default();
449 let generator = ValidationGenerator::new(config);
450
451 let field_issue = ValidationIssue {
452 field: Some("email".to_string()),
453 issue_type: ValidationIssueType::Required,
454 severity: ValidationSeverity::Error,
455 context: json!({}),
456 error_message: "".to_string(),
457 };
458
459 assert_eq!(generator.determine_error_format(&field_issue), ErrorFormat::FieldLevel);
460
461 let object_issue = ValidationIssue {
462 field: None,
463 issue_type: ValidationIssueType::Required,
464 severity: ValidationSeverity::Error,
465 context: json!({}),
466 error_message: "".to_string(),
467 };
468
469 assert_eq!(generator.determine_error_format(&object_issue), ErrorFormat::ObjectLevel);
470 }
471
472 #[tokio::test]
473 async fn test_generate_error_code() {
474 let config = BehaviorModelConfig::default();
475 let generator = ValidationGenerator::new(config);
476
477 let issue = ValidationIssue {
478 field: Some("email".to_string()),
479 issue_type: ValidationIssueType::Format,
480 severity: ValidationSeverity::Error,
481 context: json!({}),
482 error_message: "".to_string(),
483 };
484
485 let code = generator.generate_error_code(&issue);
486 assert_eq!(code, "INVALID_FORMAT");
487 }
488}