add:stage
This commit is contained in:
Binary file not shown.
@@ -1,6 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, Union # Ensure Union is imported
|
||||
from typing import List, Dict, Any, Optional, Union, Tuple
|
||||
import requests
|
||||
from urllib.parse import urljoin
|
||||
import copy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -373,9 +376,53 @@ class SwaggerEndpoint(BaseEndpoint): # Inherit from BaseEndpoint
|
||||
def __repr__(self):
|
||||
return f"<SwaggerEndpoint Method:{self.method} Path:{self.path} Summary:'{self.summary}'>"
|
||||
|
||||
class DMSEndpoint(BaseEndpoint):
|
||||
"""Represents an API endpoint discovered dynamically from the DMS service."""
|
||||
def __init__(self, method: str, path: str, title: str,
|
||||
request_body: Optional[Dict[str, Any]],
|
||||
responses: Dict[str, Any],
|
||||
parameters: Optional[List[Dict[str, Any]]] = None,
|
||||
category_name: Optional[str] = None,
|
||||
raw_record: Optional[Dict[str, Any]] = None,
|
||||
test_mode: str = 'standalone',
|
||||
operation_id: Optional[str] = None):
|
||||
super().__init__(method=method.upper(), path=path)
|
||||
self.title = title
|
||||
self.request_body = request_body
|
||||
self.responses = responses
|
||||
self.parameters = parameters if parameters is not None else []
|
||||
self.category_name = category_name
|
||||
self._raw_record = raw_record
|
||||
self.test_mode = test_mode
|
||||
self.operation_id = operation_id or f"{self.method.lower()}_{self.category_name or 'dms'}_{title.replace(' ', '_')}"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Converts the DMS endpoint data into a standardized OpenAPI-like dictionary."""
|
||||
endpoint_dict = {
|
||||
"method": self.method,
|
||||
"path": self.path,
|
||||
"title": self.title,
|
||||
"summary": self.title,
|
||||
"description": self.title,
|
||||
"operationId": self.operation_id,
|
||||
"tags": [self.category_name] if self.category_name else [],
|
||||
"parameters": self.parameters,
|
||||
"requestBody": self.request_body,
|
||||
"responses": self.responses,
|
||||
"_source_format": "dms",
|
||||
"_dms_raw_record": self._raw_record,
|
||||
"_test_mode": self.test_mode
|
||||
}
|
||||
return endpoint_dict
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DMSEndpoint Method:{self.method} Path:{self.path} Title:'{self.title}' Mode:{self.test_mode}>"
|
||||
|
||||
Endpoint = Union[YAPIEndpoint, SwaggerEndpoint, DMSEndpoint]
|
||||
|
||||
class ParsedAPISpec:
|
||||
"""解析后的API规范的通用基类"""
|
||||
def __init__(self, spec_type: str, endpoints: List[Union[YAPIEndpoint, SwaggerEndpoint]], spec: Dict[str, Any]):
|
||||
"""Base class for a parsed API specification from any source."""
|
||||
def __init__(self, spec_type: str, endpoints: List[Union[YAPIEndpoint, SwaggerEndpoint, 'DMSEndpoint']], spec: Dict[str, Any]):
|
||||
self.spec_type = spec_type
|
||||
self.endpoints = endpoints
|
||||
self.spec = spec # Store the original full spec dictionary, useful for $ref resolution if not pre-resolved
|
||||
@@ -392,6 +439,11 @@ class ParsedSwaggerSpec(ParsedAPISpec):
|
||||
super().__init__(spec_type="swagger", endpoints=endpoints, spec=spec)
|
||||
self.tags = tags
|
||||
|
||||
class ParsedDMSSpec(ParsedAPISpec):
|
||||
"""Parsed specification from the dynamic DMS source."""
|
||||
def __init__(self, endpoints: List[DMSEndpoint], spec: Dict[str, Any]):
|
||||
super().__init__(spec_type="dms", endpoints=endpoints, spec=spec)
|
||||
|
||||
class InputParser:
|
||||
"""负责解析输入(如YAPI JSON)并提取API端点信息"""
|
||||
def __init__(self):
|
||||
@@ -478,4 +530,207 @@ class InputParser:
|
||||
self.logger.error(f"Error decoding JSON from Swagger spec file {file_path}: {e}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"An unexpected error occurred while parsing Swagger spec {file_path}: {e}", exc_info=True)
|
||||
return None
|
||||
return None
|
||||
|
||||
def parse_dms_spec(self, domain_mapping_path: str, base_url: str, headers: Optional[Dict[str, str]] = None) -> Optional[ParsedDMSSpec]:
|
||||
self.logger.info(f"Starting DMS spec parsing. Base URL: {base_url}, Domain Map: {domain_mapping_path}")
|
||||
headers = headers or {}
|
||||
|
||||
try:
|
||||
with open(domain_mapping_path, 'r', encoding='utf-8') as f:
|
||||
DOMAIN_MAP = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
self.logger.warning(f"Could not load or parse domain map file '{domain_mapping_path}'. Using default domain. Error: {e}")
|
||||
DOMAIN_MAP = {}
|
||||
|
||||
list_url = urljoin(base_url, "/api/schema/manage/schema")
|
||||
self.logger.info(f"Fetching API list from: {list_url}")
|
||||
try:
|
||||
response = requests.get(list_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
api_list_data = response.json()
|
||||
|
||||
# 检查业务代码是否成功
|
||||
if api_list_data.get("code") != 0:
|
||||
self.logger.error(f"DMS API list endpoint returned a business error: {api_list_data.get('message')}")
|
||||
return None
|
||||
|
||||
# 从分页结构中提取 'records'
|
||||
api_records = api_list_data.get("data", {}).get("records", [])
|
||||
if not api_records:
|
||||
self.logger.warning("DMS API list is empty or 'records' key is missing in the response data.")
|
||||
# Returning an empty spec is valid if the list is just empty.
|
||||
return ParsedDMSSpec(endpoints=[], spec={"dms_api_list": []})
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"Failed to fetch API list from DMS: {e}")
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
self.logger.error("Failed to decode JSON response from DMS API list.")
|
||||
return None
|
||||
|
||||
endpoints: List[DMSEndpoint] = []
|
||||
|
||||
for item in api_records:
|
||||
domain_name = item.get('domain')
|
||||
name = item.get('name')
|
||||
model_id = item.get('id')
|
||||
if not all(k in item for k in ['domain', 'name', 'id']):
|
||||
self.logger.warning(f"Skipping an item in API list because it's missing 'domain', 'name', or 'id': {item}")
|
||||
continue
|
||||
|
||||
instance_code = DOMAIN_MAP.get(domain_name, domain_name)
|
||||
model_url = urljoin(base_url, f"/api/schema/manage/schema/{model_id}")
|
||||
self.logger.info(f"Fetching model for '{name}' from: {model_url}")
|
||||
|
||||
try:
|
||||
response = requests.get(model_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
model_schema_response = response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"Error fetching model for '{name}': {e}")
|
||||
continue
|
||||
except json.JSONDecodeError:
|
||||
self.logger.error(f"Failed to decode JSON for model '{name}'.")
|
||||
continue
|
||||
|
||||
if not model_schema_response or 'data' not in model_schema_response or not model_schema_response['data']:
|
||||
self.logger.warning(f"Skipping API '{name}' due to missing or empty model schema in response.")
|
||||
continue
|
||||
|
||||
model_data = model_schema_response['data']
|
||||
model = model_data.get('model')
|
||||
if not model or 'properties' not in model or not model['properties']:
|
||||
self.logger.warning(f"Skipping API '{name}' due to missing or invalid 'model' object in schema.")
|
||||
continue
|
||||
|
||||
pk_name = next(iter(model['properties']), None)
|
||||
if not pk_name:
|
||||
self.logger.warning(f"Skipping API '{name}' because no properties found in model to identify a primary key.")
|
||||
continue
|
||||
pk_schema = model['properties'][pk_name]
|
||||
|
||||
version = model_data.get('version', '1.0.0')
|
||||
dms_instance_code = instance_code
|
||||
category_name = domain_name
|
||||
|
||||
success_response = {
|
||||
"200": { "description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": {"type": "boolean"}}}}}}}
|
||||
|
||||
# Create Endpoint (POST)
|
||||
create_path = f"/api/dms/{dms_instance_code}/v1/{name}"
|
||||
create_request_body_schema = {"type": "object", "properties": {"version": {"type": "string", "example": version}, "act": {"type": "integer", "example": 0}, "data": {"type": "array", "items": model}}, "required": ["data"]}
|
||||
endpoints.append(DMSEndpoint(path=create_path, method='post', title=f"Create {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='standalone', operation_id=f"create_{name}", category_name=category_name, raw_record=item))
|
||||
|
||||
# List Endpoint (POST)
|
||||
list_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}"
|
||||
list_response_schema = {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": {"type": "array", "items": model}}}
|
||||
endpoints.append(DMSEndpoint(path=list_path, method='post', title=f"List {name}", request_body={'content': {'application/json': {'schema': {}}}}, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': list_response_schema}}}}, test_mode='scenario_only', operation_id=f"list_{name}", category_name=category_name, raw_record=item))
|
||||
|
||||
# Read Endpoint (GET)
|
||||
read_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}/{{id}}"
|
||||
read_response_schema = {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": model}}
|
||||
read_parameters = [{'name': 'id', 'in': 'path', 'required': True, 'description': f'The ID of the {name}, maps to {pk_name}', 'schema': pk_schema}]
|
||||
endpoints.append(DMSEndpoint(path=read_path, method='get', title=f"Read {name}", request_body=None, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': read_response_schema}}}}, parameters=read_parameters, test_mode='scenario_only', operation_id=f"read_{name}", category_name=category_name, raw_record=item))
|
||||
|
||||
# Update Endpoint (PUT)
|
||||
update_path = f"/api/dms/{dms_instance_code}/v1/{name}"
|
||||
endpoints.append(DMSEndpoint(path=update_path, method='put', title=f"Update {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"update_{name}", category_name=category_name, raw_record=item))
|
||||
|
||||
# Delete Endpoint (DELETE)
|
||||
delete_path = f"/api/dms/{dms_instance_code}/v1/{name}"
|
||||
delete_request_body_schema = {"type": "object", "properties": {"version": {"type": "string", "example": version}, "data": {"type": "array", "items": {"type": "object", "properties": { pk_name: pk_schema }, "required": [pk_name]}}}, "required": ["data"]}
|
||||
endpoints.append(DMSEndpoint(path=delete_path, method='delete', title=f"Delete {name}", request_body={'content': {'application/json': {'schema': delete_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"delete_{name}", category_name=category_name, raw_record=item))
|
||||
|
||||
# The 'spec' for ParsedDMSSpec should represent the whole document.
|
||||
# We can construct a dictionary holding all the raw data we fetched.
|
||||
dms_full_spec_dict = {"dms_api_list": api_records}
|
||||
return ParsedDMSSpec(endpoints=endpoints, spec=dms_full_spec_dict)
|
||||
|
||||
class DmsConfig:
|
||||
def __init__(self, base_url: str, domain_map_file: str, headers: Optional[Dict[str, str]] = None):
|
||||
self.base_url = base_url
|
||||
self.domain_map_file = domain_map_file
|
||||
self.headers = headers
|
||||
|
||||
def get_endpoints_from_swagger(file_path: str) -> Tuple[List[SwaggerEndpoint], str]:
|
||||
"""
|
||||
Parses a Swagger/OpenAPI JSON file and returns a list of SwaggerEndpoint objects
|
||||
and the base path of the API.
|
||||
"""
|
||||
logger.info(f"Parsing Swagger/OpenAPI spec from: {file_path}")
|
||||
all_endpoints: List[SwaggerEndpoint] = []
|
||||
swagger_tags: List[Dict[str, Any]] = []
|
||||
raw_spec_data_dict: Optional[Dict[str, Any]] = None # Swagger/OpenAPI is a single root object
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
# TODO: Add YAML support if needed, e.g., using PyYAML
|
||||
raw_spec_data_dict = json.load(f)
|
||||
|
||||
if not isinstance(raw_spec_data_dict, dict):
|
||||
logger.error(f"Swagger spec file {file_path} does not contain a JSON object as expected.")
|
||||
return [], ""
|
||||
|
||||
swagger_tags = raw_spec_data_dict.get("tags", [])
|
||||
paths = raw_spec_data_dict.get("paths", {})
|
||||
|
||||
for path, path_item_obj in paths.items():
|
||||
if not isinstance(path_item_obj, dict): continue
|
||||
for method, operation_obj in path_item_obj.items():
|
||||
# Common methods, can be extended
|
||||
if method.lower() not in ["get", "post", "put", "delete", "patch", "options", "head", "trace"]:
|
||||
continue # Skip non-standard HTTP methods or extensions like 'parameters' at path level
|
||||
if not isinstance(operation_obj, dict): continue
|
||||
try:
|
||||
# Pass the full raw_spec_data_dict for $ref resolution within SwaggerEndpoint
|
||||
swagger_endpoint = SwaggerEndpoint(path, method, operation_obj, global_spec=raw_spec_data_dict)
|
||||
all_endpoints.append(swagger_endpoint)
|
||||
except Exception as e_ep:
|
||||
logger.error(f"Error processing Swagger endpoint: {method.upper()} {path}. Error: {e_ep}", exc_info=True)
|
||||
|
||||
return all_endpoints, raw_spec_data_dict.get("basePath", "") if raw_spec_data_dict.get("basePath") else ""
|
||||
except FileNotFoundError:
|
||||
# It's better to log this error. Assuming a logger is available at self.logger
|
||||
logger.error(f"Swagger spec file not found: {file_path}")
|
||||
return [], ""
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error decoding JSON from Swagger spec file {file_path}: {e}")
|
||||
return [], ""
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred while parsing {file_path}: {e}", exc_info=True)
|
||||
return [], ""
|
||||
|
||||
|
||||
def parse_input_to_endpoints(input_type: str, input_path: str, dms_config: DmsConfig = None) -> Tuple[List[Endpoint], str]:
|
||||
"""
|
||||
Parses input from a given type (YAPI, Swagger, DMS) and returns a list of Endpoint objects
|
||||
and the base path of the API.
|
||||
"""
|
||||
parser = InputParser()
|
||||
if input_type == "yapi":
|
||||
parsed_spec = parser.parse_yapi_spec(input_path)
|
||||
if parsed_spec:
|
||||
return parsed_spec.endpoints, "" # YAPI doesn't have a base path in the same sense as Swagger
|
||||
elif input_type == "swagger":
|
||||
# The standalone get_endpoints_from_swagger is simple, but for consistency let's use the parser
|
||||
parsed_spec = parser.parse_swagger_spec(input_path)
|
||||
if parsed_spec:
|
||||
base_path = parsed_spec.spec.get("basePath", "") or ""
|
||||
# servers URL might be more modern (OpenAPI 3)
|
||||
if not base_path and "servers" in parsed_spec.spec and parsed_spec.spec["servers"]:
|
||||
# Use the first server URL
|
||||
base_path = parsed_spec.spec["servers"][0].get("url", "")
|
||||
return parsed_spec.endpoints, base_path
|
||||
elif input_type == "dms":
|
||||
if dms_config:
|
||||
parsed_spec = parser.parse_dms_spec(dms_config.domain_map_file, dms_config.base_url, dms_config.headers)
|
||||
if parsed_spec:
|
||||
return parsed_spec.endpoints, dms_config.base_url
|
||||
else:
|
||||
logger.error("DMS configuration not provided for DMS input type.")
|
||||
return [], ""
|
||||
else:
|
||||
logger.error(f"Unsupported input type: {input_type}")
|
||||
return [], ""
|
||||
|
||||
return [], ""
|
||||
Reference in New Issue
Block a user