This commit is contained in:
Kalzu Rekku committed 2026-10-25 18:27:35 +02:00
1 parent 536d5b14d8
commit 427a824e45
2 files changed
+262 -1

No files matched your search

+250
View File
@@ -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.<name>.type` | `string` | Expected JSON type: `string`, `integer`, `number`, `boolean`, `object`, `array`, `null`. |
| `fields.<name>.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 <<EOF > 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.
+12 -1
View File
@@ -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<Value>,
}
#[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) {