state before logs!

This commit is contained in:
Kalzu Rekku committed 2026-10-25 10:56:50 +02:00
commit 857f21b319
5 files changed
+1231

No files matched your search

+255
View File
@@ -0,0 +1,255 @@
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<Mutex<Connection>>,
schema: Arc<SchemaConfig>,
}
#[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<String, FieldSpec>,
}
#[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<String>) -> 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<AppState>,
payload: Result<Json<CreateRequest>, JsonRejection>,
) -> Result<(StatusCode, Json<ApiResponse>), 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<AppState>,
) -> Result<Json<Vec<ApiResponse>>, 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");
}