251 lines
6.2 KiB
Markdown
251 lines
6.2 KiB
Markdown
# 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.
|