#![allow(dead_code)]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use crate::core::parser::{
ParameterLocation, SchemaType, SecurityRequirement, UnifiedOperation, UnifiedParameter,
UnifiedRequestBody, UnifiedSchema, UnifiedSpec,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct CardParameter {
pub name: String,
pub location: String, pub required: bool,
pub param_type: String,
pub format: Option<String>,
pub description: Option<String>,
pub enum_values: Option<Vec<String>>,
}
impl CardParameter {
pub fn from_unified(param: &UnifiedParameter) -> Self {
let location = match param.location {
ParameterLocation::Path => "path",
ParameterLocation::Query => "query",
ParameterLocation::Header => "header",
ParameterLocation::Cookie => "cookie",
};
Self {
name: param.name.clone(),
location: location.to_string(),
required: param.required,
param_type: param.schema.schema_type.to_string(),
format: param.schema.format.clone(),
description: param.description.clone(),
enum_values: param.schema.enum_values.as_ref().map(|vals| {
vals.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
}),
}
}
pub fn to_text(&self) -> String {
let mut parts = vec![format!("{} ({})", self.name, self.param_type)];
if self.required {
parts.push("required".to_string());
}
if let Some(desc) = &self.description {
parts.push(desc.clone());
}
if let Some(enums) = &self.enum_values {
parts.push(format!("values: {}", enums.join(", ")));
}
parts.join(" - ")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CardRequestBody {
pub content_type: String,
pub required: bool,
pub schema_summary: String,
pub properties: BTreeMap<String, CardProperty>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct CardProperty {
pub name: String,
pub prop_type: String,
pub required: bool,
pub description: Option<String>,
}
impl CardRequestBody {
pub fn from_unified(rb: &UnifiedRequestBody) -> Option<Self> {
let (content_type, media_type) = rb
.content
.get("application/json")
.map(|mt| ("application/json".to_string(), mt))
.or_else(|| rb.content.iter().next().map(|(k, v)| (k.clone(), v)))?;
let schema = &media_type.schema;
let schema_summary = summarize_schema(schema);
let mut properties = BTreeMap::new();
if let Some(props) = &schema.properties {
let required_fields: Vec<String> = schema.required.clone().unwrap_or_default();
for (name, prop_schema) in props {
properties.insert(
name.clone(),
CardProperty {
name: name.clone(),
prop_type: prop_schema.schema_type.to_string(),
required: required_fields.contains(name),
description: prop_schema.description.clone(),
},
);
}
}
Some(Self {
content_type,
required: rb.required,
schema_summary,
properties,
})
}
pub fn to_text(&self) -> String {
let mut lines = vec![format!("Body ({})", self.content_type)];
for (_, prop) in &self.properties {
let req_marker = if prop.required { "*" } else { "" };
let desc = prop
.description
.as_ref()
.map(|d| format!(" - {}", d))
.unwrap_or_default();
lines.push(format!(
" {}{}: {}{}",
prop.name, req_marker, prop.prop_type, desc
));
}
lines.join("\n")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationCard {
pub spec_id: String,
pub operation_id: String,
pub content_hash: String,
pub method: String,
pub path: String,
pub summary: Option<String>,
pub description: Option<String>,
pub parameters: Vec<CardParameter>,
pub request_body: Option<CardRequestBody>,
pub auth_required: bool,
pub auth_type: Option<String>,
pub auth_scopes: Vec<String>,
pub risk_level: String, pub tags: Vec<String>,
pub alias: String,
pub embedding_text: String,
pub embedding: Option<Vec<f32>>,
}
impl OperationCard {
pub fn from_unified(spec_id: &str, op: &UnifiedOperation, spec: &UnifiedSpec) -> Self {
let mut parameters: Vec<CardParameter> = op
.parameters
.iter()
.map(CardParameter::from_unified)
.collect();
parameters.sort_by(|a, b| a.name.cmp(&b.name));
let request_body = op
.request_body
.as_ref()
.and_then(CardRequestBody::from_unified);
let (auth_required, auth_type, auth_scopes) =
extract_auth_info(op.security.as_ref(), &spec.security_schemes);
let risk_level = match op.method.to_uppercase().as_str() {
"GET" | "HEAD" | "OPTIONS" => "read",
_ => "write",
}
.to_string();
let tags = if op.tags.is_empty() {
extract_tags_from_path(&op.path)
} else {
op.tags.clone()
};
let alias = generate_alias(&op.summary, &tags, &op.operation_id);
let mut card = Self {
spec_id: spec_id.to_string(),
operation_id: op.operation_id.clone(),
content_hash: String::new(), method: op.method.clone(),
path: op.path.clone(),
summary: op.summary.clone(),
description: op.description.clone(),
parameters,
request_body,
auth_required,
auth_type,
auth_scopes,
risk_level,
tags,
alias,
embedding_text: String::new(), embedding: None,
};
card.embedding_text = card.generate_embedding_text();
card.content_hash = card.compute_hash();
card
}
fn generate_embedding_text(&self) -> String {
use crate::core::identifier_splitter::split_to_text;
let mut lines = Vec::new();
lines.push(format!("OPERATION: {}", self.operation_id));
let op_tokens = split_to_text(&self.operation_id);
if !op_tokens.is_empty() {
lines.push(format!("TOKENS: {}", op_tokens));
}
lines.push(format!("ENDPOINT: {} {}", self.method, self.path));
let path_tokens = split_to_text(&self.path);
if !path_tokens.is_empty() {
lines.push(format!("PATH_TOKENS: {}", path_tokens));
}
if let Some(summary) = &self.summary {
lines.push(format!("SUMMARY: {}", summary));
}
if let Some(desc) = &self.description {
lines.push(format!("DESCRIPTION: {}", desc));
}
if !self.parameters.is_empty() {
lines.push("PARAMETERS:".to_string());
for param in &self.parameters {
lines.push(format!(" - {}", param.to_text()));
}
let param_tokens: Vec<String> = self
.parameters
.iter()
.flat_map(|p| crate::core::identifier_splitter::split_identifier(&p.name))
.collect();
if !param_tokens.is_empty() {
lines.push(format!("PARAM_TOKENS: {}", param_tokens.join(" ")));
}
}
if let Some(rb) = &self.request_body {
lines.push(rb.to_text());
}
if self.auth_required {
let auth_info = self
.auth_type
.as_ref()
.map(|t| t.clone())
.unwrap_or_else(|| "required".to_string());
lines.push(format!("AUTH: {}", auth_info));
}
lines.push(format!("RISK: {}", self.risk_level));
if !self.tags.is_empty() {
lines.push(format!("TAGS: {}", self.tags.join(", ")));
}
lines.join("\n")
}
fn compute_hash(&self) -> String {
let hash_content = serde_json::json!({
"spec_id": self.spec_id,
"operation_id": self.operation_id,
"method": self.method,
"path": self.path,
"summary": self.summary,
"description": self.description,
"parameters": self.parameters,
"request_body": self.request_body,
"auth_required": self.auth_required,
"auth_type": self.auth_type,
"auth_scopes": self.auth_scopes,
"risk_level": self.risk_level,
"tags": self.tags,
});
let json_str = serde_json::to_string(&hash_content).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(json_str.as_bytes());
let result = hasher.finalize();
hex::encode(result)
}
pub fn to_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn to_json_compact(&self) -> Result<String> {
Ok(serde_json::to_string(self)?)
}
}
fn extract_auth_info(
security: Option<&Vec<SecurityRequirement>>,
schemes: &std::collections::HashMap<String, crate::core::parser::SecurityScheme>,
) -> (bool, Option<String>, Vec<String>) {
let Some(sec_reqs) = security else {
return (false, None, Vec::new());
};
if sec_reqs.is_empty() {
return (false, None, Vec::new());
}
let first_req = &sec_reqs[0];
let scheme_name = &first_req.scheme_name;
let scopes = first_req.scopes.clone();
let auth_type = schemes
.get(scheme_name)
.map(|scheme| match scheme.scheme_type.as_str() {
"http" => {
if let Some(s) = &scheme.scheme {
match s.as_str() {
"bearer" => "Bearer Token".to_string(),
"basic" => "Basic Auth".to_string(),
_ => s.clone(),
}
} else {
"HTTP Auth".to_string()
}
}
"apiKey" => {
let location = scheme.location.as_deref().unwrap_or("header");
let name = scheme.name.as_deref().unwrap_or("API-Key");
format!("API Key ({} in {})", name, location)
}
"oauth2" => "OAuth 2.0".to_string(),
"openIdConnect" => "OpenID Connect".to_string(),
_ => scheme.scheme_type.clone(),
});
(true, auth_type, scopes)
}
fn generate_alias(summary: &Option<String>, tags: &[String], operation_id: &str) -> String {
let camel = match summary {
Some(s) if !s.is_empty() => summary_to_camel_case(s),
_ => return operation_id.to_string(), };
if let Some(tag) = tags.first() {
let tag_lower = tag.to_lowercase();
if camel.to_lowercase().starts_with(&tag_lower) {
camel
} else {
format!("{}.{}", tag_lower, camel)
}
} else {
camel }
}
fn summary_to_camel_case(summary: &str) -> String {
let words: Vec<&str> = summary
.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.collect();
if words.is_empty() {
return String::new();
}
let mut result = words[0].to_lowercase();
for word in &words[1..] {
let mut chars = word.chars();
if let Some(first) = chars.next() {
result.extend(first.to_uppercase());
result.extend(chars);
}
}
if result.len() > 80 {
result.truncate(80);
if let Some(pos) = result.rfind(|c: char| c.is_uppercase()) {
if pos > 10 {
result.truncate(pos);
}
}
}
result
}
fn extract_tags_from_path(path: &str) -> Vec<String> {
path.split('/')
.filter(|s| !s.is_empty() && !s.starts_with('{'))
.map(|s| s.to_lowercase())
.collect()
}
fn summarize_schema(schema: &UnifiedSchema) -> String {
match schema.schema_type {
SchemaType::Object => {
if let Some(props) = &schema.properties {
let prop_names: Vec<&String> = props.keys().take(5).collect();
let more = if props.len() > 5 {
format!(" +{} more", props.len() - 5)
} else {
String::new()
};
format!(
"object {{ {} }}{}",
prop_names
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", "),
more
)
} else {
"object".to_string()
}
}
SchemaType::Array => {
if let Some(items) = &schema.items {
format!("array of {}", summarize_schema(items))
} else {
"array".to_string()
}
}
_ => schema.schema_type.to_string(),
}
}
pub fn build_cards_from_spec(spec_id: &str, spec: &UnifiedSpec) -> Vec<OperationCard> {
spec.operations
.iter()
.map(|op| OperationCard::from_unified(spec_id, op, spec))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_tags_from_path() {
assert_eq!(
extract_tags_from_path("/users/{id}/orders"),
vec!["users", "orders"]
);
assert_eq!(
extract_tags_from_path("/api/v1/products"),
vec!["api", "v1", "products"]
);
}
#[test]
fn test_risk_level_classification() {
}
#[test]
fn test_camel_case_basic() {
assert_eq!(
summary_to_camel_case("List Transactions"),
"listTransactions"
);
assert_eq!(summary_to_camel_case("Get Portfolio"), "getPortfolio");
assert_eq!(summary_to_camel_case("Create User"), "createUser");
}
#[test]
fn test_camel_case_single_word() {
assert_eq!(summary_to_camel_case("Analyze"), "analyze");
assert_eq!(summary_to_camel_case("Dashboard"), "dashboard");
}
#[test]
fn test_camel_case_empty() {
assert_eq!(summary_to_camel_case(""), "");
assert_eq!(summary_to_camel_case(" "), "");
}
#[test]
fn test_camel_case_special_chars() {
assert_eq!(
summary_to_camel_case("List/All Transactions"),
"listAllTransactions"
);
assert_eq!(
summary_to_camel_case("Get-Single-Resource"),
"getSingleResource"
);
assert_eq!(
summary_to_camel_case("Create + Update User"),
"createUpdateUser"
);
}
#[test]
fn test_camel_case_hyphens() {
assert_eq!(summary_to_camel_case("Get-All-Users"), "getAllUsers");
assert_eq!(summary_to_camel_case("health-check"), "healthCheck");
}
#[test]
fn test_camel_case_with_numbers() {
assert_eq!(summary_to_camel_case("Get V2 Users"), "getV2Users");
assert_eq!(
summary_to_camel_case("List API v1 Endpoints"),
"listAPIV1Endpoints"
);
}
#[test]
fn test_camel_case_truncation() {
let long_summary = "Delete All Transactions For Ticker From Portfolio In The Given Time Range With Full Audit Trail And Compliance Checks Across Multiple Jurisdictions";
let alias = summary_to_camel_case(long_summary);
assert!(alias.len() <= 80, "Alias too long: {} chars", alias.len());
}
#[test]
fn test_alias_with_tag_and_summary() {
let alias = generate_alias(
&Some("List Transactions".to_string()),
&["portfolio".to_string()],
"list_transactions_api_portfolio_transactions_get",
);
assert_eq!(alias, "portfolio.listTransactions");
}
#[test]
fn test_alias_summary_starts_with_tag() {
let alias = generate_alias(
&Some("Portfolio Summary".to_string()),
&["portfolio".to_string()],
"portfolio_summary_get",
);
assert_eq!(alias, "portfolioSummary");
}
#[test]
fn test_alias_no_tags() {
let alias = generate_alias(&Some("Health Check".to_string()), &[], "health_check_get");
assert_eq!(alias, "healthCheck");
}
#[test]
fn test_alias_no_summary() {
let alias = generate_alias(&None, &["users".to_string()], "get_users_api_users_get");
assert_eq!(alias, "get_users_api_users_get"); }
#[test]
fn test_alias_empty_summary() {
let alias = generate_alias(
&Some("".to_string()),
&["users".to_string()],
"get_users_api_users_get",
);
assert_eq!(alias, "get_users_api_users_get"); }
}