File size: 7,737 Bytes
a55ea57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
"""OpenAPI guarantees for discovery and LLM function-calling clients."""

from __future__ import annotations

from typing import Any

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi


_HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head"}
_CORE_TAGS = {"public", "ingest", "retrieve", "model", "meta", "tasks", "system"}
_LIST_PATHS = {
    "/meta/entity-labels",
    "/meta/entity-properties",
    "/meta/relationships-properties",
    "/retrieve/hops",
    "/system/brains-list",
}


def _error_response(description: str) -> dict[str, Any]:
    return {
        "description": description,
        "content": {
            "application/json": {
                "schema": {"$ref": "#/components/schemas/ErrorResponse"}
            }
        },
    }


def install_openapi_contract(app: FastAPI) -> None:
    """Install a deterministic, fully-described OpenAPI schema builder."""

    def custom_openapi() -> dict[str, Any]:
        if app.openapi_schema is not None:
            return app.openapi_schema

        schema = get_openapi(
            title=app.title,
            version=app.version,
            openapi_version=app.openapi_version,
            description=app.description,
            routes=app.routes,
        )
        components = schema.setdefault("components", {})
        component_schemas = components.setdefault("schemas", {})
        security_schemes = components.setdefault("securitySchemes", {})
        security_schemes.update(
            {
                "BrainPAT": {
                    "type": "apiKey",
                    "in": "header",
                    "name": "BrainPAT",
                    "description": "BrainAPI personal access token.",
                },
                "BearerAuth": {
                    "type": "http",
                    "scheme": "bearer",
                    "description": "BrainAPI PAT sent as a Bearer token.",
                },
            }
        )

        protected_errors = {
            "400": _error_response("Invalid request"),
            "401": _error_response("Authentication failed"),
            "403": _error_response("Operation forbidden"),
            "404": _error_response("Resource not found"),
            "405": _error_response("Method not allowed"),
            "406": _error_response("Brain not found"),
            "422": _error_response("Validation failed"),
            "429": _error_response("Rate limit exceeded"),
            "500": _error_response("Internal service error"),
            "503": _error_response("Dependency unavailable"),
        }

        for path, path_item in schema.get("paths", {}).items():
            for method, operation in path_item.items():
                if method.lower() not in _HTTP_METHODS or not isinstance(operation, dict):
                    continue
                summary = operation.get("summary") or operation.get("operationId")
                if not operation.get("description"):
                    operation["description"] = (
                        f"{summary}. See the BrainAPI developer documentation for "
                        "request, response, authentication, and retry semantics."
                    )

                for status_code, response in operation.get("responses", {}).items():
                    if not str(status_code).startswith("2") or not isinstance(response, dict):
                        continue
                    for media in response.get("content", {}).values():
                        if isinstance(media, dict) and media.get("schema") == {}:
                            if _CORE_TAGS.intersection(operation.get("tags", [])):
                                raise ValueError(
                                    f"Core operation {method.upper()} {path} must define "
                                    "a typed success response model"
                                )
                            model = (
                                "GenericListResponse"
                                if path in _LIST_PATHS
                                else "GenericObjectResponse"
                            )
                            component_schemas.setdefault(
                                model,
                                (
                                    {
                                        "type": "array",
                                        "description": "A list of operation-specific JSON values.",
                                        "items": {
                                            "oneOf": [
                                                {"type": "string"},
                                                {
                                                    "type": "object",
                                                    "additionalProperties": True,
                                                },
                                            ]
                                        },
                                    }
                                    if model == "GenericListResponse"
                                    else {
                                        "type": "object",
                                        "description": (
                                            "An operation-specific JSON object from an "
                                            "installed extension."
                                        ),
                                        "additionalProperties": True,
                                    }
                                ),
                            )
                            media["schema"] = {
                                "$ref": f"#/components/schemas/{model}"
                            }

                is_public = path in {"/health", "/demo/search"}
                if is_public:
                    operation["security"] = []
                    public_errors = {
                        "405": _error_response("Method not allowed"),
                        "500": _error_response("Internal service error"),
                        "503": _error_response("Dependency unavailable"),
                    }
                    if path == "/demo/search":
                        public_errors.update(
                            {
                                "404": _error_response("Public demo disabled"),
                                "422": _error_response("Validation failed"),
                            }
                        )
                    responses = operation.setdefault("responses", {})
                    for code, response in public_errors.items():
                        if code == "422" or code not in responses:
                            responses[code] = response
                else:
                    operation["security"] = [{"BrainPAT": []}, {"BearerAuth": []}]
                    responses = operation.setdefault("responses", {})
                    for code, response in protected_errors.items():
                        if code == "422" or code not in responses:
                            responses[code] = response

                if path == "/demo/search":
                    operation.setdefault("responses", {})["429"] = _error_response(
                        "Public demo rate limit exceeded"
                    )
                    operation["x-rate-limit"] = {
                        "requests": 30,
                        "window": "60 seconds",
                    }

        schema["servers"] = [
            {"url": "http://localhost:8000", "description": "Local BrainAPI"}
        ]
        app.openapi_schema = schema
        return schema

    app.openapi = custom_openapi