//! REST API request handlers
use axum::{
extract::{Path, Query, State},
http::StatusCode,
Json,
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use utoipa::ToSchema;
use crate::models::Worker;
use crate::api::{ApiResponse, ApiError};
use crate::matching::MatchResult;
use super::state::AppState;
/// Health check response
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct HealthResponse {
pub status: String,
pub service: String,
pub version: String,
}
/// Health check endpoint
#[utoipa::path(
get,
path = "/api/v1/health",
tag = "health",
responses(
(status = 200, description = "Service is healthy", body = HealthResponse)
)
)]
pub async fn health_check() -> impl IntoResponse {
Json(HealthResponse {
status: "healthy".to_string(),
service: "worker-service".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
})
}
/// Create worker request
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateWorkerRequest {
#[serde(flatten)]
pub worker: Worker,
}
/// Create a new worker
pub async fn create_worker(
State(_state): State<AppState>,
Json(payload): Json<Worker>,
) -> impl IntoResponse {
// TODO: Actually insert into database using Diesel
// For now, return the worker as-is
// In a real implementation:
// 1. Validate worker data
// 2. Check for duplicates using matcher
// 3. Insert into database
// 4. Index in search engine
// 5. Publish event to stream
(StatusCode::CREATED, Json(ApiResponse::success(payload)))
}
/// Get a worker by ID
#[utoipa::path(
get,
path = "/api/v1/workers/{id}",
tag = "workers",
params(
("id" = Uuid, Path, description = "Worker UUID")
),
responses(
(status = 200, description = "Worker found", body = ApiResponse<Worker>),
(status = 404, description = "Worker not found", body = ApiResponse<()>),
(status = 500, description = "Internal server error", body = ApiResponse<()>)
)
)]
pub async fn get_worker(
State(_state): State<AppState>,
Path(_id): Path<Uuid>,
) -> impl IntoResponse {
// TODO: Implement worker retrieval from database
// 1. Query database by UUID
// 2. Convert DbWorker to Worker model
// 3. Return worker or 404
(StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::<()>::error(
"NOT_IMPLEMENTED",
"Worker retrieval not yet implemented"
)))
}
/// Update a worker
#[utoipa::path(
put,
path = "/api/v1/workers/{id}",
tag = "workers",
params(
("id" = Uuid, Path, description = "Worker UUID")
),
request_body = Worker,
responses(
(status = 200, description = "Worker updated", body = ApiResponse<Worker>),
(status = 404, description = "Worker not found", body = ApiResponse<()>),
(status = 500, description = "Internal server error", body = ApiResponse<()>)
)
)]
pub async fn update_worker(
State(_state): State<AppState>,
Path(_id): Path<Uuid>,
Json(_payload): Json<Worker>,
) -> impl IntoResponse {
// TODO: Implement worker update
// 1. Verify worker exists
// 2. Update database record
// 3. Update search index
// 4. Publish update event
(StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::<()>::error(
"NOT_IMPLEMENTED",
"Worker update not yet implemented"
)))
}
/// Delete a worker (soft delete)
#[utoipa::path(
delete,
path = "/api/v1/workers/{id}",
tag = "workers",
params(
("id" = Uuid, Path, description = "Worker UUID")
),
responses(
(status = 204, description = "Worker deleted"),
(status = 404, description = "Worker not found", body = ApiResponse<()>),
(status = 500, description = "Internal server error", body = ApiResponse<()>)
)
)]
pub async fn delete_worker(
State(_state): State<AppState>,
Path(_id): Path<Uuid>,
) -> impl IntoResponse {
// TODO: Implement soft worker deletion
// 1. Set deleted_at timestamp
// 2. Optionally remove from search index
// 3. Publish deletion event
(StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::<()>::error(
"NOT_IMPLEMENTED",
"Worker deletion not yet implemented"
)))
}
/// Search query parameters
#[derive(Debug, Deserialize, ToSchema)]
pub struct SearchQuery {
/// Search query string
pub q: String,
/// Maximum number of results (default: 10, max: 100)
#[serde(default = "default_limit")]
pub limit: usize,
/// Use fuzzy search
#[serde(default)]
pub fuzzy: bool,
}
fn default_limit() -> usize {
10
}
/// Search results response
#[derive(Debug, Serialize, ToSchema)]
pub struct SearchResponse {
pub workers: Vec<Worker>,
pub total: usize,
pub query: String,
}
/// Search for workers
#[utoipa::path(
get,
path = "/api/v1/workers/search",
tag = "search",
params(
("q" = String, Query, description = "Search query"),
("limit" = Option<usize>, Query, description = "Maximum results (default: 10, max: 100)"),
("fuzzy" = Option<bool>, Query, description = "Enable fuzzy search")
),
responses(
(status = 200, description = "Search results", body = ApiResponse<SearchResponse>),
(status = 400, description = "Invalid query", body = ApiResponse<()>),
(status = 500, description = "Internal server error", body = ApiResponse<()>)
)
)]
pub async fn search_workers(
State(state): State<AppState>,
Query(params): Query<SearchQuery>,
) -> impl IntoResponse {
// Limit to max 100 results
let limit = params.limit.min(100);
// Perform search using search engine
let worker_ids = if params.fuzzy {
state.search_engine.fuzzy_search(¶ms.q, limit)
} else {
state.search_engine.search(¶ms.q, limit)
};
match worker_ids {
Ok(ids) => {
// TODO: Fetch full worker records from database
// For now, return empty list
let response = SearchResponse {
workers: vec![],
total: ids.len(),
query: params.q,
};
(StatusCode::OK, Json(ApiResponse::success(response)))
}
Err(e) => {
let error = ApiResponse::<SearchResponse>::error(
"SEARCH_ERROR",
format!("Search failed: {}", e)
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
}
}
}
/// Match request payload
#[derive(Debug, Deserialize, ToSchema)]
pub struct MatchRequest {
/// Worker to match against existing records
#[serde(flatten)]
pub worker: Worker,
/// Minimum match score threshold (0.0 to 1.0)
#[serde(default)]
pub threshold: Option<f64>,
/// Maximum number of matches to return
#[serde(default = "default_match_limit")]
pub limit: usize,
}
fn default_match_limit() -> usize {
10
}
/// Match result with score
#[derive(Debug, Serialize, ToSchema)]
pub struct MatchResponse {
pub worker: Worker,
pub score: f64,
pub quality: String,
}
/// Match results response
#[derive(Debug, Serialize, ToSchema)]
pub struct MatchResultsResponse {
pub matches: Vec<MatchResponse>,
pub total: usize,
}
/// Match a worker against existing records
#[utoipa::path(
post,
path = "/api/v1/workers/match",
tag = "matching",
request_body = MatchRequest,
responses(
(status = 200, description = "Matching results", body = ApiResponse<MatchResultsResponse>),
(status = 400, description = "Invalid request", body = ApiResponse<()>),
(status = 500, description = "Internal server error", body = ApiResponse<()>)
)
)]
pub async fn match_worker(
State(state): State<AppState>,
Json(payload): Json<MatchRequest>,
) -> impl IntoResponse {
// Use search engine to get candidate workers (blocking)
let family_name = &payload.worker.name.family;
let birth_year = payload.worker.birth_date.map(|d| d.year());
let candidate_ids = state.search_engine
.search_by_name_and_year(family_name, birth_year, 100);
match candidate_ids {
Ok(ids) => {
// TODO: Fetch full worker records from database
// TODO: Run matcher.find_matches() on candidates
// For now, return empty results
let response = MatchResultsResponse {
matches: vec![],
total: ids.len(),
};
(StatusCode::OK, Json(ApiResponse::success(response)))
}
Err(e) => {
let error = ApiResponse::<MatchResultsResponse>::error(
"MATCH_ERROR",
format!("Matching failed: {}", e)
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
}
}
}