Clean up OpenAPI scripts
This commit is contained in:
+2
-1
@@ -95,7 +95,8 @@ build/
|
||||
.cache/
|
||||
.env
|
||||
uploads/
|
||||
lab/.venv/
|
||||
scripts/lab/.venv/
|
||||
scripts/lab/__pycache__/
|
||||
|
||||
### CMake Patch ###
|
||||
# External projects
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
- Build: `cmake --build build`
|
||||
- Test: `ctest --test-dir build --output-on-failure`
|
||||
- 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`
|
||||
- App: `./build/visione-backend`
|
||||
|
||||
|
||||
+2
-2
@@ -77,13 +77,13 @@ set(OPENAPI_OUTPUT ${CMAKE_BINARY_DIR}/openapi/visione.openapi.json)
|
||||
add_custom_command(
|
||||
OUTPUT ${OPENAPI_OUTPUT}
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generate_openapi.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_openapi.py
|
||||
--controllers ${CMAKE_CURRENT_SOURCE_DIR}/controllers
|
||||
--out ${OPENAPI_OUTPUT}
|
||||
--title "Visione API"
|
||||
--version ${PROJECT_VERSION}
|
||||
DEPENDS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generate_openapi.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_openapi.py
|
||||
${CONTROLLER_HEADERS}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
VERBATIM
|
||||
|
||||
@@ -90,11 +90,11 @@ The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at
|
||||
|
||||
## Visione Lab
|
||||
|
||||
`lab/` is a small Streamlit frontend used only to test the backend visually
|
||||
while the real frontend is not ready.
|
||||
`scripts/lab/` is a small Streamlit frontend used only to test the backend
|
||||
visually while the real frontend is not ready.
|
||||
|
||||
```bash
|
||||
cd lab
|
||||
cd scripts/lab
|
||||
uv sync
|
||||
uv run streamlit run app.py
|
||||
```
|
||||
|
||||
@@ -6,23 +6,113 @@ class AdminController : public drogon::HttpController<AdminController>
|
||||
{
|
||||
public:
|
||||
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,
|
||||
"/api/v1/admin/categories",
|
||||
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,
|
||||
"/api/v1/admin/categories",
|
||||
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);
|
||||
/*
|
||||
* 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);
|
||||
/*
|
||||
* 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);
|
||||
/*
|
||||
* 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);
|
||||
/*
|
||||
* 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,
|
||||
"/api/v1/admin/stats/overview",
|
||||
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);
|
||||
/*
|
||||
* 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,
|
||||
"/api/v1/admin/users/{1}/roles",
|
||||
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,
|
||||
"/api/v1/admin/users/{1}/ban",
|
||||
drogon::Patch);
|
||||
@@ -61,4 +151,3 @@ class AdminController : public drogon::HttpController<AdminController>
|
||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||
long long userId);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,9 +6,45 @@ class ApiController : public drogon::HttpController<ApiController>
|
||||
{
|
||||
public:
|
||||
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);
|
||||
/*
|
||||
* 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);
|
||||
/*
|
||||
* 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);
|
||||
/*
|
||||
* 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);
|
||||
METHOD_LIST_END
|
||||
|
||||
|
||||
@@ -6,21 +6,75 @@ class AuthController : public drogon::HttpController<AuthController>
|
||||
{
|
||||
public:
|
||||
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,
|
||||
"/api/v1/auth/register",
|
||||
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,
|
||||
"/api/v1/auth/otp/request",
|
||||
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,
|
||||
"/api/v1/auth/otp/verify",
|
||||
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,
|
||||
"/api/v1/auth/login/password",
|
||||
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,
|
||||
"/api/v1/auth/change-password",
|
||||
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);
|
||||
METHOD_LIST_END
|
||||
|
||||
|
||||
@@ -6,27 +6,117 @@ class ReportController : public drogon::HttpController<ReportController>
|
||||
{
|
||||
public:
|
||||
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);
|
||||
/*
|
||||
* 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);
|
||||
/*
|
||||
* 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);
|
||||
/*
|
||||
* 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,
|
||||
"/api/v1/reports/{1}/status",
|
||||
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,
|
||||
"/api/v1/reports/{1}/take",
|
||||
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,
|
||||
"/api/v1/reports/{1}/messages",
|
||||
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,
|
||||
"/api/v1/reports/{1}/messages",
|
||||
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,
|
||||
"/api/v1/reports/{1}/assignments",
|
||||
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,
|
||||
"/api/v1/reports/{1}/feedback",
|
||||
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,
|
||||
"/api/v1/reports/{1}/media",
|
||||
drogon::Post);
|
||||
|
||||
+14
-13
@@ -6,17 +6,17 @@
|
||||
- `repositories/`: database helpers and migrations.
|
||||
- `common/`: reusable HTTP, auth, crypto, email and validation utilities.
|
||||
- `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.
|
||||
|
||||
## Drogon Usage
|
||||
Controllers use Drogon route macros and return JSON with
|
||||
`HttpResponse::newHttpJsonResponse`. Database access uses Drogon's MySQL client
|
||||
with parameterized SQL placeholders.
|
||||
`HttpResponse::newHttpJsonResponse`. Database access uses
|
||||
`drogon::orm::DbClient` with raw parameterized SQL placeholders.
|
||||
|
||||
The codebase starts with raw SQL repositories because the schema is still
|
||||
evolving. Drogon ORM/codegen can be introduced later after the database model
|
||||
stabilizes.
|
||||
The codebase does not use Drogon generated models or `Mapper<T>`. Raw SQL stays
|
||||
in repository/database helpers because the schema is still evolving.
|
||||
|
||||
## Local Email
|
||||
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.
|
||||
|
||||
## OpenAPI Generation
|
||||
`generate_openapi.py` scans Drogon `ADD_METHOD_TO` route macros in
|
||||
`controllers/*.h` and generates `build/openapi/visione.openapi.json`.
|
||||
`scripts/generate_openapi.py` scans Drogon `ADD_METHOD_TO` route macros and the
|
||||
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
|
||||
route list, not a manually maintained static Swagger document. The current tool
|
||||
generates paths, methods, path parameters, bearer auth hints and common JSON or
|
||||
multipart request bodies. Deeper request/response schemas can be added later by
|
||||
teaching the generator project-specific conventions or comments.
|
||||
generates paths, methods, path parameters, summaries, descriptions, auth hints
|
||||
and common JSON or multipart request bodies. Generation fails if a route lacks
|
||||
metadata.
|
||||
|
||||
## 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:
|
||||
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/`.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
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:
|
||||
|
||||
- `users`: identity, personal data, password hash, points and moderation flags.
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
than plain SHA-256 and available through the existing OpenSSL dependency.
|
||||
- Added report media uploads at `POST /api/v1/reports/{id}/media`.
|
||||
- Supported JPEG, PNG, WebP, MP4, WebM and QuickTime uploads.
|
||||
- Removed the old `/photos` upload path; report attachments now use `/media`.
|
||||
- Added generated OpenAPI JSON from Drogon controller route macros.
|
||||
- Report attachments now use the `/media` upload path.
|
||||
- Added generated OpenAPI JSON from Drogon controller route macros and adjacent
|
||||
metadata blocks.
|
||||
- Exposed `/api/v1/openapi.json` and `/api/v1/docs`.
|
||||
- Added a small Streamlit API lab for visual endpoint testing.
|
||||
|
||||
## To do next
|
||||
- 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.
|
||||
- Add minimal media upload integration tests after endpoint contracts settle.
|
||||
- Keep API and docs on `media` terminology only.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
@@ -12,21 +12,12 @@ from pathlib import Path
|
||||
ROUTE_RE = re.compile(
|
||||
r"ADD_METHOD_TO\(\s*([A-Za-z_]\w*)::([A-Za-z_]\w*)\s*,\s*"
|
||||
r'"([^"]+)"\s*,\s*drogon::([A-Za-z]+)\s*\)',
|
||||
re.MULTILINE,
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
METHODS_WITH_JSON_BODY = {"post", "put", "patch"}
|
||||
PUBLIC_OPERATIONS = {
|
||||
"health",
|
||||
"ready",
|
||||
"openApi",
|
||||
"docs",
|
||||
"registerCitizen",
|
||||
"requestOtp",
|
||||
"verifyOtp",
|
||||
"loginWithPassword",
|
||||
}
|
||||
REQUIRED_METADATA = {"tags", "summary", "description", "auth", "body", "response"}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def request_body(path: str, method: str) -> dict | None:
|
||||
if method != "post" and method != "patch":
|
||||
def parse_metadata(header: Path, text: str, route_start: int) -> dict[str, str]:
|
||||
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
|
||||
if path.endswith("/media"):
|
||||
if body_kind == "multipart":
|
||||
return {
|
||||
"required": True,
|
||||
"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 {
|
||||
"required": True,
|
||||
"content": {
|
||||
"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)
|
||||
op = {
|
||||
"tags": [controller_tag(controller)],
|
||||
"tags": parse_tags(metadata["tags"]) or [controller_tag(controller)],
|
||||
"operationId": f"{controller}.{handler}",
|
||||
"summary": metadata["summary"],
|
||||
"description": metadata["description"],
|
||||
"parameters": params,
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "JSON response",
|
||||
"description": metadata["response"],
|
||||
"content": {"application/json": {"schema": {"type": "object"}}},
|
||||
}
|
||||
},
|
||||
}
|
||||
body = request_body(path, method)
|
||||
body = request_body(metadata["body"], method)
|
||||
if body:
|
||||
op["requestBody"] = body
|
||||
if handler not in PUBLIC_OPERATIONS:
|
||||
if not is_public(metadata):
|
||||
op["security"] = [{"bearerAuth": []}]
|
||||
if method == "post":
|
||||
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")):
|
||||
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()
|
||||
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
|
||||
return spec
|
||||
|
||||
Generated
Reference in New Issue
Block a user