|
4 | 4 | This is the most accurate scanner since FastAPI's OpenAPI generation |
5 | 5 | handles all edge cases (Depends, File, Form, etc.). |
6 | 6 |
|
7 | | -Module ID format: {tag}.{operation_id}.{method} |
| 7 | +Module ID format: |
| 8 | + Default: {tag}.{operationId_without_method}.{method} |
| 9 | + simplify_ids: {tag}.{func_name}.{method} |
8 | 10 | """ |
9 | 11 |
|
10 | 12 | from __future__ import annotations |
@@ -34,6 +36,17 @@ class OpenAPIScanner(BaseScanner): |
34 | 36 | - Response models with $ref |
35 | 37 | """ |
36 | 38 |
|
| 39 | + def __init__(self, *, simplify_ids: bool = False) -> None: |
| 40 | + """Initialize the OpenAPI scanner. |
| 41 | +
|
| 42 | + Args: |
| 43 | + simplify_ids: When True, generate simplified module IDs using only |
| 44 | + the function name (e.g. ``product.get_product.get``). |
| 45 | + When False (default), use the full FastAPI operationId |
| 46 | + (e.g. ``product.get_product_product__product_id_.get``). |
| 47 | + """ |
| 48 | + self._simplify_ids = simplify_ids |
| 49 | + |
37 | 50 | def scan( |
38 | 51 | self, |
39 | 52 | app: FastAPI, |
@@ -108,39 +121,72 @@ def get_source_name(self) -> str: |
108 | 121 | return "openapi-fastapi" |
109 | 122 |
|
110 | 123 | def _generate_module_id(self, operation: dict[str, Any], path: str, method: str) -> str: |
111 | | - """Generate module_id from tags + operation_id + method. |
| 124 | + """Generate module_id from tags + function_name + method. |
| 125 | +
|
| 126 | + When ``simplify_ids=True`` (set in constructor), extracts the clean |
| 127 | + function name from FastAPI's operationId:: |
112 | 128 |
|
113 | | - Format: {tag}.{operation_id}.{method} |
114 | | - Falls back to path-based ID if no tags. |
| 129 | + GET /product/{product_id} → product.get_product.get |
| 130 | + POST /task/create → task.create_task.post |
| 131 | +
|
| 132 | + When ``simplify_ids=False`` (default), uses the raw operationId |
| 133 | + with only the trailing method stripped:: |
| 134 | +
|
| 135 | + GET /product/{product_id} → product.get_product_product__product_id_.get |
115 | 136 | """ |
116 | 137 | operation_id: str = operation.get("operationId", "unknown") |
117 | 138 | tags = operation.get("tags", []) |
118 | 139 |
|
119 | | - # Clean operation_id: FastAPI generates e.g. "create_task_task_create_post" |
120 | | - # We use the simpler function name portion |
121 | | - func_name = self._simplify_operation_id(operation_id) |
| 140 | + if self._simplify_ids: |
| 141 | + func_name = self._extract_func_name(operation_id, path, method) |
| 142 | + else: |
| 143 | + func_name = self._strip_method_suffix(operation_id, method) |
122 | 144 |
|
123 | 145 | if tags: |
124 | 146 | prefix = str(tags[0]).lower().replace(" ", "_") |
125 | | - module_id = f"{prefix}.{func_name}.{method.lower()}" |
126 | 147 | else: |
127 | 148 | path_parts = [p for p in path.strip("/").split("/") if not p.startswith("{")] |
128 | 149 | prefix = ".".join(path_parts) if path_parts else "root" |
129 | | - module_id = f"{prefix}.{func_name}.{method.lower()}" |
130 | 150 |
|
| 151 | + module_id = f"{prefix}.{func_name}.{method.lower()}" |
131 | 152 | return re.sub(r"[^a-zA-Z0-9._]", "_", module_id) |
132 | 153 |
|
133 | | - def _simplify_operation_id(self, operation_id: str) -> str: |
134 | | - """Simplify FastAPI's auto-generated operation IDs. |
| 154 | + @staticmethod |
| 155 | + def _strip_method_suffix(operation_id: str, method: str) -> str: |
| 156 | + """Strip the trailing ``_{method}`` from an operationId. |
| 157 | +
|
| 158 | + This is the default (non-short) simplification — removes only the |
| 159 | + HTTP method suffix while preserving the full path information. |
| 160 | + """ |
| 161 | + suffix = f"_{method.lower()}" |
| 162 | + if operation_id.endswith(suffix): |
| 163 | + return operation_id[: -len(suffix)] |
| 164 | + return operation_id |
| 165 | + |
| 166 | + @staticmethod |
| 167 | + def _extract_func_name(operation_id: str, path: str, method: str) -> str: |
| 168 | + """Extract the original function name from a FastAPI operationId. |
| 169 | +
|
| 170 | + FastAPI generates operationId as:: |
| 171 | +
|
| 172 | + re.sub(r"\\W", "_", f"{func_name}{path}") + "_{method}" |
135 | 173 |
|
136 | | - FastAPI generates IDs like 'create_task_task_create_post'. |
137 | | - We strip the trailing path+method suffix to get the function name. |
| 174 | + This method reverses that transformation to recover ``func_name``. |
138 | 175 | """ |
139 | | - # FastAPI pattern: {func_name}_{path_part}_{path_part}_{method} |
140 | | - # Try to extract just the function name by removing the suffix |
141 | | - parts = operation_id.rsplit("_", 1) |
142 | | - if len(parts) == 2 and parts[1] in ("get", "post", "put", "delete", "patch"): |
143 | | - return parts[0] |
| 176 | + method_lower = method.lower() |
| 177 | + |
| 178 | + # Reconstruct the path suffix exactly as FastAPI generates it: |
| 179 | + # every non-word character (\W) in the path becomes "_" |
| 180 | + path_suffix = re.sub(r"\W", "_", path) |
| 181 | + expected_suffix = f"{path_suffix}_{method_lower}" |
| 182 | + |
| 183 | + if operation_id.endswith(expected_suffix): |
| 184 | + return operation_id[: -len(expected_suffix)].rstrip("_") |
| 185 | + |
| 186 | + # Fallback: strip trailing _{method} |
| 187 | + if operation_id.endswith(f"_{method_lower}"): |
| 188 | + return operation_id[: -(len(method_lower) + 1)] |
| 189 | + |
144 | 190 | return operation_id |
145 | 191 |
|
146 | 192 | def _build_view_map(self, app: FastAPI) -> dict[str, str]: |
|
0 commit comments