"""把 Swagger 2 / OpenAPI 3 编译为连接器使用的统一 operation catalog。""" from __future__ import annotations import re from dataclasses import dataclass from typing import Any HTTP_METHODS = ( "get", "head", "post", "put", "patch", "delete", "options", "trace", ) def resolve_local_object(spec: dict[str, Any], value: Any) -> Any: """解析单个本地 JSON Pointer 引用;远端引用保留原值并由上层拒绝。""" if not isinstance(value, dict): return value ref = value.get("$ref") if not isinstance(ref, str) or not ref.startswith("#/"): return value current: Any = spec for raw in ref[2:].split("/"): part = raw.replace("~1", "/").replace("~0", "~") if not isinstance(current, dict) or part not in current: return value current = current[part] return current if isinstance(current, dict) else value def operation_id(method: str, path: str, operation: dict[str, Any]) -> str: explicit = operation.get("operationId") if isinstance(explicit, str) and explicit.strip(): return explicit.strip() safe_path = re.sub(r"[^a-zA-Z0-9]+", "_", path).strip("_") return f"{method}_{safe_path}" @dataclass(frozen=True) class OperationCatalog: operations: tuple[dict[str, Any], ...] def find(self, operation_id_value: str) -> tuple[dict[str, Any], ...]: return tuple( operation for operation in self.operations if operation["operation_id"] == operation_id_value ) def compile_operation_catalog(spec: dict[str, Any]) -> OperationCatalog: results: list[dict[str, Any]] = [] for path, path_item in (spec.get("paths") or {}).items(): if not isinstance(path, str) or not isinstance(path_item, dict): continue common = path_item.get("parameters") or [] for method in HTTP_METHODS: operation = path_item.get(method) if not isinstance(operation, dict): continue params = [ resolve_local_object(spec, param) for param in list(common) + list(operation.get("parameters") or []) ] results.append( { "operation_id": operation_id(method, path, operation), "method": method.upper(), "path": path, "summary": operation.get("summary") or "", "description": operation.get("description") or "", "tags": operation.get("tags") or [], "parameters": params, "request_body": resolve_local_object( spec, operation.get("requestBody") ), } ) return OperationCatalog(tuple(results)) def validate_json_value( spec: dict[str, Any], value: Any, schema: Any, *, path: str = "body", depth: int = 0, ) -> None: """验证调用前最关键的 JSON Schema 子集,复杂语义仍由上游最终判定。""" if depth > 20 or not isinstance(schema, dict): return schema = resolve_local_object(spec, schema) expected = schema.get("type") type_ok = { "object": isinstance(value, dict), "array": isinstance(value, list), "string": isinstance(value, str), "integer": isinstance(value, int) and not isinstance(value, bool), "number": isinstance(value, (int, float)) and not isinstance(value, bool), "boolean": isinstance(value, bool), "null": value is None, } if expected in type_ok and not type_ok[expected]: raise ValueError(f"{path} 应为 {expected}") if "enum" in schema and value not in schema.get("enum", []): raise ValueError(f"{path} 不在允许值范围内") if isinstance(value, (int, float)) and not isinstance(value, bool): minimum = schema.get("minimum") maximum = schema.get("maximum") exclusive_minimum = schema.get("exclusiveMinimum") exclusive_maximum = schema.get("exclusiveMaximum") if isinstance(minimum, (int, float)): if value < minimum or (exclusive_minimum is True and value == minimum): operator = ">" if exclusive_minimum is True else ">=" raise ValueError(f"{path} 必须 {operator} {minimum}") if isinstance(maximum, (int, float)): if value > maximum or (exclusive_maximum is True and value == maximum): operator = "<" if exclusive_maximum is True else "<=" raise ValueError(f"{path} 必须 {operator} {maximum}") if isinstance(exclusive_minimum, (int, float)) and not isinstance( exclusive_minimum, bool ): if value <= exclusive_minimum: raise ValueError(f"{path} 必须 > {exclusive_minimum}") if isinstance(exclusive_maximum, (int, float)) and not isinstance( exclusive_maximum, bool ): if value >= exclusive_maximum: raise ValueError(f"{path} 必须 < {exclusive_maximum}") if isinstance(value, str): minimum_length = schema.get("minLength") maximum_length = schema.get("maxLength") if isinstance(minimum_length, int) and len(value) < minimum_length: raise ValueError(f"{path} 长度不能小于 {minimum_length}") if isinstance(maximum_length, int) and len(value) > maximum_length: raise ValueError(f"{path} 长度不能超过 {maximum_length}") if isinstance(value, list): minimum_items = schema.get("minItems") maximum_items = schema.get("maxItems") if isinstance(minimum_items, int) and len(value) < minimum_items: raise ValueError(f"{path} 项数不能少于 {minimum_items}") if isinstance(maximum_items, int) and len(value) > maximum_items: raise ValueError(f"{path} 项数不能超过 {maximum_items}") if isinstance(value, dict): required = schema.get("required") or [] missing = [str(name) for name in required if name not in value] if missing: raise ValueError(f"{path} 缺少必填字段: " + ", ".join(missing)) properties = schema.get("properties") or {} if isinstance(properties, dict): for name, child in value.items(): if name in properties: validate_json_value( spec, child, properties[name], path=f"{path}.{name}", depth=depth + 1, ) elif schema.get("additionalProperties") is False: raise ValueError(f"{path} 包含未定义字段: {name}") if isinstance(value, list) and isinstance(schema.get("items"), dict): for index, item in enumerate(value): validate_json_value( spec, item, schema["items"], path=f"{path}[{index}]", depth=depth + 1, )