use anyhow::Result;
use axum::{
extract::{DefaultBodyLimit, Path, Query, State},
http::{HeaderMap, Method, StatusCode},
middleware::{self, Next},
response::{Html, IntoResponse, Json, Response},
routing::{delete, get, post, put},
Router,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower::ServiceBuilder;
use tower_http::{
compression::CompressionLayer,
cors::{Any, CorsLayer},
limit::RequestBodyLimitLayer,
request_id::{MakeRequestId, PropagateRequestIdLayer, RequestId},
sensitive_headers::SetSensitiveRequestHeadersLayer,
timeout::TimeoutLayer,
trace::TraceLayer,
};
use tracing::{error, info};
use utoipa::{OpenApi, ToSchema};
use utoipa_redoc::{Redoc, Servable};
use utoipa_swagger_ui::SwaggerUi;
use uuid::Uuid;
use crate::core::AnalysisResult;
use crate::core::{AppState, OpenCrates};
use crate::providers::{CodexProvider, GenerationResponse, LLMProvider};
use crate::utils::{
cache::CacheManager,
config::{CodexConfig, OpenCratesConfig},
health::HealthManager,
metrics::MetricRegistry,
templates::{CrateSpec, CrateType},
};
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CodeGenerationRequest {
pub instruction: String,
pub context: Option<String>,
pub language: Option<String>,
pub include_tests: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CodeGenerationResponse {
pub code: String,
pub metadata: GenerationMetadata,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GenerationMetadata {
pub model: String,
pub tokens_used: usize,
pub timestamp: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PatchApplicationRequest {
pub file_path: String,
pub patch_content: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PatchApplicationResponse {
pub success: bool,
pub message: String,
pub file_path: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GenerateRequest {
pub name: String,
pub description: String,
pub features: Vec<String>,
pub context: Option<String>,
pub provider: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GenerateResponse {
pub request_id: String,
pub crate_spec: CrateSpec,
pub metrics: GenerationMetrics,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GenerationMetrics {
pub generation_time_ms: u64,
pub tokens_used: Option<u64>,
pub cost_estimate: Option<f64>,
pub provider: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct AnalyzeRequest {
pub path: std::path::PathBuf,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct HealthStatus {
pub status: String,
pub timestamp: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ApiError {
pub message: String,
pub code: String,
}
#[derive(OpenApi)]
#[openapi(
paths(
health_handler,
ready_handler,
metrics_handler,
generate_handler,
codex_generate_handler,
codex_apply_patch_handler,
codex_health_handler,
codex_models_handler,
generate_crate,
analyze_project,
health_check,
system_status,
metrics_endpoint,
list_templates,
list_providers,
),
components(
schemas(CodeGenerationRequest, CodeGenerationResponse, GenerationMetadata, PatchApplicationRequest, PatchApplicationResponse)
),
tags(
(name = "OpenCrates", description = "OpenCrates API"),
(name = "generation", description = "Crate generation endpoints"),
(name = "analysis", description = "Project analysis endpoints"),
(name = "system", description = "System monitoring endpoints"),
(name = "templates", description = "Template management endpoints"),
(name = "providers", description = "AI provider endpoints"),
),
info(
title = "OpenCrates API",
version = "3.0.0",
description = "Enterprise-grade AI-powered Rust development companion",
contact(
name = "OpenCrates Team",
email = "support@opencrates.dev",
),
license(
name = "MIT OR Apache-2.0",
url = "https://opensource.org/licenses/MIT",
),
),
servers(
(url = "http://localhost:8080", description = "Local development server"),
(url = "https://api.opencrates.dev", description = "Production server"),
),
)]
struct ApiDoc;
pub async fn create_app(config: OpenCratesConfig) -> Result<Router> {
info!("Creating OpenCrates server application");
let core = Arc::new(OpenCrates::new_with_config(config.clone()).await?);
let metrics = Arc::new(MetricRegistry::new());
let health_manager = Arc::new(HealthManager::new().await?);
let cache_manager = Arc::new(CacheManager::new());
let app_state = AppState {
core,
metrics,
health: health_manager,
cache: cache_manager,
start_time: std::time::Instant::now(),
};
let router = Router::new()
.route("/health", get(health_handler))
.route("/ready", get(ready_handler))
.route("/metrics", get(metrics_handler))
.route("/api/v1/generate", post(generate_handler))
.route("/api/v1/codex/generate", post(codex_generate_handler))
.route("/api/v1/codex/apply-patch", post(codex_apply_patch_handler))
.route("/api/v1/codex/health", get(codex_health_handler))
.route("/api/v1/codex/models", get(codex_models_handler))
.route("/generate", post(generate_crate))
.route("/analyze", post(analyze_project))
.route("/templates", get(list_templates))
.route("/providers", get(list_providers))
.route("/providers/:provider/models", get(list_provider_models))
.route("/system/health", get(health_check))
.route("/system/status", get(system_status))
.route("/system/metrics", get(metrics_endpoint))
.route("/version", get(version_info))
.route("/admin/stats", get(admin_stats))
.route("/admin/config", get(get_config))
.route("/admin/config", put(update_config))
.route("/admin/cache/clear", post(clear_cache))
.with_state(app_state);
let swagger_ui = SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi());
let redoc = Redoc::with_url("/redoc", ApiDoc::openapi());
let app = router.merge(swagger_ui).merge(redoc);
Ok(app)
}
#[utoipa::path(
get,
path = "/health",
responses(
(status = 200, description = "Service is healthy")
)
)]
async fn health_handler() -> Result<Json<Value>, StatusCode> {
Ok(Json(serde_json::json!({
"status": "healthy",
"service": "opencrates",
"timestamp": chrono::Utc::now().to_rfc3339(),
"version": env!("CARGO_PKG_VERSION")
})))
}
#[utoipa::path(
get,
path = "/ready",
responses(
(status = 200, description = "Service is ready to accept traffic")
)
)]
async fn ready_handler() -> Result<Json<Value>, StatusCode> {
Ok(Json(serde_json::json!({
"status": "ready",
"timestamp": chrono::Utc::now().to_rfc3339()
})))
}
#[utoipa::path(
get,
path = "/metrics",
responses(
(status = 200, description = "Prometheus metrics")
)
)]
async fn metrics_handler() -> Result<String, StatusCode> {
Ok("# OpenCrates metrics placeholder\n".to_string())
}
#[utoipa::path(
post,
path = "/api/v1/generate",
responses(
(status = 200, description = "Crate generation placeholder")
)
)]
async fn generate_handler(
State(_app_state): State<AppState>,
Json(_payload): Json<Value>,
) -> Result<Json<Value>, StatusCode> {
Err(StatusCode::NOT_IMPLEMENTED)
}
#[utoipa::path(
post,
path = "/api/v1/codex/generate",
request_body = CodeGenerationRequest,
responses(
(status = 200, description = "Code generated successfully", body = CodeGenerationResponse)
)
)]
async fn codex_generate_handler(
State(app_state): State<AppState>,
Json(request): Json<CodeGenerationRequest>,
) -> Result<Json<CodeGenerationResponse>, StatusCode> {
info!("Handling codex code generation request");
let provider = app_state
.core
.providers
.get_provider("codex")
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let gen_request = crate::providers::GenerationRequest {
spec: CrateSpec::default(), prompt: Some(request.instruction),
max_tokens: Some(4096),
model: None,
temperature: None,
context: request.context,
};
let response = provider.generate(&gen_request).await.map_err(|e| {
error!("Codex generation failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let response = CodeGenerationResponse {
code: response.preview,
metadata: GenerationMetadata {
model: "codex".to_string(), tokens_used: response.metrics.total_tokens,
timestamp: Utc::now().to_rfc3339(),
},
};
Ok(Json(response))
}
#[utoipa::path(
post,
path = "/api/v1/codex/apply-patch",
request_body = PatchApplicationRequest,
responses(
(status = 200, description = "Patch applied successfully", body = PatchApplicationResponse)
)
)]
async fn codex_apply_patch_handler(
State(app_state): State<AppState>,
Json(request): Json<PatchApplicationRequest>,
) -> Result<Json<PatchApplicationResponse>, StatusCode> {
info!("Handling codex patch application request");
let provider = app_state
.core
.providers
.get_provider("codex")
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let codex_provider = provider
.as_any()
.downcast_ref::<CodexProvider>()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let file_path = std::path::PathBuf::from(&request.file_path);
codex_provider
.apply_patch(&file_path, &request.patch_content)
.map_err(|e| {
error!("Failed to apply patch: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let response = PatchApplicationResponse {
success: true,
message: "Patch applied successfully".to_string(),
file_path: request.file_path,
};
Ok(Json(response))
}
#[utoipa::path(
get,
path = "/api/v1/codex/health",
responses(
(status = 200, description = "Codex provider is healthy")
)
)]
async fn codex_health_handler(
State(app_state): State<AppState>,
) -> Result<Json<Value>, StatusCode> {
info!("Handling codex health request");
let provider = app_state
.core
.providers
.get_provider("codex")
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let health = provider.health_check().await.map_err(|e| {
error!("Codex health check failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let status = if health { "ok" } else { "error" };
Ok(Json(serde_json::json!({ "status": status })))
}
#[utoipa::path(
get,
path = "/api/v1/codex/models",
responses(
(status = 200, description = "List of available models")
)
)]
async fn codex_models_handler(
State(_app_state): State<AppState>,
) -> Result<Json<Value>, StatusCode> {
info!("Handling codex models request");
let models = vec!["davinci-codex"];
Ok(Json(serde_json::json!({ "models": models })))
}
pub async fn create_test_server() -> Result<Router> {
let config = OpenCratesConfig::default();
create_app(config).await
}
pub async fn run_server(config: OpenCratesConfig) -> Result<()> {
info!("Starting OpenCrates server v{}", env!("CARGO_PKG_VERSION"));
let app = create_app(config).await?;
let host = "127.0.0.1";
let port = 8080;
info!("Server listening on http://{}:{}", host, port);
let addr = SocketAddr::new(host.parse().unwrap(), port);
let listener = TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
pub use crate::utils::fastapi_integration::create_router as build_router;
async fn request_id_middleware(
mut request: axum::http::Request<axum::body::Body>,
next: Next,
) -> Result<Response, StatusCode> {
if !request.headers().contains_key("x-request-id") {
let request_id = Uuid::new_v4().to_string();
request.headers_mut().insert(
"x-request-id",
request_id
.parse()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
);
}
Ok(next.run(request).await)
}
async fn rate_limit_middleware(
request: axum::http::Request<axum::body::Body>,
next: Next,
) -> Result<Response, StatusCode> {
Ok(next.run(request).await)
}
async fn auth_middleware(
request: axum::http::Request<axum::body::Body>,
next: Next,
) -> Result<Response, StatusCode> {
if let Some(_auth_header) = request.headers().get("authorization") {
}
Ok(next.run(request).await)
}
#[utoipa::path(
post,
path = "/api/v1/generate",
request_body = GenerateRequest,
responses(
(status = 200, description = "Crate generated successfully", body = GenerateResponse),
(status = 400, description = "Invalid request", body = ApiError),
(status = 500, description = "Internal server error", body = ApiError),
),
tag = "generation"
)]
async fn generate_crate(
State(state): State<AppState>,
headers: HeaderMap,
Json(request): Json<GenerateRequest>,
) -> Result<Json<GenerateResponse>, StatusCode> {
let request_id = headers
.get("x-request-id")
.and_then(|h| h.to_str().ok())
.unwrap_or("unknown")
.to_string();
let start_time = std::time::Instant::now();
let provider = request.provider.unwrap_or_else(|| "openai".to_string());
match state
.core
.generate_crate(
&request.name,
&request.description,
request.features.clone(),
"./output".into(),
)
.await
{
Ok(()) => {
let crate_spec = CrateSpec {
name: request.name.clone(),
description: request.description.clone(),
version: "0.1.0".to_string(),
authors: vec!["OpenCrates".to_string()],
license: Some("MIT".to_string()),
crate_type: CrateType::Library,
dependencies: std::collections::HashMap::new(),
dev_dependencies: std::collections::HashMap::new(),
features: request.features,
keywords: vec![],
categories: vec![],
repository: None,
homepage: None,
documentation: None,
readme: None,
rust_version: None,
edition: "2021".to_string(),
publish: true,
author: None,
template: None,
};
let generation_time = start_time.elapsed();
let response = GenerateResponse {
request_id,
crate_spec,
metrics: GenerationMetrics {
generation_time_ms: generation_time.as_millis() as u64,
tokens_used: None, cost_estimate: None,
provider,
},
message: "Crate generated successfully".to_string(),
};
Ok(Json(response))
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
#[utoipa::path(
post,
path = "/api/v1/analyze",
request_body = AnalyzeRequest,
responses(
(status = 200, description = "Analysis completed successfully", body = AnalysisResult),
(status = 400, description = "Invalid request", body = ApiError),
(status = 500, description = "Internal server error", body = ApiError),
),
tag = "analysis"
)]
async fn analyze_project(
State(state): State<AppState>,
Json(request): Json<AnalyzeRequest>,
) -> Result<Json<AnalysisResult>, StatusCode> {
let analysis = state.core.analyze_crate(&request.path).await.map_err(|e| {
error!("Failed to analyze project: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(analysis))
}
#[utoipa::path(
get,
path = "/system/health",
responses(
(status = 200, description = "System is healthy", body = HealthStatus),
(status = 503, description = "System is unhealthy", body = HealthStatus),
),
tag = "system"
)]
async fn health_check(State(state): State<AppState>) -> impl IntoResponse {
let health = state.health.get_health_info().await;
Json(health)
}
async fn detailed_health_check(State(state): State<AppState>) -> impl IntoResponse {
let health = state.health.get_health_info().await;
Json(health)
}
#[utoipa::path(
get,
path = "/system/status",
responses(
(status = 200, description = "System status retrieved successfully"),
),
tag = "system"
)]
async fn system_status(State(state): State<AppState>) -> Json<serde_json::Value> {
let status = state.core.get_system_status().await.unwrap();
Json(serde_json::json!(status))
}
#[utoipa::path(
get,
path = "/system/metrics",
responses(
(status = 200, description = "Metrics retrieved successfully"),
),
tag = "system"
)]
async fn metrics_endpoint(State(state): State<AppState>) -> impl IntoResponse {
match state.metrics.export_prometheus().await {
Ok(metrics) => (StatusCode::OK, metrics).into_response(),
Err(e) => {
error!("Failed to export metrics: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
}
}
}
#[utoipa::path(
get,
path = "/api/v1/templates",
responses(
(status = 200, description = "Templates retrieved successfully"),
),
tag = "templates"
)]
async fn list_templates(State(state): State<AppState>) -> Json<Vec<String>> {
let templates = state.core.template_manager.templates();
let template_names: Vec<String> = templates.keys().cloned().collect();
Json(template_names)
}
#[utoipa::path(
get,
path = "/api/v1/providers",
responses(
(status = 200, description = "Providers retrieved successfully"),
),
tag = "providers"
)]
async fn list_providers(State(_state): State<AppState>) -> Json<Vec<String>> {
let providers = vec!["openai".to_string(), "codex".to_string()]; Json(providers)
}
#[utoipa::path(
get,
path = "/providers/:provider/models",
responses(
(status = 200, description = "List of available models")
),
tag = "providers"
)]
async fn list_provider_models(
State(state): State<AppState>,
Path(provider): Path<String>,
) -> Json<Vec<String>> {
let models = state
.core
.providers()
.get_provider(&provider)
.map(|_p| {
serde_json::json!({
"name": "default_model",
"provider": "openai"
})
})
.map(|_info| vec!["default_model".to_string()])
.unwrap_or_default();
Json(models)
}
#[utoipa::path(
get,
path = "/version",
responses(
(status = 200, description = "Server version information")
),
tag = "system"
)]
async fn version_info() -> Json<serde_json::Value> {
Json(serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"build_timestamp": std::env::var("VERGEN_BUILD_TIMESTAMP").unwrap_or_else(|_| "unknown".to_string()),
"git_sha": std::env::var("VERGEN_GIT_SHA").unwrap_or_else(|_| "unknown".to_string())
}))
}
#[utoipa::path(
get,
path = "/admin/stats",
responses(
(status = 200, description = "Server statistics")
),
tag = "admin"
)]
async fn admin_stats(State(app_state): State<AppState>) -> Json<serde_json::Value> {
let statistics = serde_json::json!({
"uptime_seconds": app_state.start_time.elapsed().as_secs(),
"core_stats": "Not implemented"
});
Json(statistics)
}
#[utoipa::path(
get,
path = "/admin/config",
responses(
(status = 200, description = "Current server configuration")
),
tag = "admin"
)]
async fn get_config(State(state): State<AppState>) -> Json<OpenCratesConfig> {
Json(state.core.config.as_ref().clone())
}
#[utoipa::path(
put,
path = "/admin/config",
request_body = OpenCratesConfig,
responses(
(status = 200, description = "Server configuration updated successfully")
),
tag = "admin"
)]
async fn update_config(
State(_state): State<AppState>,
Json(_new_config): Json<OpenCratesConfig>,
) -> StatusCode {
if false {
let e = "update not implemented";
error!("Failed to update config: {}", e);
return StatusCode::INTERNAL_SERVER_ERROR;
}
StatusCode::OK
}
#[utoipa::path(
post,
path = "/admin/cache/clear",
responses(
(status = 200, description = "Server cache cleared")
),
tag = "admin"
)]
async fn clear_cache(State(_state): State<AppState>) -> StatusCode {
info!("Cache clear requested");
StatusCode::OK
}
async fn root_handler() -> Html<&'static str> {
Html(
r#"
<!DOCTYPE html>
<html>
<head>
<title>OpenCrates API</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #ff6b35; }
a { color: #007acc; text-decoration: none; }
a:hover { text-decoration: underline; }
.links { margin: 20px 0; }
.links a { display: inline-block; margin-right: 20px; padding: 10px 15px;
background: #f0f0f0; border-radius: 5px; }
</style>
</head>
<body>
<h1> OpenCrates API Server</h1>
<p>Enterprise-grade AI-powered Rust development companion</p>
<div class="links">
<a href="/docs"> Swagger UI</a>
<a href="/redoc"> ReDoc</a>
<a href="/system/health">Health Check</a>
<a href="/system/status">System Status</a>
<a href="/system/metrics">Metrics</a>
</div>
<p>Version: 3.0.0 | Status: Running</p>
</body>
</html>
"#,
)
}