use crate::error::{Error, Result};
use crate::handler::ApiHandlerInventory;
use axum::{
body::Body,
extract::{FromRequestParts, Path},
response::{IntoResponse, Response},
routing::{on, MethodFilter},
Router,
};
use axum::http::{Request, HeaderMap};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use utoipa::openapi::{OpenApi, PathItem};
async fn extract_and_merge_params(req: Request<Body>) -> std::result::Result<(Value, HeaderMap, Option<String>, Vec<u8>, Option<String>), Response> {
let (mut parts, body) = req.into_parts();
let path_params: HashMap<String, String> =
match Path::<HashMap<String, String>>::from_request_parts(&mut parts, &()).await {
Ok(path) => path.0,
Err(e) => return Err(e.into_response()),
};
let mut merged_params = serde_json::to_value(path_params)
.unwrap_or_else(|_| Value::Object(Default::default()));
let raw_query: Option<String> = parts.uri.query().map(|s| s.to_string());
if let Some(query_str) = raw_query.as_deref() {
if let Ok(pairs) = serde_urlencoded::from_str::<Vec<(String, String)>>(query_str) {
if let Some(merged) = merged_params.as_object_mut() {
for (k, v) in pairs {
if let Ok(n) = v.parse::<f64>() {
merged.insert(k, serde_json::json!(n));
} else if v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("false") {
merged.insert(k, serde_json::json!(v.eq_ignore_ascii_case("true")));
} else {
merged.insert(k, Value::String(v));
}
}
}
}
}
let headers = parts.headers.clone();
let content_type_opt: Option<String> = headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
return Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to read request body: {}", e),
)
.into_response())
}
};
Ok((merged_params, headers, raw_query, body_bytes, content_type_opt))
}
#[derive(Default, Clone)]
pub struct RestRouterBuilder {
openapi: Option<OpenApi>,
router_state: Option<Arc<dyn std::any::Any + Send + Sync>>,
}
impl RestRouterBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn openapi(mut self, openapi: OpenApi) -> Self {
self.openapi = Some(openapi);
self
}
pub fn with_state<S: Send + Sync + 'static>(mut self, state: S) -> Self {
self.router_state = Some(Arc::new(state));
self
}
pub fn build<S: Send + Sync + Clone + 'static>(self) -> Result<Router<S>> {
let openapi = self.openapi.ok_or_else(|| {
Error::SpecError("OpenAPI document not provided".to_string())
})?;
let mut handler_map: std::collections::HashMap<&'static str, fn(&serde_json::Value, HeaderMap, Option<String>, Option<Arc<dyn std::any::Any + Send + Sync>>, Vec<u8>, Option<String>) -> crate::handler::DynHandlerFuture> = std::collections::HashMap::new();
for inv in inventory::iter::<ApiHandlerInventory> {
handler_map.insert(inv.operation_id, inv.handler);
}
{
let has_cb2 = handler_map.contains_key("connections_callback_v2");
eprintln!("[service_kit][debug] inventory.has(callback_v2)={}", has_cb2);
}
let mut router: Router<S> = Router::new();
let router_state_outer = self.router_state.clone();
for (path, path_item) in openapi.paths.paths.iter() {
for (method, operation) in operations_from_path_item(path_item) {
if path == "/api/v1/connections/callback-v2" {
eprintln!(
"[service_kit][debug] scanning path='{}' method='{:?}' op_id={:?}",
path,
method,
operation.operation_id
);
}
if let Some(op_id) = operation.operation_id.as_deref() {
if let Some(handler_fn) = handler_map.get(op_id) {
if path == "/api/v1/connections/callback-v2" {
eprintln!(
"[service_kit][debug] handler found for op_id='{}' -> registering route",
op_id
);
}
let handler_fn = *handler_fn;
let router_state_for_route = router_state_outer.clone();
let path_for_handler = path.clone();
let route_handler = move |req: Request<Body>| async move {
if path_for_handler == "/api/v1/connections/callback-v2" {
let m = req.method().clone();
eprintln!(
"[service_kit][debug] route_handler invoked path='{}' method='{}'",
path_for_handler,
m
);
}
match extract_and_merge_params(req).await {
Ok((params, headers, raw_query, body_bytes, content_type)) => {
let router_state_cloned = router_state_for_route.clone();
match handler_fn(¶ms, headers, raw_query, router_state_cloned, body_bytes, content_type).await {
Ok(resp) => resp,
Err(e) => e.into_response(),
}
},
Err(response) => response,
}
};
let axum_path = path.clone();
router = router.route(&axum_path, on(method, route_handler));
} else {
if path == "/api/v1/connections/callback-v2" {
eprintln!(
"[service_kit][debug] handler NOT found for op_id='{}' at path='{}' method='{:?}'",
op_id,
path,
method
);
}
eprintln!(
"[service_kit] warning: handler not found for operation_id '{}', path '{}', method '{:?}'",
op_id, path, method
);
}
}
}
}
Ok(router)
}
}
fn operations_from_path_item(path_item: &PathItem) -> Vec<(MethodFilter, &utoipa::openapi::path::Operation)> {
let mut operations = Vec::new();
if let Some(op) = &path_item.get { operations.push((MethodFilter::GET, op)); }
if let Some(op) = &path_item.post { operations.push((MethodFilter::POST, op)); }
if let Some(op) = &path_item.put { operations.push((MethodFilter::PUT, op)); }
if let Some(op) = &path_item.delete { operations.push((MethodFilter::DELETE, op)); }
if let Some(op) = &path_item.patch { operations.push((MethodFilter::PATCH, op)); }
operations
}