pub mod openapi;
pub mod postman;
use crate::Resolver;
use openapi::ParsedSpec;
#[derive(Debug, Clone)]
pub struct ImportResult {
pub created: Vec<ImportedIntent>,
pub skipped: Vec<String>,
pub total_operations: usize,
}
#[derive(Debug, Clone)]
pub struct ImportedIntent {
pub intent_id: String,
pub phrases: Vec<String>,
pub endpoint: String,
pub method: String,
}
pub fn import_spec(router: &mut Resolver, spec: &ParsedSpec) -> ImportResult {
let mut created = Vec::new();
let mut skipped = Vec::new();
for op in &spec.operations {
let intent_id = op.operation_id.as_deref().unwrap_or(&op.id);
let intent_name = to_snake_case(intent_id);
let name_lower = op.name.to_lowercase();
if name_lower.is_empty() {
skipped.push(intent_name);
continue;
}
let _ = router.add_intent(&intent_name, &[name_lower.as_str()]);
let desc = op.summary.as_deref().or(Some(&op.name)).unwrap_or("");
let _ = router.update_intent(
&intent_name,
crate::IntentEdit {
description: if desc.is_empty() {
None
} else {
Some(desc.to_string())
},
..Default::default()
},
);
let endpoint = format!("{} {}", op.method, op.path);
created.push(ImportedIntent {
intent_id: intent_name,
phrases: vec![name_lower],
endpoint,
method: op.method.clone(),
});
}
ImportResult {
total_operations: spec.operations.len(),
created,
skipped,
}
}
pub fn parse_spec(input: &str) -> Result<ParsedSpec, String> {
if let Ok(collection) = serde_json::from_str::<postman::PostmanCollection>(input) {
if collection
.info
.schema
.as_ref()
.is_some_and(|s| s.contains("postman"))
{
return postman::convert_postman(&collection)
.map_err(|e| format!("Postman parse error: {}", e));
}
}
openapi::parse_openapi(input).map_err(|e| format!("OpenAPI parse error: {}", e))
}
pub fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
if c == '-' || c == ' ' {
result.push('_');
} else {
result.push(c.to_lowercase().next().unwrap_or(c));
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_snake_case() {
assert_eq!(to_snake_case("getOrder"), "get_order");
assert_eq!(
to_snake_case("createPaymentIntent"),
"create_payment_intent"
);
assert_eq!(to_snake_case("list-users"), "list_users");
assert_eq!(to_snake_case("cancelOrder"), "cancel_order");
}
#[test]
fn test_import_openapi_spec() {
let spec_yaml = r#"
openapi: "3.0.0"
info:
title: Test API
version: "1.0.0"
paths:
/orders:
get:
summary: List all orders
description: Retrieve a list of all customer orders. Supports pagination and filtering.
operationId: listOrders
tags: [orders]
responses:
"200":
description: Success
post:
summary: Create a new order
description: Place a new order for the customer. Requires items and shipping address.
operationId: createOrder
tags: [orders]
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
"201":
description: Created
/orders/{id}/cancel:
post:
summary: Cancel an order
description: Cancel a pending order. Cannot cancel orders that have already shipped.
operationId: cancelOrder
tags: [orders]
parameters:
- name: id
in: path
required: true
responses:
"200":
description: Cancelled
"#;
let spec = openapi::parse_openapi(spec_yaml).unwrap();
assert_eq!(spec.operations.len(), 3);
let mut router = Resolver::new();
let result = import_spec(&mut router, &spec);
assert_eq!(result.created.len(), 3);
assert_eq!(result.skipped.len(), 0);
let cancel_phrases = router.training("cancel_order").unwrap_or_default();
assert!(!cancel_phrases.is_empty());
let list_phrases = router.training("list_orders").unwrap_or_default();
assert!(!list_phrases.is_empty());
}
#[test]
fn test_parse_spec_auto_detect() {
let openapi_json = r#"{"openapi":"3.0.0","info":{"title":"Test","version":"1.0"},"paths":{"/test":{"get":{"summary":"Test endpoint","operationId":"test","responses":{"200":{"description":"OK"}}}}}}"#;
let spec = parse_spec(openapi_json).unwrap();
assert_eq!(spec.title, "Test");
assert_eq!(spec.operations.len(), 1);
}
}