Clean up OpenAPI scripts

This commit is contained in:
gabdevele
2026-06-03 01:34:27 +02:00 Unverified
parent 74ff09fefe
commit 1043768da7
15 changed files with 379 additions and 48 deletions
+2 -1
View File
@@ -95,7 +95,8 @@ build/
.cache/ .cache/
.env .env
uploads/ uploads/
lab/.venv/ scripts/lab/.venv/
scripts/lab/__pycache__/
### CMake Patch ### ### CMake Patch ###
# External projects # External projects
+1 -1
View File
@@ -24,7 +24,7 @@
- Build: `cmake --build build` - Build: `cmake --build build`
- Test: `ctest --test-dir build --output-on-failure` - Test: `ctest --test-dir build --output-on-failure`
- OpenAPI: `cmake --build build --target openapi` - OpenAPI: `cmake --build build --target openapi`
- Lab frontend: `cd lab && uv run streamlit run app.py` - Lab frontend: `cd scripts/lab && uv run streamlit run app.py`
- Local services: `docker compose up -d` - Local services: `docker compose up -d`
- App: `./build/visione-backend` - App: `./build/visione-backend`
+2 -2
View File
@@ -77,13 +77,13 @@ set(OPENAPI_OUTPUT ${CMAKE_BINARY_DIR}/openapi/visione.openapi.json)
add_custom_command( add_custom_command(
OUTPUT ${OPENAPI_OUTPUT} OUTPUT ${OPENAPI_OUTPUT}
COMMAND ${Python3_EXECUTABLE} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/generate_openapi.py ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_openapi.py
--controllers ${CMAKE_CURRENT_SOURCE_DIR}/controllers --controllers ${CMAKE_CURRENT_SOURCE_DIR}/controllers
--out ${OPENAPI_OUTPUT} --out ${OPENAPI_OUTPUT}
--title "Visione API" --title "Visione API"
--version ${PROJECT_VERSION} --version ${PROJECT_VERSION}
DEPENDS DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/generate_openapi.py ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_openapi.py
${CONTROLLER_HEADERS} ${CONTROLLER_HEADERS}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
VERBATIM VERBATIM
+3 -3
View File
@@ -90,11 +90,11 @@ The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at
## Visione Lab ## Visione Lab
`lab/` is a small Streamlit frontend used only to test the backend visually `scripts/lab/` is a small Streamlit frontend used only to test the backend
while the real frontend is not ready. visually while the real frontend is not ready.
```bash ```bash
cd lab cd scripts/lab
uv sync uv sync
uv run streamlit run app.py uv run streamlit run app.py
``` ```
+90 -1
View File
@@ -6,23 +6,113 @@ class AdminController : public drogon::HttpController<AdminController>
{ {
public: public:
METHOD_LIST_BEGIN METHOD_LIST_BEGIN
/*
* openapi:
* tags: Admin
* summary: List categories
* description: Lists active report categories.
* auth: bearer
* body: none
* response: Report category list.
*/
ADD_METHOD_TO(AdminController::listCategories, ADD_METHOD_TO(AdminController::listCategories,
"/api/v1/admin/categories", "/api/v1/admin/categories",
drogon::Get); drogon::Get);
/*
* openapi:
* tags: Admin
* summary: Create category
* description: Creates a report category.
* auth: bearer
* body: json
* response: Created category result.
*/
ADD_METHOD_TO(AdminController::createCategory, ADD_METHOD_TO(AdminController::createCategory,
"/api/v1/admin/categories", "/api/v1/admin/categories",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Admin
* summary: List areas
* description: Lists configured municipal areas.
* auth: bearer
* body: none
* response: Area list.
*/
ADD_METHOD_TO(AdminController::listAreas, "/api/v1/admin/areas", drogon::Get); ADD_METHOD_TO(AdminController::listAreas, "/api/v1/admin/areas", drogon::Get);
/*
* openapi:
* tags: Admin
* summary: Create area
* description: Creates a municipal area with GeoJSON boundary.
* auth: bearer
* body: json
* response: Created area result.
*/
ADD_METHOD_TO(AdminController::createArea, "/api/v1/admin/areas", drogon::Post); ADD_METHOD_TO(AdminController::createArea, "/api/v1/admin/areas", drogon::Post);
/*
* openapi:
* tags: Admin
* summary: List teams
* description: Lists intervention teams.
* auth: bearer
* body: none
* response: Team list.
*/
ADD_METHOD_TO(AdminController::listTeams, "/api/v1/admin/teams", drogon::Get); ADD_METHOD_TO(AdminController::listTeams, "/api/v1/admin/teams", drogon::Get);
/*
* openapi:
* tags: Admin
* summary: Create team
* description: Creates an intervention team.
* auth: bearer
* body: json
* response: Created team result.
*/
ADD_METHOD_TO(AdminController::createTeam, "/api/v1/admin/teams", drogon::Post); ADD_METHOD_TO(AdminController::createTeam, "/api/v1/admin/teams", drogon::Post);
/*
* openapi:
* tags: Admin
* summary: Overview stats
* description: Returns report statistics for administration dashboards.
* auth: bearer
* body: none
* response: Aggregated report statistics.
*/
ADD_METHOD_TO(AdminController::overviewStats, ADD_METHOD_TO(AdminController::overviewStats,
"/api/v1/admin/stats/overview", "/api/v1/admin/stats/overview",
drogon::Get); drogon::Get);
/*
* openapi:
* tags: Admin
* summary: List users
* description: Lists platform users with role information.
* auth: bearer
* body: none
* response: User list.
*/
ADD_METHOD_TO(AdminController::listUsers, "/api/v1/admin/users", drogon::Get); ADD_METHOD_TO(AdminController::listUsers, "/api/v1/admin/users", drogon::Get);
/*
* openapi:
* tags: Admin
* summary: Update user roles
* description: Replaces the roles assigned to a user.
* auth: bearer
* body: json
* response: Role update result.
*/
ADD_METHOD_TO(AdminController::updateUserRoles, ADD_METHOD_TO(AdminController::updateUserRoles,
"/api/v1/admin/users/{1}/roles", "/api/v1/admin/users/{1}/roles",
drogon::Patch); drogon::Patch);
/*
* openapi:
* tags: Admin
* summary: Ban or unban user
* description: Updates a user's banned flag.
* auth: bearer
* body: json
* response: Ban update result.
*/
ADD_METHOD_TO(AdminController::updateUserBan, ADD_METHOD_TO(AdminController::updateUserBan,
"/api/v1/admin/users/{1}/ban", "/api/v1/admin/users/{1}/ban",
drogon::Patch); drogon::Patch);
@@ -61,4 +151,3 @@ class AdminController : public drogon::HttpController<AdminController>
std::function<void(const drogon::HttpResponsePtr &)> &&callback, std::function<void(const drogon::HttpResponsePtr &)> &&callback,
long long userId); long long userId);
}; };
+36
View File
@@ -6,9 +6,45 @@ class ApiController : public drogon::HttpController<ApiController>
{ {
public: public:
METHOD_LIST_BEGIN METHOD_LIST_BEGIN
/*
* openapi:
* tags: Technical
* summary: Health check
* description: Returns process health without checking dependencies.
* auth: public
* body: none
* response: Service health status.
*/
ADD_METHOD_TO(ApiController::health, "/api/v1/health", drogon::Get); ADD_METHOD_TO(ApiController::health, "/api/v1/health", drogon::Get);
/*
* openapi:
* tags: Technical
* summary: Readiness check
* description: Checks whether MySQL is reachable.
* auth: public
* body: none
* response: Dependency readiness status.
*/
ADD_METHOD_TO(ApiController::ready, "/api/v1/ready", drogon::Get); ADD_METHOD_TO(ApiController::ready, "/api/v1/ready", drogon::Get);
/*
* openapi:
* tags: Technical
* summary: OpenAPI document
* description: Returns the generated OpenAPI JSON document.
* auth: public
* body: none
* response: OpenAPI 3 JSON document.
*/
ADD_METHOD_TO(ApiController::openApi, "/api/v1/openapi.json", drogon::Get); ADD_METHOD_TO(ApiController::openApi, "/api/v1/openapi.json", drogon::Get);
/*
* openapi:
* tags: Technical
* summary: Swagger UI
* description: Serves a local Swagger UI page backed by openapi.json.
* auth: public
* body: none
* response: HTML documentation page.
*/
ADD_METHOD_TO(ApiController::docs, "/api/v1/docs", drogon::Get); ADD_METHOD_TO(ApiController::docs, "/api/v1/docs", drogon::Get);
METHOD_LIST_END METHOD_LIST_END
+54
View File
@@ -6,21 +6,75 @@ class AuthController : public drogon::HttpController<AuthController>
{ {
public: public:
METHOD_LIST_BEGIN METHOD_LIST_BEGIN
/*
* openapi:
* tags: Auth
* summary: Register citizen
* description: Creates a citizen account with personal data and password.
* auth: public
* body: json
* response: Registration result.
*/
ADD_METHOD_TO(AuthController::registerCitizen, ADD_METHOD_TO(AuthController::registerCitizen,
"/api/v1/auth/register", "/api/v1/auth/register",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Auth
* summary: Request OTP
* description: Sends a one-time code by email through MailHog locally.
* auth: public
* body: json
* response: OTP challenge result.
*/
ADD_METHOD_TO(AuthController::requestOtp, ADD_METHOD_TO(AuthController::requestOtp,
"/api/v1/auth/otp/request", "/api/v1/auth/otp/request",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Auth
* summary: Verify OTP
* description: Verifies an email OTP and creates an API token.
* auth: public
* body: json
* response: Bearer token when verification succeeds.
*/
ADD_METHOD_TO(AuthController::verifyOtp, ADD_METHOD_TO(AuthController::verifyOtp,
"/api/v1/auth/otp/verify", "/api/v1/auth/otp/verify",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Auth
* summary: Password login
* description: Password-based login fallback for local development.
* auth: public
* body: json
* response: Bearer token when credentials are valid.
*/
ADD_METHOD_TO(AuthController::loginWithPassword, ADD_METHOD_TO(AuthController::loginWithPassword,
"/api/v1/auth/login/password", "/api/v1/auth/login/password",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Auth
* summary: Change password
* description: Changes the authenticated user's password.
* auth: bearer
* body: json
* response: Password change result.
*/
ADD_METHOD_TO(AuthController::changePassword, ADD_METHOD_TO(AuthController::changePassword,
"/api/v1/auth/change-password", "/api/v1/auth/change-password",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Auth
* summary: Current user
* description: Returns the authenticated user's profile and permissions.
* auth: bearer
* body: none
* response: Authenticated user context.
*/
ADD_METHOD_TO(AuthController::me, "/api/v1/me", drogon::Get); ADD_METHOD_TO(AuthController::me, "/api/v1/me", drogon::Get);
METHOD_LIST_END METHOD_LIST_END
+90
View File
@@ -6,27 +6,117 @@ class ReportController : public drogon::HttpController<ReportController>
{ {
public: public:
METHOD_LIST_BEGIN METHOD_LIST_BEGIN
/*
* openapi:
* tags: Reports
* summary: Create report
* description: Creates a citizen urban issue report.
* auth: bearer
* body: json
* response: Created report id and inferred area.
*/
ADD_METHOD_TO(ReportController::create, "/api/v1/reports", drogon::Post); ADD_METHOD_TO(ReportController::create, "/api/v1/reports", drogon::Post);
/*
* openapi:
* tags: Reports
* summary: List reports
* description: Lists reports visible to the authenticated user.
* auth: bearer
* body: none
* response: Report list.
*/
ADD_METHOD_TO(ReportController::list, "/api/v1/reports", drogon::Get); ADD_METHOD_TO(ReportController::list, "/api/v1/reports", drogon::Get);
/*
* openapi:
* tags: Reports
* summary: Report detail
* description: Returns report detail, status history and media.
* auth: bearer
* body: none
* response: Report detail.
*/
ADD_METHOD_TO(ReportController::detail, "/api/v1/reports/{1}", drogon::Get); ADD_METHOD_TO(ReportController::detail, "/api/v1/reports/{1}", drogon::Get);
/*
* openapi:
* tags: Reports
* summary: Update report status
* description: Updates a report status following allowed transitions.
* auth: bearer
* body: json
* response: Status update result.
*/
ADD_METHOD_TO(ReportController::updateStatus, ADD_METHOD_TO(ReportController::updateStatus,
"/api/v1/reports/{1}/status", "/api/v1/reports/{1}/status",
drogon::Patch); drogon::Patch);
/*
* openapi:
* tags: Reports
* summary: Take report
* description: Assigns an open report to the current operator.
* auth: bearer
* body: none
* response: Take report result.
*/
ADD_METHOD_TO(ReportController::takeReport, ADD_METHOD_TO(ReportController::takeReport,
"/api/v1/reports/{1}/take", "/api/v1/reports/{1}/take",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Reports
* summary: Add message
* description: Adds a citizen/operator message to a report.
* auth: bearer
* body: json
* response: Message creation result.
*/
ADD_METHOD_TO(ReportController::addMessage, ADD_METHOD_TO(ReportController::addMessage,
"/api/v1/reports/{1}/messages", "/api/v1/reports/{1}/messages",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Reports
* summary: List messages
* description: Lists messages for a report conversation.
* auth: bearer
* body: none
* response: Message list.
*/
ADD_METHOD_TO(ReportController::listMessages, ADD_METHOD_TO(ReportController::listMessages,
"/api/v1/reports/{1}/messages", "/api/v1/reports/{1}/messages",
drogon::Get); drogon::Get);
/*
* openapi:
* tags: Reports
* summary: Assign team
* description: Assigns an intervention team to a report.
* auth: bearer
* body: json
* response: Assignment result.
*/
ADD_METHOD_TO(ReportController::assignTeam, ADD_METHOD_TO(ReportController::assignTeam,
"/api/v1/reports/{1}/assignments", "/api/v1/reports/{1}/assignments",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Reports
* summary: Add feedback
* description: Adds citizen feedback after report resolution.
* auth: bearer
* body: json
* response: Feedback result.
*/
ADD_METHOD_TO(ReportController::addFeedback, ADD_METHOD_TO(ReportController::addFeedback,
"/api/v1/reports/{1}/feedback", "/api/v1/reports/{1}/feedback",
drogon::Post); drogon::Post);
/*
* openapi:
* tags: Reports
* summary: Upload media
* description: Uploads report images or videos as multipart files.
* auth: bearer
* body: multipart
* response: Uploaded media list.
*/
ADD_METHOD_TO(ReportController::uploadMedia, ADD_METHOD_TO(ReportController::uploadMedia,
"/api/v1/reports/{1}/media", "/api/v1/reports/{1}/media",
drogon::Post); drogon::Post);
+14 -13
View File
@@ -6,17 +6,17 @@
- `repositories/`: database helpers and migrations. - `repositories/`: database helpers and migrations.
- `common/`: reusable HTTP, auth, crypto, email and validation utilities. - `common/`: reusable HTTP, auth, crypto, email and validation utilities.
- `migrations/`: versioned SQL files. - `migrations/`: versioned SQL files.
- `lab/`: local Streamlit frontend for visual endpoint testing. - `scripts/generate_openapi.py`: OpenAPI generator.
- `scripts/lab/`: local Streamlit frontend for visual endpoint testing.
- `docs/`: product, API, schema and milestone documentation. - `docs/`: product, API, schema and milestone documentation.
## Drogon Usage ## Drogon Usage
Controllers use Drogon route macros and return JSON with Controllers use Drogon route macros and return JSON with
`HttpResponse::newHttpJsonResponse`. Database access uses Drogon's MySQL client `HttpResponse::newHttpJsonResponse`. Database access uses
with parameterized SQL placeholders. `drogon::orm::DbClient` with raw parameterized SQL placeholders.
The codebase starts with raw SQL repositories because the schema is still The codebase does not use Drogon generated models or `Mapper<T>`. Raw SQL stays
evolving. Drogon ORM/codegen can be introduced later after the database model in repository/database helpers because the schema is still evolving.
stabilizes.
## Local Email ## Local Email
OTP emails are sent to MailHog over SMTP. MailHog keeps messages local and OTP emails are sent to MailHog over SMTP. MailHog keeps messages local and
@@ -29,16 +29,17 @@ available through OpenSSL, which is already a Drogon dependency in this project.
Legacy `sha256$` password hashes are rejected instead of silently accepted. Legacy `sha256$` password hashes are rejected instead of silently accepted.
## OpenAPI Generation ## OpenAPI Generation
`generate_openapi.py` scans Drogon `ADD_METHOD_TO` route macros in `scripts/generate_openapi.py` scans Drogon `ADD_METHOD_TO` route macros and the
`controllers/*.h` and generates `build/openapi/visione.openapi.json`. adjacent `openapi:` metadata blocks in `controllers/*.h`, then generates
`build/openapi/visione.openapi.json`.
This is a contract-last generator: the source of truth is the C++ controller This is a contract-last generator: the source of truth is the C++ controller
route list, not a manually maintained static Swagger document. The current tool route list, not a manually maintained static Swagger document. The current tool
generates paths, methods, path parameters, bearer auth hints and common JSON or generates paths, methods, path parameters, summaries, descriptions, auth hints
multipart request bodies. Deeper request/response schemas can be added later by and common JSON or multipart request bodies. Generation fails if a route lacks
teaching the generator project-specific conventions or comments. metadata.
## Visione Lab ## Visione Lab
`lab/` is a Streamlit dev frontend for visual endpoint testing. It is `scripts/lab/` is a Streamlit dev frontend for visual endpoint testing. It is
intentionally separate from the real product frontend and should stay small: intentionally separate from the real product frontend and should stay small:
forms, uploads and response rendering only. Run it with uv from `lab/`. forms, uploads and response rendering only. Run it with uv from `scripts/lab/`.
+4
View File
@@ -2,6 +2,10 @@
The schema is stored in `migrations/001_initial_schema.sql`. The schema is stored in `migrations/001_initial_schema.sql`.
Persistence currently uses `drogon::orm::DbClient` with raw parameterized SQL.
The project does not use Drogon generated models or `Mapper<T>` while the schema
is still evolving.
Important tables: Important tables:
- `users`: identity, personal data, password hash, points and moderation flags. - `users`: identity, personal data, password hash, points and moderation flags.
+4 -3
View File
@@ -7,14 +7,15 @@
than plain SHA-256 and available through the existing OpenSSL dependency. than plain SHA-256 and available through the existing OpenSSL dependency.
- Added report media uploads at `POST /api/v1/reports/{id}/media`. - Added report media uploads at `POST /api/v1/reports/{id}/media`.
- Supported JPEG, PNG, WebP, MP4, WebM and QuickTime uploads. - Supported JPEG, PNG, WebP, MP4, WebM and QuickTime uploads.
- Removed the old `/photos` upload path; report attachments now use `/media`. - Report attachments now use the `/media` upload path.
- Added generated OpenAPI JSON from Drogon controller route macros. - Added generated OpenAPI JSON from Drogon controller route macros and adjacent
metadata blocks.
- Exposed `/api/v1/openapi.json` and `/api/v1/docs`. - Exposed `/api/v1/openapi.json` and `/api/v1/docs`.
- Added a small Streamlit API lab for visual endpoint testing. - Added a small Streamlit API lab for visual endpoint testing.
## To do next ## To do next
- Add richer OpenAPI schema metadata for request and response bodies. - Add richer OpenAPI schema metadata for request and response bodies.
- Consider extracting `generate_openapi.py` into an open-source standalone - Consider extracting `scripts/generate_openapi.py` into an open-source standalone
project once the conventions stabilize. project once the conventions stabilize.
- Add minimal media upload integration tests after endpoint contracts settle. - Add minimal media upload integration tests after endpoint contracts settle.
- Keep API and docs on `media` terminology only. - Keep API and docs on `media` terminology only.
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generate a compact OpenAPI spec from Drogon HttpController route macros.""" """Generate a compact OpenAPI spec from Drogon route macros and metadata blocks."""
from __future__ import annotations from __future__ import annotations
@@ -12,21 +12,12 @@ from pathlib import Path
ROUTE_RE = re.compile( ROUTE_RE = re.compile(
r"ADD_METHOD_TO\(\s*([A-Za-z_]\w*)::([A-Za-z_]\w*)\s*,\s*" r"ADD_METHOD_TO\(\s*([A-Za-z_]\w*)::([A-Za-z_]\w*)\s*,\s*"
r'"([^"]+)"\s*,\s*drogon::([A-Za-z]+)\s*\)', r'"([^"]+)"\s*,\s*drogon::([A-Za-z]+)\s*\)',
re.MULTILINE, re.MULTILINE | re.DOTALL,
) )
METHODS_WITH_JSON_BODY = {"post", "put", "patch"} METHODS_WITH_JSON_BODY = {"post", "put", "patch"}
PUBLIC_OPERATIONS = { REQUIRED_METADATA = {"tags", "summary", "description", "auth", "body", "response"}
"health",
"ready",
"openApi",
"docs",
"registerCitizen",
"requestOtp",
"verifyOtp",
"loginWithPassword",
}
def controller_tag(controller: str) -> str: def controller_tag(controller: str) -> str:
@@ -51,10 +42,47 @@ def openapi_path(path: str) -> tuple[str, list[dict]]:
return re.sub(r"\{(\d+)\}", replace, path), parameters return re.sub(r"\{(\d+)\}", replace, path), parameters
def request_body(path: str, method: str) -> dict | None: def parse_metadata(header: Path, text: str, route_start: int) -> dict[str, str]:
if method != "post" and method != "patch": prefix = text[:route_start].rstrip()
if not prefix.endswith("*/"):
raise ValueError(f"{header}: route is missing an openapi metadata block")
comment_start = prefix.rfind("/*")
if comment_start == -1:
raise ValueError(f"{header}: route is missing an openapi metadata block")
block = prefix[comment_start + 2:-2]
metadata: dict[str, str] = {}
in_openapi_block = False
for raw_line in block.splitlines():
line = raw_line.strip()
if line.startswith("*"):
line = line[1:].strip()
if not line:
continue
if line == "openapi:":
in_openapi_block = True
continue
if not in_openapi_block:
continue
match = re.match(r"([A-Za-z_]+):\s*(.+)", line)
if match:
metadata[match.group(1)] = match.group(2).strip()
missing = sorted(REQUIRED_METADATA - metadata.keys())
if missing:
names = ", ".join(missing)
raise ValueError(f"{header}: openapi metadata block is missing: {names}")
return metadata
def request_body(body_kind: str, method: str) -> dict | None:
if body_kind == "none":
return None return None
if path.endswith("/media"): if body_kind == "multipart":
return { return {
"required": True, "required": True,
"content": { "content": {
@@ -71,33 +99,56 @@ def request_body(path: str, method: str) -> dict | None:
} }
}, },
} }
if method in METHODS_WITH_JSON_BODY: if body_kind == "json" and method in METHODS_WITH_JSON_BODY:
return { return {
"required": True, "required": True,
"content": { "content": {
"application/json": {"schema": {"type": "object"}} "application/json": {"schema": {"type": "object"}}
}, },
} }
return None if body_kind == "json":
raise ValueError(f"JSON body is not supported for {method.upper()} routes")
raise ValueError(f"Unsupported request body kind: {body_kind}")
def operation(controller: str, handler: str, raw_path: str, method: str) -> dict: def parse_tags(value: str) -> list[str]:
return [tag.strip() for tag in value.split(",") if tag.strip()]
def is_public(metadata: dict[str, str]) -> bool:
auth = metadata["auth"]
if auth == "public":
return True
if auth == "bearer":
return False
raise ValueError(f"Unsupported auth kind: {auth}")
def operation(
controller: str,
handler: str,
raw_path: str,
method: str,
metadata: dict[str, str],
) -> dict:
path, params = openapi_path(raw_path) path, params = openapi_path(raw_path)
op = { op = {
"tags": [controller_tag(controller)], "tags": parse_tags(metadata["tags"]) or [controller_tag(controller)],
"operationId": f"{controller}.{handler}", "operationId": f"{controller}.{handler}",
"summary": metadata["summary"],
"description": metadata["description"],
"parameters": params, "parameters": params,
"responses": { "responses": {
"200": { "200": {
"description": "JSON response", "description": metadata["response"],
"content": {"application/json": {"schema": {"type": "object"}}}, "content": {"application/json": {"schema": {"type": "object"}}},
} }
}, },
} }
body = request_body(path, method) body = request_body(metadata["body"], method)
if body: if body:
op["requestBody"] = body op["requestBody"] = body
if handler not in PUBLIC_OPERATIONS: if not is_public(metadata):
op["security"] = [{"bearerAuth": []}] op["security"] = [{"bearerAuth": []}]
if method == "post": if method == "post":
op["responses"]["201"] = op["responses"]["200"] op["responses"]["201"] = op["responses"]["200"]
@@ -122,9 +173,13 @@ def generate(controllers_dir: Path, title: str, version: str) -> dict:
} }
for header in sorted(controllers_dir.glob("*.h")): for header in sorted(controllers_dir.glob("*.h")):
text = header.read_text(encoding="utf-8") text = header.read_text(encoding="utf-8")
for controller, handler, raw_path, drogon_method in ROUTE_RE.findall(text): for match in ROUTE_RE.finditer(text):
controller, handler, raw_path, drogon_method = match.groups()
metadata = parse_metadata(header, text, match.start())
method = drogon_method.lower() method = drogon_method.lower()
path, op = operation(controller, handler, raw_path, method) path, op = operation(controller, handler, raw_path, method, metadata)
if "/photos" in path:
raise ValueError(f"{header}: generated OpenAPI path still uses /photos")
spec["paths"].setdefault(path, {})[method] = op spec["paths"].setdefault(path, {})[method] = op
return spec return spec
View File
View File