Readme
This commit is contained in:
1 parent
536d5b14d8
commit
427a824e45
2 files changed
+263
-2
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user