use axum::{ extract::{rejection::JsonRejection, State}, http::StatusCode, routing::get, Json, Router, }; use chrono::Utc; use rusqlite::{params, Connection}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::{ collections::HashMap, fs, sync::{Arc, Mutex}, }; use uuid::Uuid; #[derive(Clone)] struct AppState { db: Arc>, schema: Arc, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] enum ValidationMode { Strict, Loose, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] enum DataType { String, Integer, Number, Boolean, Object, Array, Null, } #[derive(Debug, Deserialize)] struct FieldSpec { #[serde(rename = "type")] data_type: DataType, } #[derive(Debug, Deserialize)] struct SchemaConfig { mode: ValidationMode, fields: HashMap, } #[derive(Debug, Serialize, Deserialize)] struct ApiResponse { uuid: Uuid, date: String, data: Value, } #[derive(Debug, Deserialize)] struct CreateRequest { data: Value, } type ApiError = (StatusCode, String); fn bad_request(message: impl Into) -> ApiError { (StatusCode::BAD_REQUEST, message.into()) } fn internal_error() -> ApiError { ( StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string(), ) } fn matches_type(value: &Value, expected: &DataType) -> bool { match expected { DataType::String => value.is_string(), DataType::Integer => { value.as_i64().is_some() || value.as_u64().is_some() } DataType::Number => value.is_number(), DataType::Boolean => value.is_boolean(), DataType::Object => value.is_object(), DataType::Array => value.is_array(), DataType::Null => value.is_null(), } } fn validate_data( data: &Value, schema: &SchemaConfig, ) -> Result<(), String> { let object = data .as_object() .ok_or("data must be a JSON object")?; // Every configured field must exist and have the correct type. for (key, spec) in &schema.fields { let value = object .get(key) .ok_or_else(|| format!("Missing required field: {key}"))?; if !matches_type(value, &spec.data_type) { return Err(format!( "Field '{key}' has the wrong type; expected {}", match spec.data_type { DataType::String => "string", DataType::Integer => "integer", DataType::Number => "number", DataType::Boolean => "boolean", DataType::Object => "object", DataType::Array => "array", DataType::Null => "null", } )); } } // Strict mode rejects any fields not defined in the config. if let ValidationMode::Strict = schema.mode { for key in object.keys() { if !schema.fields.contains_key(key) { return Err(format!("Unknown field: {key}")); } } } Ok(()) } async fn create_record( State(state): State, payload: Result, JsonRejection>, ) -> Result<(StatusCode, Json), ApiError> { let Json(request) = payload.map_err(|_| bad_request("Invalid JSON request body"))?; validate_data(&request.data, &state.schema) .map_err(bad_request)?; let record = ApiResponse { uuid: Uuid::new_v4(), date: Utc::now().to_rfc3339(), data: request.data, }; let data_json = serde_json::to_string(&record.data).map_err(|_| internal_error())?; let db = state.db.lock().map_err(|_| internal_error())?; db.execute( "INSERT INTO records (uuid, date, data) VALUES (?1, ?2, ?3)", params![ record.uuid.to_string(), record.date, data_json, ], ) .map_err(|_| internal_error())?; Ok((StatusCode::CREATED, Json(record))) } async fn list_records( State(state): State, ) -> Result>, ApiError> { let db = state.db.lock().map_err(|_| internal_error())?; let mut statement = db .prepare( "SELECT uuid, date, data FROM records ORDER BY date DESC LIMIT 100", ) .map_err(|_| internal_error())?; let rows = statement .query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, )) }) .map_err(|_| internal_error())?; let mut records = Vec::new(); for row in rows { let (uuid, date, data_json) = row.map_err(|_| internal_error())?; let uuid = Uuid::parse_str(&uuid).map_err(|_| internal_error())?; let data: Value = serde_json::from_str(&data_json) .map_err(|_| internal_error())?; records.push(ApiResponse { uuid, date, data }); } Ok(Json(records)) } #[tokio::main] async fn main() { let config_text = fs::read_to_string("config.json") .expect("Failed to read config.json"); let schema: SchemaConfig = serde_json::from_str(&config_text) .expect("Invalid config.json"); let db = Connection::open("messages.db") .expect("Failed to open SQLite database"); db.execute_batch( "CREATE TABLE IF NOT EXISTS records ( uuid TEXT PRIMARY KEY, date TEXT NOT NULL, data TEXT NOT NULL );", ) .expect("Failed to initialize database"); let state = AppState { db: Arc::new(Mutex::new(db)), schema: Arc::new(schema), }; let app = Router::new() .route("/api/records", get(list_records).post(create_record)) .with_state(state); let addr = "127.0.0.1:3000"; let listener = tokio::net::TcpListener::bind(addr) .await .expect("Failed to bind address"); println!("Listening on http://{addr}"); axum::serve(listener, app) .await .expect("HTTP server failed"); }