Skip to content

Commit fac4d2c

Browse files
committed
Add schema validation docs, Unix socket client docs, and example
Document JSON Schema validation with .schema() in README with usage examples for required fields, enums, and nested objects. Add Unix socket client-level configuration section. Update build options table. Add server_schema_example.cpp demonstrating schema validation with nested objects, enums, arrays, and curl commands to test.
1 parent 3d2c610 commit fac4d2c

3 files changed

Lines changed: 192 additions & 0 deletions

File tree

README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,52 @@ server.serve_static("/app", "/var/www/app", "app.html");
236236
server.serve_static("/assets", "/var/www/assets", "");
237237
```
238238

239+
### JSON Schema Validation
240+
241+
Validate request bodies against [JSON Schema](https://json-schema.org/) using [Valijson](https://github.com/tristanpenman/valijson). Chain `.schema()` on any route to enforce validation before the handler is called. Invalid requests get a `400 Bad Request` with error details.
242+
243+
```cpp
244+
server.post("/api/users", [](nlohmann::json& json, auto& res) {
245+
// json is already validated against the schema
246+
res.json({{"created", true}, {"name", json["name"]}});
247+
}).schema({
248+
{"type", "object"},
249+
{"required", {"name", "email"}},
250+
{"properties", {
251+
{"name", {{"type", "string"}}},
252+
{"email", {{"type", "string"}}},
253+
{"age", {{"type", "integer"}, {"minimum", 0}}}
254+
}}
255+
});
256+
```
257+
258+
Works with all JSON handler signatures and HTTP methods:
259+
260+
```cpp
261+
// With request access
262+
server.put("/api/users/:id", [](auto& req, nlohmann::json& json, auto& res) {
263+
res.json({{"updated", true}, {"id", req["id"]}});
264+
}).schema({
265+
{"type", "object"},
266+
{"properties", {
267+
{"name", {{"type", "string"}}}
268+
}}
269+
});
270+
271+
// Enum validation
272+
server.post("/api/status", [](nlohmann::json& json, auto& res) {
273+
res.json({{"status", json["status"]}});
274+
}).schema({
275+
{"type", "object"},
276+
{"required", {"status"}},
277+
{"properties", {
278+
{"status", {{"type", "string"}, {"enum", {"active", "inactive", "pending"}}}}
279+
}}
280+
});
281+
```
282+
283+
Valijson is enabled by default. Disable with `-DTHINGER_HTTP_ENABLE_VALIJSON=OFF`.
284+
239285
### CORS
240286

241287
```cpp
@@ -294,13 +340,36 @@ auto res = client.put(url, body, content_type);
294340
auto res = client.del(url);
295341
```
296342

343+
### Unix Socket
344+
345+
Connect to services via Unix domain sockets (e.g., Docker, systemd). Configure once on the client — all methods work transparently:
346+
347+
```cpp
348+
thinger::http::client client;
349+
client.unix_socket("/var/run/docker.sock");
350+
351+
// Same API as TCP — the socket is used automatically
352+
auto containers = client.get("http://localhost/containers/json");
353+
auto result = client.post("http://localhost/containers/create",
354+
R"({"Image": "nginx"})", "application/json");
355+
```
356+
357+
Also available via the request builder for per-request control:
358+
359+
```cpp
360+
auto res = client.request("http://localhost/containers/json")
361+
.unix_socket("/var/run/docker.sock")
362+
.get();
363+
```
364+
297365
### Configuration
298366

299367
```cpp
300368
client.timeout(std::chrono::seconds(30));
301369
client.follow_redirects(true);
302370
client.max_redirects(10);
303371
client.verify_ssl(false); // Disable SSL verification
372+
client.unix_socket("/path/to/socket"); // Unix domain socket
304373
```
305374

306375
### Request Builder (Fluent API)
@@ -663,6 +732,8 @@ thinger::logging::set_logger(logger);
663732
| CMake Option | Default | Description |
664733
|--------------|---------|-------------|
665734
| `THINGER_HTTP_ENABLE_LOGGING` | `ON` | Enable spdlog integration |
735+
| `THINGER_HTTP_ENABLE_VALIJSON` | `ON` | Enable JSON Schema validation |
736+
| `THINGER_HTTP_ENABLE_SSL` | `ON` | Enable SSL/TLS support |
666737
| `THINGER_HTTP_BUILD_TESTS` | `ON` | Build test suite |
667738
| `THINGER_HTTP_BUILD_EXAMPLES` | `OFF` | Build examples |
668739

examples/http_server/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ target_link_libraries(server_upload_example PRIVATE thinger::http)
4848
add_executable(server_streaming_upload_example server_streaming_upload_example.cpp)
4949
target_link_libraries(server_streaming_upload_example PRIVATE thinger::http)
5050

51+
# Server Schema Validation example (JSON Schema with Valijson)
52+
add_executable(server_schema_example server_schema_example.cpp)
53+
target_link_libraries(server_schema_example PRIVATE thinger::http)
54+
5155
# Install examples
5256
install(TARGETS simple_http_server simple_https_server advanced_http_server
5357
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}/examples
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#include <thinger/http_server.hpp>
2+
#include <iostream>
3+
4+
using namespace thinger;
5+
6+
// This example demonstrates JSON Schema validation on route handlers.
7+
// Invalid requests are automatically rejected with 400 Bad Request.
8+
9+
int main(int argc, char* argv[]) {
10+
http::server server;
11+
12+
// POST /api/users - Create a user with required fields
13+
server.post("/api/users", [](nlohmann::json& json, http::response& res) {
14+
// json is already validated — guaranteed to have name and email as strings
15+
res.json({
16+
{"created", true},
17+
{"name", json["name"]},
18+
{"email", json["email"]}
19+
}, http::http_response::status::created);
20+
}).schema({
21+
{"type", "object"},
22+
{"required", {"name", "email"}},
23+
{"properties", {
24+
{"name", {{"type", "string"}, {"minLength", 1}}},
25+
{"email", {{"type", "string"}}},
26+
{"age", {{"type", "integer"}, {"minimum", 0}, {"maximum", 150}}}
27+
}},
28+
{"additionalProperties", false}
29+
});
30+
31+
// PUT /api/users/:id - Update a user (partial update allowed)
32+
server.put("/api/users/:id", [](http::request& req, nlohmann::json& json, http::response& res) {
33+
res.json({
34+
{"updated", true},
35+
{"id", req["id"]},
36+
{"fields", json}
37+
});
38+
}).schema({
39+
{"type", "object"},
40+
{"properties", {
41+
{"name", {{"type", "string"}, {"minLength", 1}}},
42+
{"email", {{"type", "string"}}},
43+
{"age", {{"type", "integer"}, {"minimum", 0}}}
44+
}}
45+
});
46+
47+
// POST /api/orders - Nested object with enum validation
48+
server.post("/api/orders", [](nlohmann::json& json, http::response& res) {
49+
res.json({
50+
{"order_id", "ORD-001"},
51+
{"status", json["status"]},
52+
{"items_count", json["items"].size()}
53+
}, http::http_response::status::created);
54+
}).schema({
55+
{"type", "object"},
56+
{"required", {"items", "status", "shipping"}},
57+
{"properties", {
58+
{"status", {
59+
{"type", "string"},
60+
{"enum", {"pending", "confirmed", "shipped"}}
61+
}},
62+
{"items", {
63+
{"type", "array"},
64+
{"minItems", 1},
65+
{"items", {
66+
{"type", "object"},
67+
{"required", {"product", "quantity"}},
68+
{"properties", {
69+
{"product", {{"type", "string"}}},
70+
{"quantity", {{"type", "integer"}, {"minimum", 1}}}
71+
}}
72+
}}
73+
}},
74+
{"shipping", {
75+
{"type", "object"},
76+
{"required", {"city", "country"}},
77+
{"properties", {
78+
{"city", {{"type", "string"}}},
79+
{"country", {{"type", "string"}}},
80+
{"zip", {{"type", "string"}}}
81+
}}
82+
}}
83+
}}
84+
});
85+
86+
// Endpoint without schema — accepts any JSON
87+
server.post("/api/raw", [](nlohmann::json& json, http::response& res) {
88+
res.json({{"received", true}, {"data", json}});
89+
});
90+
91+
uint16_t port = 8090;
92+
if (argc > 1) port = std::stoi(argv[1]);
93+
94+
std::cout << "Schema Validation Server starting on port " << port << std::endl;
95+
std::cout << "\nTry these requests:\n" << std::endl;
96+
97+
std::cout << "# Valid user creation:" << std::endl;
98+
std::cout << R"(curl -X POST http://localhost:)" << port
99+
<< R"(/api/users -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com","age":30}')" << std::endl;
100+
101+
std::cout << "\n# Missing required field (returns 400):" << std::endl;
102+
std::cout << R"(curl -X POST http://localhost:)" << port
103+
<< R"(/api/users -H "Content-Type: application/json" -d '{"name":"Alice"}')" << std::endl;
104+
105+
std::cout << "\n# Wrong type (returns 400):" << std::endl;
106+
std::cout << R"(curl -X POST http://localhost:)" << port
107+
<< R"(/api/users -H "Content-Type: application/json" -d '{"name":123,"email":"test@test.com"}')" << std::endl;
108+
109+
std::cout << "\n# Valid order with nested objects:" << std::endl;
110+
std::cout << R"(curl -X POST http://localhost:)" << port
111+
<< R"(/api/orders -H "Content-Type: application/json" -d '{"status":"pending","items":[{"product":"Widget","quantity":3}],"shipping":{"city":"Madrid","country":"Spain"}}')" << std::endl;
112+
113+
std::cout << std::endl;
114+
115+
server.start("0.0.0.0", port);
116+
return 0;
117+
}

0 commit comments

Comments
 (0)