146 lines
3.9 KiB
Rust
146 lines
3.9 KiB
Rust
//! Osiris Server - Generic OpenAPI REST server for Osiris data structures
|
|
//!
|
|
//! Provides generic CRUD operations for all Osiris structs via REST API.
|
|
//! Routes follow pattern: GET /api/:struct_name/:id
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Json},
|
|
routing::get,
|
|
Router,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{json, Value};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tower_http::cors::{Any, CorsLayer};
|
|
use tracing::{info, warn};
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
// In a real implementation, this would be a Redis connection pool
|
|
// For now, we'll use an in-memory store for demonstration
|
|
store: Arc<tokio::sync::RwLock<HashMap<String, HashMap<String, Value>>>>,
|
|
}
|
|
|
|
impl AppState {
|
|
fn new() -> Self {
|
|
Self {
|
|
store: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
// Initialize tracing
|
|
tracing_subscriber::fmt()
|
|
.with_target(false)
|
|
.compact()
|
|
.init();
|
|
|
|
let state = AppState::new();
|
|
|
|
// Build router
|
|
let app = Router::new()
|
|
.route("/health", get(health_check))
|
|
.route("/api/:struct_name", get(list_structs))
|
|
.route("/api/:struct_name/:id", get(get_struct))
|
|
.layer(
|
|
CorsLayer::new()
|
|
.allow_origin(Any)
|
|
.allow_methods(Any)
|
|
.allow_headers(Any),
|
|
)
|
|
.with_state(state);
|
|
|
|
let addr = "0.0.0.0:8081";
|
|
info!("🚀 Osiris Server starting on {}", addr);
|
|
info!("📖 API Documentation: http://localhost:8081/health");
|
|
|
|
let listener = tokio::net::TcpListener::bind(addr)
|
|
.await
|
|
.expect("Failed to bind address");
|
|
|
|
axum::serve(listener, app)
|
|
.await
|
|
.expect("Server failed");
|
|
}
|
|
|
|
/// Health check endpoint
|
|
async fn health_check() -> impl IntoResponse {
|
|
Json(json!({
|
|
"status": "healthy",
|
|
"service": "osiris-server",
|
|
"version": "0.1.0"
|
|
}))
|
|
}
|
|
|
|
/// Generic GET endpoint for a single struct by ID
|
|
/// GET /api/:struct_name/:id
|
|
async fn get_struct(
|
|
State(state): State<AppState>,
|
|
Path((struct_name, id)): Path<(String, String)>,
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
info!("GET /api/{}/{}", struct_name, id);
|
|
|
|
let store = state.store.read().await;
|
|
|
|
if let Some(struct_store) = store.get(&struct_name) {
|
|
if let Some(data) = struct_store.get(&id) {
|
|
return Ok(Json(data.clone()));
|
|
}
|
|
}
|
|
|
|
warn!("Not found: {}/{}", struct_name, id);
|
|
Err((
|
|
StatusCode::NOT_FOUND,
|
|
format!("{}/{} not found", struct_name, id),
|
|
))
|
|
}
|
|
|
|
/// Generic LIST endpoint for all instances of a struct
|
|
/// GET /api/:struct_name?field=value
|
|
async fn list_structs(
|
|
State(state): State<AppState>,
|
|
Path(struct_name): Path<String>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Result<Json<Vec<Value>>, (StatusCode, String)> {
|
|
info!("GET /api/{} with params: {:?}", struct_name, params);
|
|
|
|
let store = state.store.read().await;
|
|
|
|
if let Some(struct_store) = store.get(&struct_name) {
|
|
let mut results: Vec<Value> = struct_store.values().cloned().collect();
|
|
|
|
// Apply filters if any
|
|
if !params.is_empty() {
|
|
results.retain(|item| {
|
|
params.iter().all(|(key, value)| {
|
|
item.get(key)
|
|
.and_then(|v| v.as_str())
|
|
.map(|v| v == value)
|
|
.unwrap_or(false)
|
|
})
|
|
});
|
|
}
|
|
|
|
return Ok(Json(results));
|
|
}
|
|
|
|
// Return empty array if struct type doesn't exist yet
|
|
Ok(Json(vec![]))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_health_check() {
|
|
let response = health_check().await.into_response();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
}
|
|
}
|