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 seed: None,
186 };
187
188 let llm_response = self.llm_client.generate(&request).await?;
189
190 self.parse_llm_response(llm_response, input).await
192 }
193
194 fn build_prompts(&self, input: &SuggestionInput) -> Result<(String, String)> {
196 let system_prompt = self.build_system_prompt();
197 let user_prompt = match input {
198 SuggestionInput::Endpoint {
199 method,
200 path,
201 request,
202 response,
203 description,
204 } => self.build_endpoint_prompt(method, path, request, response, description),
205 SuggestionInput::Description { text } => self.build_description_prompt(text),
206 SuggestionInput::PartialSpec { spec } => self.build_partial_spec_prompt(spec),
207 SuggestionInput::Paths { paths } => self.build_paths_prompt(paths),
208 };
209
210 Ok((system_prompt, user_prompt))
211 }
212
213 fn build_system_prompt(&self) -> String {
215 let format_desc = match self.config.output_format {
216 OutputFormat::OpenAPI => "OpenAPI 3.0 specification",
217 OutputFormat::MockForge => "MockForge YAML configuration",
218 OutputFormat::Both => "both OpenAPI 3.0 specification and MockForge YAML configuration",
219 };
220
221 format!(
222 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.
223
224Your task is to generate {}. When generating specifications, follow these principles:
225
2261. **RESTful Best Practices**: Use appropriate HTTP methods, status codes, and follow REST conventions
2272. **Consistency**: Maintain consistent naming conventions, response structures, and error handling
2283. **Completeness**: Include request/response schemas, parameters, error responses, and examples
2294. **Realistic**: Generate realistic and practical API designs that solve real problems
2305. **Security**: Include authentication/authorization considerations where appropriate
2316. **Documentation**: Provide clear descriptions for all endpoints, parameters, and responses
232
233When suggesting additional endpoints, consider:
234- CRUD operations for identified resources
235- Common utility endpoints (health, status, metrics)
236- Related resources and their relationships
237- Filtering, pagination, and search capabilities
238- Batch operations where appropriate
239
240Respond with valid JSON in the following structure:
241{{
242 "detected_domain": "string (e.g., 'e-commerce', 'social-media', 'fintech')",
243 "endpoints": [
244 {{
245 "method": "GET|POST|PUT|DELETE|PATCH",
246 "path": "/api/resource",
247 "description": "What this endpoint does",
248 "parameters": [
249 {{
250 "name": "param_name",
251 "location": "path|query|header|body",
252 "data_type": "string|integer|boolean|object",
253 "required": true|false,
254 "description": "Parameter description"
255 }}
256 ],
257 "response_schema": {{ /* JSON schema */ }},
258 "reasoning": "Why this endpoint is suggested"
259 }}
260 ],
261 "openapi_spec": {{ /* Complete OpenAPI 3.0 spec if requested */ }},
262 "mockforge_config": {{ /* Complete MockForge config if requested */ }}
263}}
264
265Generate {} additional endpoint suggestions beyond what was provided in the input."#,
266 format_desc, self.config.num_suggestions
267 )
268 }
269
270 fn build_endpoint_prompt(
272 &self,
273 method: &str,
274 path: &str,
275 request: &Option<Value>,
276 response: &Option<Value>,
277 description: &Option<String>,
278 ) -> String {
279 let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
280
281 let desc_text = description
282 .as_ref()
283 .map(|d| format!("Description: {}\n", d))
284 .unwrap_or_default();
285
286 let request_text = request
287 .as_ref()
288 .map(|r| {
289 format!(
290 "Request:\n```json\n{}\n```\n",
291 serde_json::to_string_pretty(r).unwrap_or_default()
292 )
293 })
294 .unwrap_or_default();
295
296 let response_text = response
297 .as_ref()
298 .map(|r| {
299 format!(
300 "Response:\n```json\n{}\n```\n",
301 serde_json::to_string_pretty(r).unwrap_or_default()
302 )
303 })
304 .unwrap_or_default();
305
306 format!(
307 r#"I have the following API endpoint example:
308
309Method: {}
310Path: {}
311{}{}{}
312API Domain/Category: {}
313
314Based on this single endpoint, please:
3151. Analyze the API's purpose and domain
3162. Suggest additional endpoints that would typically exist in such an API
3173. Generate a complete specification with realistic request/response schemas
3184. Include appropriate error handling and status codes
3195. Add pagination, filtering, or search capabilities where relevant
320
321Focus on creating a cohesive and practical API design that follows industry best practices."#,
322 method, path, desc_text, request_text, response_text, domain_hint
323 )
324 }
325
326 fn build_description_prompt(&self, description: &str) -> String {
328 let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
329
330 format!(
331 r#"I need to create an API with the following description:
332
333{}
334
335API Domain/Category: {}
336
337Based on this description, please:
3381. Design a comprehensive REST API with all necessary endpoints
3392. Define resource models and their relationships
3403. Include CRUD operations for main resources
3414. Add supporting endpoints (search, filters, pagination)
3425. Generate complete request/response schemas with realistic examples
3436. Consider authentication, authorization, and error handling
3447. Generate a complete specification ready for implementation
345
346Create a production-ready API design that follows REST best practices and industry standards."#,
347 description, domain_hint
348 )
349 }
350
351 fn build_partial_spec_prompt(&self, spec: &Value) -> String {
353 format!(
354 r#"I have a partial API specification:
355
356```json
357{}
358```
359
360Please:
3611. Analyze the existing specification structure
3622. Complete missing sections (schemas, responses, parameters)
3633. Suggest additional endpoints that would complement the existing ones
3644. Ensure consistency across all endpoints
3655. Add realistic examples and descriptions
3666. Fill in any gaps in the specification
3677. Generate a complete, production-ready specification
368
369Maintain the style and conventions of the original specification while expanding it."#,
370 serde_json::to_string_pretty(spec).unwrap_or_default()
371 )
372 }
373
374 fn build_paths_prompt(&self, paths: &[String]) -> String {
376 let paths_list = paths.join("\n- ");
377 let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
378
379 format!(
380 r#"I have a list of API endpoint paths:
381
382- {}
383
384API Domain/Category: {}
385
386Based on these paths, please:
3871. Infer the API's purpose and resource model
3882. Design appropriate HTTP methods for each path
3893. Generate complete request/response schemas
3904. Add query parameters for filtering, pagination, and sorting where appropriate
3915. Include proper error responses
3926. Suggest additional related endpoints that are missing
3937. Generate a complete specification
394
395Create a cohesive API design that makes sense for these endpoints and follows REST conventions."#,
396 paths_list, domain_hint
397 )
398 }
399
400 async fn parse_llm_response(
402 &self,
403 response: Value,
404 _input: &SuggestionInput,
405 ) -> Result<SuggestionResult> {
406 let endpoints = response
408 .get("endpoints")
409 .and_then(|e| e.as_array())
410 .ok_or_else(|| mockforge_foundation::Error::internal("No endpoints in LLM response"))?;
411
412 let suggestions: Vec<EndpointSuggestion> =
413 endpoints.iter().filter_map(|e| self.parse_endpoint_suggestion(e)).collect();
414
415 let openapi_spec =
417 if matches!(self.config.output_format, OutputFormat::OpenAPI | OutputFormat::Both) {
418 response.get("openapi_spec").cloned()
419 } else {
420 None
421 };
422
423 let mockforge_config =
424 if matches!(self.config.output_format, OutputFormat::MockForge | OutputFormat::Both) {
425 response.get("mockforge_config").cloned()
426 } else {
427 None
428 };
429
430 let detected_domain =
432 response.get("detected_domain").and_then(|d| d.as_str()).map(String::from);
433
434 let metadata = SuggestionMetadata {
435 endpoint_count: suggestions.len(),
436 detected_domain,
437 timestamp: chrono::Utc::now().to_rfc3339(),
438 model: self.config.llm_config.model.clone(),
439 };
440
441 Ok(SuggestionResult {
442 openapi_spec,
443 mockforge_config,
444 suggestions,
445 metadata,
446 })
447 }
448
449 fn parse_endpoint_suggestion(&self, endpoint: &Value) -> Option<EndpointSuggestion> {
451 let method = endpoint.get("method")?.as_str()?.to_string();
452 let path = endpoint.get("path")?.as_str()?.to_string();
453 let description = endpoint.get("description")?.as_str()?.to_string();
454 let reasoning = endpoint
455 .get("reasoning")
456 .and_then(|r| r.as_str())
457 .unwrap_or("Suggested by AI")
458 .to_string();
459
460 let parameters = endpoint
461 .get("parameters")
462 .and_then(|p| p.as_array())
463 .map(|params| params.iter().filter_map(|p| self.parse_parameter(p)).collect())
464 .unwrap_or_default();
465
466 let response_schema = endpoint.get("response_schema").cloned();
467
468 Some(EndpointSuggestion {
469 method,
470 path,
471 description,
472 parameters,
473 response_schema,
474 reasoning,
475 })
476 }
477
478 fn parse_parameter(&self, param: &Value) -> Option<ParameterInfo> {
480 Some(ParameterInfo {
481 name: param.get("name")?.as_str()?.to_string(),
482 location: param.get("location")?.as_str()?.to_string(),
483 data_type: param.get("data_type")?.as_str()?.to_string(),
484 required: param.get("required")?.as_bool()?,
485 description: param.get("description").and_then(|d| d.as_str()).map(String::from),
486 })
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn test_output_format_from_str() {
496 assert_eq!("openapi".parse::<OutputFormat>().unwrap(), OutputFormat::OpenAPI);
497 assert_eq!("mockforge".parse::<OutputFormat>().unwrap(), OutputFormat::MockForge);
498 assert_eq!("both".parse::<OutputFormat>().unwrap(), OutputFormat::Both);
499 assert!("invalid".parse::<OutputFormat>().is_err());
500 }
501
502 #[test]
503 fn test_suggestion_config_default() {
504 let config = SuggestionConfig::default();
505 assert_eq!(config.output_format, OutputFormat::OpenAPI);
506 assert_eq!(config.num_suggestions, 5);
507 assert!(config.include_examples);
508 }
509}