1use super::config::BehaviorModelConfig;
8use super::llm_client::LlmClient;
9use super::types::LlmGenerationRequest;
10use mockforge_foundation::Result;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "lowercase")]
17pub enum SuggestionInput {
18 Endpoint {
20 method: String,
22 path: String,
24 request: Option<Value>,
26 response: Option<Value>,
28 description: Option<String>,
30 },
31 Description {
33 text: String,
35 },
36 PartialSpec {
38 spec: Value,
40 },
41 Paths {
43 paths: Vec<String>,
45 },
46}
47
48#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "lowercase")]
51pub enum OutputFormat {
52 OpenAPI,
54 MockForge,
56 Both,
58}
59
60impl std::str::FromStr for OutputFormat {
61 type Err = String;
62
63 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
64 match s.to_lowercase().as_str() {
65 "openapi" => Ok(Self::OpenAPI),
66 "mockforge" => Ok(Self::MockForge),
67 "both" => Ok(Self::Both),
68 _ => Err(format!("Invalid output format: {}", s)),
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct SuggestionConfig {
76 pub llm_config: BehaviorModelConfig,
78 pub output_format: OutputFormat,
80 pub num_suggestions: usize,
82 pub include_examples: bool,
84 pub domain_hint: Option<String>,
86}
87
88impl Default for SuggestionConfig {
89 fn default() -> Self {
90 Self {
91 llm_config: BehaviorModelConfig::default(),
92 output_format: OutputFormat::OpenAPI,
93 num_suggestions: 5,
94 include_examples: true,
95 domain_hint: None,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SuggestionResult {
103 pub openapi_spec: Option<Value>,
105 pub mockforge_config: Option<Value>,
107 pub suggestions: Vec<EndpointSuggestion>,
109 pub metadata: SuggestionMetadata,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct EndpointSuggestion {
116 pub method: String,
118 pub path: String,
120 pub description: String,
122 pub parameters: Vec<ParameterInfo>,
124 pub response_schema: Option<Value>,
126 pub reasoning: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ParameterInfo {
133 pub name: String,
135 pub location: String,
137 pub data_type: String,
139 pub required: bool,
141 pub description: Option<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct SuggestionMetadata {
148 pub endpoint_count: usize,
150 pub detected_domain: Option<String>,
152 pub timestamp: String,
154 pub model: String,
156}
157
158pub struct SpecSuggestionEngine {
160 llm_client: LlmClient,
162 config: SuggestionConfig,
164}
165
166impl SpecSuggestionEngine {
167 pub fn new(config: SuggestionConfig) -> Self {
169 let llm_client = LlmClient::new(config.llm_config.clone());
170 Self { llm_client, config }
171 }
172
173 pub async fn suggest(&self, input: &SuggestionInput) -> Result<SuggestionResult> {
175 let (system_prompt, user_prompt) = self.build_prompts(input)?;
177
178 let request = LlmGenerationRequest {
180 system_prompt,
181 user_prompt,
182 temperature: 0.7,
183 max_tokens: 4000,
184 schema: None,
185 };
186
187 let llm_response = self.llm_client.generate(&request).await?;
188
189 self.parse_llm_response(llm_response, input).await
191 }
192
193 fn build_prompts(&self, input: &SuggestionInput) -> Result<(String, String)> {
195 let system_prompt = self.build_system_prompt();
196 let user_prompt = match input {
197 SuggestionInput::Endpoint {
198 method,
199 path,
200 request,
201 response,
202 description,
203 } => self.build_endpoint_prompt(method, path, request, response, description),
204 SuggestionInput::Description { text } => self.build_description_prompt(text),
205 SuggestionInput::PartialSpec { spec } => self.build_partial_spec_prompt(spec),
206 SuggestionInput::Paths { paths } => self.build_paths_prompt(paths),
207 };
208
209 Ok((system_prompt, user_prompt))
210 }
211
212 fn build_system_prompt(&self) -> String {
214 let format_desc = match self.config.output_format {
215 OutputFormat::OpenAPI => "OpenAPI 3.0 specification",
216 OutputFormat::MockForge => "MockForge YAML configuration",
217 OutputFormat::Both => "both OpenAPI 3.0 specification and MockForge YAML configuration",
218 };
219
220 format!(
221 r#"You are an expert API architect and specification designer. Your role is to analyze API examples or descriptions and generate comprehensive, production-ready API specifications.
222
223Your task is to generate {}. When generating specifications, follow these principles:
224
2251. **RESTful Best Practices**: Use appropriate HTTP methods, status codes, and follow REST conventions
2262. **Consistency**: Maintain consistent naming conventions, response structures, and error handling
2273. **Completeness**: Include request/response schemas, parameters, error responses, and examples
2284. **Realistic**: Generate realistic and practical API designs that solve real problems
2295. **Security**: Include authentication/authorization considerations where appropriate
2306. **Documentation**: Provide clear descriptions for all endpoints, parameters, and responses
231
232When suggesting additional endpoints, consider:
233- CRUD operations for identified resources
234- Common utility endpoints (health, status, metrics)
235- Related resources and their relationships
236- Filtering, pagination, and search capabilities
237- Batch operations where appropriate
238
239Respond with valid JSON in the following structure:
240{{
241 "detected_domain": "string (e.g., 'e-commerce', 'social-media', 'fintech')",
242 "endpoints": [
243 {{
244 "method": "GET|POST|PUT|DELETE|PATCH",
245 "path": "/api/resource",
246 "description": "What this endpoint does",
247 "parameters": [
248 {{
249 "name": "param_name",
250 "location": "path|query|header|body",
251 "data_type": "string|integer|boolean|object",
252 "required": true|false,
253 "description": "Parameter description"
254 }}
255 ],
256 "response_schema": {{ /* JSON schema */ }},
257 "reasoning": "Why this endpoint is suggested"
258 }}
259 ],
260 "openapi_spec": {{ /* Complete OpenAPI 3.0 spec if requested */ }},
261 "mockforge_config": {{ /* Complete MockForge config if requested */ }}
262}}
263
264Generate {} additional endpoint suggestions beyond what was provided in the input."#,
265 format_desc, self.config.num_suggestions
266 )
267 }
268
269 fn build_endpoint_prompt(
271 &self,
272 method: &str,
273 path: &str,
274 request: &Option<Value>,
275 response: &Option<Value>,
276 description: &Option<String>,
277 ) -> String {
278 let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
279
280 let desc_text = description
281 .as_ref()
282 .map(|d| format!("Description: {}\n", d))
283 .unwrap_or_default();
284
285 let request_text = request
286 .as_ref()
287 .map(|r| {
288 format!(
289 "Request:\n```json\n{}\n```\n",
290 serde_json::to_string_pretty(r).unwrap_or_default()
291 )
292 })
293 .unwrap_or_default();
294
295 let response_text = response
296 .as_ref()
297 .map(|r| {
298 format!(
299 "Response:\n```json\n{}\n```\n",
300 serde_json::to_string_pretty(r).unwrap_or_default()
301 )
302 })
303 .unwrap_or_default();
304
305 format!(
306 r#"I have the following API endpoint example:
307
308Method: {}
309Path: {}
310{}{}{}
311API Domain/Category: {}
312
313Based on this single endpoint, please:
3141. Analyze the API's purpose and domain
3152. Suggest additional endpoints that would typically exist in such an API
3163. Generate a complete specification with realistic request/response schemas
3174. Include appropriate error handling and status codes
3185. Add pagination, filtering, or search capabilities where relevant
319
320Focus on creating a cohesive and practical API design that follows industry best practices."#,
321 method, path, desc_text, request_text, response_text, domain_hint
322 )
323 }
324
325 fn build_description_prompt(&self, description: &str) -> String {
327 let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
328
329 format!(
330 r#"I need to create an API with the following description:
331
332{}
333
334API Domain/Category: {}
335
336Based on this description, please:
3371. Design a comprehensive REST API with all necessary endpoints
3382. Define resource models and their relationships
3393. Include CRUD operations for main resources
3404. Add supporting endpoints (search, filters, pagination)
3415. Generate complete request/response schemas with realistic examples
3426. Consider authentication, authorization, and error handling
3437. Generate a complete specification ready for implementation
344
345Create a production-ready API design that follows REST best practices and industry standards."#,
346 description, domain_hint
347 )
348 }
349
350 fn build_partial_spec_prompt(&self, spec: &Value) -> String {
352 format!(
353 r#"I have a partial API specification:
354
355```json
356{}
357```
358
359Please:
3601. Analyze the existing specification structure
3612. Complete missing sections (schemas, responses, parameters)
3623. Suggest additional endpoints that would complement the existing ones
3634. Ensure consistency across all endpoints
3645. Add realistic examples and descriptions
3656. Fill in any gaps in the specification
3667. Generate a complete, production-ready specification
367
368Maintain the style and conventions of the original specification while expanding it."#,
369 serde_json::to_string_pretty(spec).unwrap_or_default()
370 )
371 }
372
373 fn build_paths_prompt(&self, paths: &[String]) -> String {
375 let paths_list = paths.join("\n- ");
376 let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
377
378 format!(
379 r#"I have a list of API endpoint paths:
380
381- {}
382
383API Domain/Category: {}
384
385Based on these paths, please:
3861. Infer the API's purpose and resource model
3872. Design appropriate HTTP methods for each path
3883. Generate complete request/response schemas
3894. Add query parameters for filtering, pagination, and sorting where appropriate
3905. Include proper error responses
3916. Suggest additional related endpoints that are missing
3927. Generate a complete specification
393
394Create a cohesive API design that makes sense for these endpoints and follows REST conventions."#,
395 paths_list, domain_hint
396 )
397 }
398
399 async fn parse_llm_response(
401 &self,
402 response: Value,
403 _input: &SuggestionInput,
404 ) -> Result<SuggestionResult> {
405 let endpoints = response
407 .get("endpoints")
408 .and_then(|e| e.as_array())
409 .ok_or_else(|| mockforge_foundation::Error::internal("No endpoints in LLM response"))?;
410
411 let suggestions: Vec<EndpointSuggestion> =
412 endpoints.iter().filter_map(|e| self.parse_endpoint_suggestion(e)).collect();
413
414 let openapi_spec =
416 if matches!(self.config.output_format, OutputFormat::OpenAPI | OutputFormat::Both) {
417 response.get("openapi_spec").cloned()
418 } else {
419 None
420 };
421
422 let mockforge_config =
423 if matches!(self.config.output_format, OutputFormat::MockForge | OutputFormat::Both) {
424 response.get("mockforge_config").cloned()
425 } else {
426 None
427 };
428
429 let detected_domain =
431 response.get("detected_domain").and_then(|d| d.as_str()).map(String::from);
432
433 let metadata = SuggestionMetadata {
434 endpoint_count: suggestions.len(),
435 detected_domain,
436 timestamp: chrono::Utc::now().to_rfc3339(),
437 model: self.config.llm_config.model.clone(),
438 };
439
440 Ok(SuggestionResult {
441 openapi_spec,
442 mockforge_config,
443 suggestions,
444 metadata,
445 })
446 }
447
448 fn parse_endpoint_suggestion(&self, endpoint: &Value) -> Option<EndpointSuggestion> {
450 let method = endpoint.get("method")?.as_str()?.to_string();
451 let path = endpoint.get("path")?.as_str()?.to_string();
452 let description = endpoint.get("description")?.as_str()?.to_string();
453 let reasoning = endpoint
454 .get("reasoning")
455 .and_then(|r| r.as_str())
456 .unwrap_or("Suggested by AI")
457 .to_string();
458
459 let parameters = endpoint
460 .get("parameters")
461 .and_then(|p| p.as_array())
462 .map(|params| params.iter().filter_map(|p| self.parse_parameter(p)).collect())
463 .unwrap_or_default();
464
465 let response_schema = endpoint.get("response_schema").cloned();
466
467 Some(EndpointSuggestion {
468 method,
469 path,
470 description,
471 parameters,
472 response_schema,
473 reasoning,
474 })
475 }
476
477 fn parse_parameter(&self, param: &Value) -> Option<ParameterInfo> {
479 Some(ParameterInfo {
480 name: param.get("name")?.as_str()?.to_string(),
481 location: param.get("location")?.as_str()?.to_string(),
482 data_type: param.get("data_type")?.as_str()?.to_string(),
483 required: param.get("required")?.as_bool()?,
484 description: param.get("description").and_then(|d| d.as_str()).map(String::from),
485 })
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_output_format_from_str() {
495 assert_eq!("openapi".parse::<OutputFormat>().unwrap(), OutputFormat::OpenAPI);
496 assert_eq!("mockforge".parse::<OutputFormat>().unwrap(), OutputFormat::MockForge);
497 assert_eq!("both".parse::<OutputFormat>().unwrap(), OutputFormat::Both);
498 assert!("invalid".parse::<OutputFormat>().is_err());
499 }
500
501 #[test]
502 fn test_suggestion_config_default() {
503 let config = SuggestionConfig::default();
504 assert_eq!(config.output_format, OutputFormat::OpenAPI);
505 assert_eq!(config.num_suggestions, 5);
506 assert!(config.include_examples);
507 }
508}