# 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.