diff --git a/src/docgen/generators/openapi.py b/src/docgen/generators/openapi.py new file mode 100644 index 0000000..7182e11 --- /dev/null +++ b/src/docgen/generators/openapi.py @@ -0,0 +1,95 @@ +"""OpenAPI documentation generator.""" + +import json +from pathlib import Path +from typing import Any +from docgen.models import DocConfig, Endpoint, HTTPMethod +from docgen.generators import BaseGenerator + + +class OpenAPIGenerator(BaseGenerator): + """Generator for OpenAPI 3.0 specification.""" + + def generate(self, endpoints: list[Endpoint], output_dir: Path) -> Path: + """Generate OpenAPI specification.""" + output_dir = self._ensure_output_dir(output_dir) + + spec = self._build_openapi_spec(endpoints) + spec_path = output_dir / "openapi.json" + spec_path.write_text(json.dumps(spec, indent=2)) + + return spec_path + + def _build_openapi_spec(self, endpoints: list[Endpoint]) -> dict[str, Any]: + """Build the OpenAPI specification dictionary.""" + spec = { + "openapi": "3.0.3", + "info": { + "title": self.config.title, + "description": self.config.description, + "version": self.config.version, + }, + "paths": {}, + "servers": [{"url": "/"}], + } + + for endpoint in endpoints: + path_item = self._endpoint_to_path_item(endpoint) + + if endpoint.path not in spec["paths"]: + spec["paths"][endpoint.path] = path_item + else: + existing = spec["paths"][endpoint.path] + for method in ["get", "post", "put", "patch", "delete", "options", "head"]: + if method in path_item: + existing[method] = path_item[method] + + return spec + + def _endpoint_to_path_item(self, endpoint: Endpoint) -> dict[str, Any]: + """Convert an Endpoint to OpenAPI path item.""" + method = endpoint.method.value.lower() + + operation = { + "summary": endpoint.summary or f"{endpoint.method.value} {endpoint.path}", + "description": endpoint.description, + "operationId": endpoint.operation_id or f"{method}_{endpoint.path.replace('/', '_').strip('_')}", + "deprecated": endpoint.deprecated, + "parameters": [self._param_to_openapi(p) for p in endpoint.parameters], + "responses": self._build_responses(endpoint.responses), + } + + if endpoint.security: + operation["security"] = [{"bearerAuth": []}] + + return {method: operation} + + def _param_to_openapi(self, param) -> dict[str, Any]: + """Convert Parameter to OpenAPI parameter.""" + return { + "name": param.name, + "in": param.location.value, + "description": param.description, + "required": param.required, + "schema": {"type": param.type, "default": param.default}, + "example": param.example, + } + + def _build_responses(self, responses) -> dict[str, Any]: + """Build OpenAPI responses object.""" + if not responses: + return { + "200": {"description": "Successful response"}, + "default": {"description": "Error response"}, + } + + result = {} + for resp in responses: + resp_obj = {"description": resp.description} + if resp.example: + resp_obj["content"] = { + resp.content_type: {"schema": resp.example} + } + result[str(resp.status_code)] = resp_obj + + return result