From 427a824e4584b07c79e0075f3b971332009705b3 Mon Sep 17 00:00:00 2001 From: Kalzu Rekku Date: Sun, 25 Oct 2026 18:27:35 +0200 Subject: [PATCH] Readme --- README.md | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 15 +++- 2 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5a89fa --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +# Dynamic Schema Records API + +A lightweight, high-performance JSON records service built with **Rust**, **Axum**, and **SQLite**. + +The service validates incoming JSON payloads against a dynamic schema specification defined in a JSON configuration file. Schemas support validation modes (`strict` vs. `loose`), data type constraints, and exact forced values (e.g., pinning API payload versions). Schemas can be reloaded at runtime without restarting the server. + +--- + +## Features + +- **Dynamic Schema Validation**: + - Enforce required types: `string`, `integer`, `number`, `boolean`, `object`, `array`, `null`. + - Force exact values on any field (e.g., pin `"version": 2`). + - Validation modes: + - **`strict`**: All defined fields are required, and unknown fields are rejected. + - **`loose`**: All defined fields are required, but additional unknown fields are accepted. +- **Hot-Reloadable Configuration**: Reload `config.json` via an HTTP admin endpoint with atomic swap and rollback on error. +- **Persistent Storage**: Validated records are stored with a generated UUID and ISO-8601 UTC timestamp in an SQLite database (`messages.db`). +- **Structured Logging**: Built with `tracing` outputting to standard output. + +--- + +## Prerequisites + +- [Rust toolchain](https://rustup.rs/) (1.75+ recommended) +- SQLite development libraries (if linking dynamically) + +--- + +## Configuration + +The service looks for a configuration file at the path specified by the `APP_CONFIG` environment variable, defaulting to `config.json` in the current working directory. + +### Example: `config.json` + +```json +{ + "mode": "strict", + "fields": { + "version": { + "type": "integer", + "value": 2 + }, + "message": { + "type": "string" + }, + "enabled": { + "type": "boolean" + } + } +} +``` + +### Schema Specification + +| Property | Type | Description | +| :--- | :--- | :--- | +| `mode` | `string` | `"strict"` or `"loose"`. | +| `fields` | `object` | Map of field names to validation specifications. Must contain at least one field. | +| `fields..type` | `string` | Expected JSON type: `string`, `integer`, `number`, `boolean`, `object`, `array`, `null`. | +| `fields..value` | *any* *(optional)* | Expected exact value. Rejects any request where the value does not strictly match. | + +--- + +## Getting Started + +### 1. Build and Run + +```bash +# Clone the repository and enter the directory +cd your-project-name + +# Create your config file +cat < config.json +{ + "mode": "strict", + "fields": { + "version": { + "type": "integer", + "value": 1 + }, + "message": { + "type": "string" + } + } +} +EOF + +# Run the server (default: 127.0.0.1:3000) +cargo run +``` + +To specify a custom configuration file path: + +```bash +APP_CONFIG=custom_config.json cargo run +``` + +--- + +## API Reference + +### Health / Service Info +- **Endpoint:** `GET /` or `GET /api/now` +- **Description:** Returns server timestamp and status payload. +- **Example Response:** + ```json + { + "uuid": "4fc736f8-45e0-47da-8da9-6c30fef6a72e", + "date": "2026-10-25T10:19:00Z", + "data": { + "version": 1, + "message": "Hello from Rust!" + } + } + ``` + +--- + +### Create Record +- **Endpoint:** `POST /api/records` +- **Headers:** `Content-Type: application/json` +- **Request Body:** + ```json + { + "data": { + "version": 1, + "message": "Hello, world!" + } + } + ``` +- **Response (`201 Created`):** + ```json + { + "uuid": "f3b392b9-dc81-42cb-b9ef-dc7e90aa7ce5", + "date": "2026-10-25T10:20:15Z", + "data": { + "version": 1, + "message": "Hello, world!" + } + } + ``` +- **Error Responses (`400 Bad Request`):** + - Missing field: `Missing required field: message` + - Wrong type: `Field 'version' must be of type integer` + - Value mismatch: `Field 'version' must equal 1` + - Unknown field (in strict mode): `Unknown field: extra_field` + +--- + +### List Records +- **Endpoint:** `GET /api/records` +- **Description:** Returns up to 100 most recent records ordered by `date DESC`. +- **Response (`200 OK`):** + ```json + [ + { + "uuid": "f3b392b9-dc81-42cb-b9ef-dc7e90aa7ce5", + "date": "2026-10-25T10:20:15Z", + "data": { + "version": 1, + "message": "Hello, world!" + } + } + ] + ``` + +--- + +### Get Active Configuration +- **Endpoint:** `GET /api/admin/config` +- **Description:** Inspect the currently running schema and mode. +- **Response (`200 OK`):** + ```json + { + "mode": "strict", + "fields": { + "version": { + "type": "integer", + "value": 1 + }, + "message": { + "type": "string" + } + } + } + ``` + +--- + +### Hot-Reload Configuration +- **Endpoint:** `POST /api/admin/reload-config` +- **Description:** Reads and validates the file at `APP_CONFIG`. If valid, atomically updates the active schema in memory. +- **Response (`200 OK`):** + ```json + { + "status": "ok", + "mode": "strict", + "fields": 2 + } + ``` +- **Error Response (`422 Unprocessable Entity`):** Returned if the configuration file is unreadable, invalid JSON, or contains no fields. The active schema remains untouched. + +--- + +## Example Usage Workflow + +```bash +# 1. Create a valid record +curl -i -X POST http://127.0.0.1:3000/api/records \ + -H "Content-Type: application/json" \ + -d '{"data": {"version": 1, "message": "First record"}}' + +# 2. Modify config.json (e.g. bump required version to 2) +# Update "value": 2 in config.json... + +# 3. Reload config without downtime +curl -i -X POST http://127.0.0.1:3000/api/admin/reload-config + +# 4. Old version requests are now rejected +curl -i -X POST http://127.0.0.1:3000/api/records \ + -H "Content-Type: application/json" \ + -d '{"data": {"version": 1, "message": "Rejected"}}' +# -> HTTP 400: Field 'version' must equal 2 + +# 5. New version requests succeed +curl -i -X POST http://127.0.0.1:3000/api/records \ + -H "Content-Type: application/json" \ + -d '{"data": {"version": 2, "message": "Accepted"}}' +``` + +--- + +## Database Architecture + +On startup, the application creates a local SQLite database named `messages.db` with the following schema: + +```sql +CREATE TABLE IF NOT EXISTS records ( + uuid TEXT PRIMARY KEY, + date TEXT NOT NULL, + data TEXT NOT NULL +); +``` +This project is licensed under the [MIT License](LICENSE) (or choose your preferred license). + + +## License + +This project is licensed under the MIT License. diff --git a/src/main.rs b/src/main.rs index 831135a..c27d42b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ - use axum::{ extract::{rejection::JsonRejection, State}, http::StatusCode, @@ -34,6 +33,9 @@ enum ValidationMode { struct FieldSpec { #[serde(rename = "type")] data_type: DataType, + + #[serde(default, skip_serializing_if = "Option::is_none")] + value: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -122,6 +124,15 @@ fn validate_data(data: &Value, schema: &SchemaConfig) -> Result<(), String> { type_name(&spec.data_type) )); } + + if let Some(expected) = &spec.value { + if value != expected { + return Err(format!( + "Field '{key}' must equal {}", + expected + )); + } + } } if matches!(schema.mode, ValidationMode::Strict) { @@ -405,4 +416,4 @@ async fn main() { axum::serve(listener, app) .await .expect("HTTP server failed"); -} +} \ No newline at end of file