use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
impl HttpMethod {
fn as_str(&self) -> &'static str {
match self {
Self::Get => "get",
Self::Post => "post",
Self::Put => "put",
Self::Delete => "delete",
Self::Patch => "patch",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ParameterLocation {
Path,
Query,
Header,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiParameter {
pub name: String,
pub location: ParameterLocation,
pub required: bool,
pub data_type: String,
pub description: String,
}
impl ApiParameter {
pub fn path(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
location: ParameterLocation::Path,
required: true,
data_type: "string".to_string(),
description: description.into(),
}
}
pub fn query(
name: impl Into<String>,
data_type: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
name: name.into(),
location: ParameterLocation::Query,
required: false,
data_type: data_type.into(),
description: description.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResponse {
pub status_code: u16,
pub description: String,
pub content_type: Option<String>,
pub example: Option<String>,
}
impl ApiResponse {
pub fn json(status_code: u16, description: impl Into<String>) -> Self {
Self {
status_code,
description: description.into(),
content_type: Some("application/json".to_string()),
example: None,
}
}
pub fn with_example(mut self, example: impl Into<String>) -> Self {
self.example = Some(example.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiEndpoint {
pub path: String,
pub method: HttpMethod,
pub summary: String,
pub description: String,
pub tags: Vec<String>,
pub parameters: Vec<ApiParameter>,
pub responses: Vec<ApiResponse>,
pub auth_required: bool,
}
impl ApiEndpoint {
pub fn new(method: HttpMethod, path: impl Into<String>, summary: impl Into<String>) -> Self {
Self {
path: path.into(),
method,
summary: summary.into(),
description: String::new(),
tags: Vec::new(),
parameters: Vec::new(),
responses: vec![ApiResponse::json(200, "Successful response")],
auth_required: false,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_parameter(mut self, param: ApiParameter) -> Self {
self.parameters.push(param);
self
}
pub fn with_response(mut self, response: ApiResponse) -> Self {
self.responses.push(response);
self
}
pub fn requires_auth(mut self) -> Self {
self.auth_required = true;
self
}
}
pub struct OpenApiGenerator {
title: String,
version: String,
description: String,
server_url: String,
endpoints: Vec<ApiEndpoint>,
}
impl OpenApiGenerator {
pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
Self {
title: title.into(),
version: version.into(),
description: String::new(),
server_url: std::env::var("ARGENTOR_SERVER_URL")
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
endpoints: Vec::new(),
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn with_server(mut self, url: impl Into<String>) -> Self {
self.server_url = url.into();
self
}
pub fn add_endpoint(&mut self, endpoint: ApiEndpoint) {
self.endpoints.push(endpoint);
}
pub fn generate(&self) -> serde_json::Value {
let mut paths = serde_json::Map::new();
for endpoint in &self.endpoints {
let path_entry = paths
.entry(endpoint.path.clone())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(obj) = path_entry.as_object_mut() {
let mut operation = serde_json::Map::new();
operation.insert(
"summary".to_string(),
serde_json::Value::String(endpoint.summary.clone()),
);
if !endpoint.description.is_empty() {
operation.insert(
"description".to_string(),
serde_json::Value::String(endpoint.description.clone()),
);
}
if !endpoint.tags.is_empty() {
let tags: Vec<serde_json::Value> = endpoint
.tags
.iter()
.map(|t| serde_json::Value::String(t.clone()))
.collect();
operation.insert("tags".to_string(), serde_json::Value::Array(tags));
}
if !endpoint.parameters.is_empty() {
let params: Vec<serde_json::Value> = endpoint
.parameters
.iter()
.map(|p| {
let loc = match p.location {
ParameterLocation::Path => "path",
ParameterLocation::Query => "query",
ParameterLocation::Header => "header",
};
serde_json::json!({
"name": p.name,
"in": loc,
"required": p.required,
"description": p.description,
"schema": { "type": p.data_type }
})
})
.collect();
operation.insert("parameters".to_string(), serde_json::Value::Array(params));
}
let mut responses_map = serde_json::Map::new();
for resp in &endpoint.responses {
let mut resp_obj = serde_json::Map::new();
resp_obj.insert(
"description".to_string(),
serde_json::Value::String(resp.description.clone()),
);
if let Some(ct) = &resp.content_type {
let mut content = serde_json::Map::new();
let mut media_type = serde_json::Map::new();
if let Some(ex) = &resp.example {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(ex) {
let mut ex_obj = serde_json::Map::new();
ex_obj.insert("example".to_string(), parsed);
media_type = ex_obj;
}
}
content.insert(ct.clone(), serde_json::Value::Object(media_type));
resp_obj.insert("content".to_string(), serde_json::Value::Object(content));
}
responses_map.insert(
resp.status_code.to_string(),
serde_json::Value::Object(resp_obj),
);
}
operation.insert(
"responses".to_string(),
serde_json::Value::Object(responses_map),
);
if endpoint.auth_required {
operation.insert(
"security".to_string(),
serde_json::json!([{"bearerAuth": []}]),
);
}
obj.insert(
endpoint.method.as_str().to_string(),
serde_json::Value::Object(operation),
);
}
}
serde_json::json!({
"openapi": "3.0.3",
"info": {
"title": self.title,
"version": self.version,
"description": self.description
},
"servers": [{ "url": self.server_url }],
"paths": paths,
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
},
"apiKey": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key"
}
}
}
})
}
pub fn generate_json(&self) -> String {
serde_json::to_string_pretty(&self.generate()).unwrap_or_default()
}
pub fn argentor_default() -> Self {
let mut gen = Self::new("Argentor API", "1.0.0")
.with_description("Argentor — Secure AI Agent Framework API")
.with_server(
std::env::var("ARGENTOR_SERVER_URL")
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
);
gen.add_endpoint(ApiEndpoint::new(HttpMethod::Get, "/health", "Health check"));
gen.add_endpoint(ApiEndpoint::new(
HttpMethod::Get,
"/metrics",
"Prometheus metrics",
));
gen.add_endpoint(ApiEndpoint::new(
HttpMethod::Get,
"/dashboard",
"Web dashboard",
));
gen.add_endpoint(
ApiEndpoint::new(HttpMethod::Get, "/api/v1/sessions", "List sessions")
.with_tag("Sessions"),
);
gen.add_endpoint(
ApiEndpoint::new(HttpMethod::Post, "/api/v1/sessions", "Create session")
.with_tag("Sessions"),
);
gen.add_endpoint(
ApiEndpoint::new(HttpMethod::Get, "/api/v1/skills", "List skills").with_tag("Skills"),
);
gen.add_endpoint(
ApiEndpoint::new(
HttpMethod::Get,
"/api/v1/control-plane/deployments",
"List deployments",
)
.with_tag("Control Plane")
.requires_auth(),
);
gen.add_endpoint(
ApiEndpoint::new(
HttpMethod::Post,
"/api/v1/control-plane/deployments",
"Create deployment",
)
.with_tag("Control Plane")
.requires_auth(),
);
gen
}
pub fn endpoint_count(&self) -> usize {
self.endpoints.len()
}
}
pub fn argentor_openapi_spec() -> serde_json::Value {
OpenApiGenerator::argentor_default().generate()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_basic_spec() {
let gen = OpenApiGenerator::new("Test API", "1.0.0");
let spec = gen.generate();
assert_eq!(spec["openapi"], "3.0.3");
assert_eq!(spec["info"]["title"], "Test API");
}
#[test]
fn test_add_endpoint() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
gen.add_endpoint(ApiEndpoint::new(HttpMethod::Get, "/test", "Test endpoint"));
let spec = gen.generate();
assert!(spec["paths"]["/test"]["get"].is_object());
}
#[test]
fn test_endpoint_with_params() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
gen.add_endpoint(
ApiEndpoint::new(HttpMethod::Get, "/users/{id}", "Get user")
.with_parameter(ApiParameter::path("id", "User ID"))
.with_parameter(ApiParameter::query("fields", "string", "Fields to include")),
);
let spec = gen.generate();
let params = spec["paths"]["/users/{id}"]["get"]["parameters"]
.as_array()
.unwrap();
assert_eq!(params.len(), 2);
}
#[test]
fn test_endpoint_tags() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
gen.add_endpoint(
ApiEndpoint::new(HttpMethod::Get, "/test", "Test")
.with_tag("Users")
.with_tag("Admin"),
);
let spec = gen.generate();
let tags = spec["paths"]["/test"]["get"]["tags"].as_array().unwrap();
assert_eq!(tags.len(), 2);
}
#[test]
fn test_auth_required() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
gen.add_endpoint(ApiEndpoint::new(HttpMethod::Post, "/secure", "Secure").requires_auth());
let spec = gen.generate();
assert!(spec["paths"]["/secure"]["post"]["security"].is_array());
}
#[test]
fn test_multiple_methods() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
gen.add_endpoint(ApiEndpoint::new(HttpMethod::Get, "/items", "List items"));
gen.add_endpoint(ApiEndpoint::new(HttpMethod::Post, "/items", "Create item"));
let spec = gen.generate();
assert!(spec["paths"]["/items"]["get"].is_object());
assert!(spec["paths"]["/items"]["post"].is_object());
}
#[test]
fn test_server_url() {
let gen = OpenApiGenerator::new("Test", "1.0").with_server("https://api.example.com");
let spec = gen.generate();
assert_eq!(spec["servers"][0]["url"], "https://api.example.com");
}
#[test]
fn test_security_schemes() {
let gen = OpenApiGenerator::new("Test", "1.0");
let spec = gen.generate();
assert!(spec["components"]["securitySchemes"]["bearerAuth"].is_object());
assert!(spec["components"]["securitySchemes"]["apiKey"].is_object());
}
#[test]
fn test_generate_json() {
let gen = OpenApiGenerator::new("Test", "1.0");
let json = gen.generate_json();
assert!(json.contains("\"openapi\": \"3.0.3\""));
}
#[test]
fn test_argentor_default() {
let gen = OpenApiGenerator::argentor_default();
assert!(gen.endpoint_count() >= 7);
let spec = gen.generate();
assert_eq!(spec["info"]["title"], "Argentor API");
}
#[test]
fn test_argentor_openapi_spec() {
let spec = argentor_openapi_spec();
assert_eq!(spec["openapi"], "3.0.3");
}
#[test]
fn test_response_with_example() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
gen.add_endpoint(
ApiEndpoint::new(HttpMethod::Get, "/status", "Status")
.with_response(ApiResponse::json(200, "OK").with_example(r#"{"status": "ok"}"#)),
);
let spec = gen.generate();
assert!(spec["paths"]["/status"]["get"]["responses"]["200"].is_object());
}
#[test]
fn test_endpoint_count() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
assert_eq!(gen.endpoint_count(), 0);
gen.add_endpoint(ApiEndpoint::new(HttpMethod::Get, "/a", "A"));
gen.add_endpoint(ApiEndpoint::new(HttpMethod::Get, "/b", "B"));
assert_eq!(gen.endpoint_count(), 2);
}
#[test]
fn test_endpoint_serializable() {
let ep = ApiEndpoint::new(HttpMethod::Get, "/test", "Test");
let json = serde_json::to_string(&ep).unwrap();
assert!(json.contains("\"method\":\"get\""));
}
#[test]
fn test_http_method() {
assert_eq!(HttpMethod::Get.as_str(), "get");
assert_eq!(HttpMethod::Post.as_str(), "post");
assert_eq!(HttpMethod::Put.as_str(), "put");
assert_eq!(HttpMethod::Delete.as_str(), "delete");
assert_eq!(HttpMethod::Patch.as_str(), "patch");
}
#[test]
fn test_path_parameter() {
let p = ApiParameter::path("id", "Resource ID");
assert_eq!(p.location, ParameterLocation::Path);
assert!(p.required);
}
#[test]
fn test_query_parameter() {
let p = ApiParameter::query("limit", "integer", "Max results");
assert_eq!(p.location, ParameterLocation::Query);
assert!(!p.required);
}
#[test]
fn test_description() {
let gen = OpenApiGenerator::new("Test", "1.0").with_description("My API");
let spec = gen.generate();
assert_eq!(spec["info"]["description"], "My API");
}
#[test]
fn test_endpoint_description() {
let mut gen = OpenApiGenerator::new("Test", "1.0");
gen.add_endpoint(
ApiEndpoint::new(HttpMethod::Get, "/test", "Test")
.with_description("Detailed description"),
);
let spec = gen.generate();
assert_eq!(
spec["paths"]["/test"]["get"]["description"],
"Detailed description"
);
}
#[test]
fn test_empty_paths() {
let gen = OpenApiGenerator::new("Test", "1.0");
let spec = gen.generate();
assert!(spec["paths"].as_object().unwrap().is_empty());
}
}