Initial commit: Add OpenAPI Mock Server project
This commit is contained in:
203
.src/openapi_mock/core/spec_parser.py
Normal file
203
.src/openapi_mock/core/spec_parser.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
"""Parse and validate OpenAPI specifications."""
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
from openapi_spec_validator import validate
|
||||||
|
from openapi_spec_validator.versions import consts as validator_consts
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAPISpecError(Exception):
|
||||||
|
"""Base exception for OpenAPI spec errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SpecValidationError(OpenAPISpecError):
|
||||||
|
"""Raised when OpenAPI spec validation fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SpecNotFoundError(OpenAPISpecError):
|
||||||
|
"""Raised when OpenAPI spec file is not found."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_spec(spec_path: str) -> Dict[str, Any]:
|
||||||
|
"""Load and parse an OpenAPI specification from a YAML or JSON file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec_path: Path to the OpenAPI specification file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed OpenAPI specification as a dictionary.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SpecNotFoundError: If the spec file does not exist.
|
||||||
|
SpecValidationError: If the spec is invalid.
|
||||||
|
"""
|
||||||
|
path = Path(spec_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise SpecNotFoundError(f"OpenAPI spec file not found: {spec_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
spec = yaml.safe_load(f)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
raise OpenAPISpecError(f"Invalid YAML syntax in {spec_path}: {e}")
|
||||||
|
except IOError as e:
|
||||||
|
raise OpenAPISpecError(f"Error reading {spec_path}: {e}")
|
||||||
|
|
||||||
|
if spec is None:
|
||||||
|
raise OpenAPISpecError(f"Empty specification file: {spec_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate(spec)
|
||||||
|
except Exception as e:
|
||||||
|
raise SpecValidationError(f"OpenAPI spec validation failed: {e}")
|
||||||
|
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
def extract_paths(spec: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Extract all paths and their operations from an OpenAPI spec.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: Parsed OpenAPI specification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping path strings to operation dictionaries.
|
||||||
|
"""
|
||||||
|
return spec.get('paths', {})
|
||||||
|
|
||||||
|
|
||||||
|
def extract_schemas(spec: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Extract all schemas from an OpenAPI spec.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: Parsed OpenAPI specification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping schema names to schema definitions.
|
||||||
|
"""
|
||||||
|
components = spec.get('components', {})
|
||||||
|
schemas = components.get('schemas', {})
|
||||||
|
|
||||||
|
if not schemas and 'definitions' in spec:
|
||||||
|
schemas = spec.get('definitions', {})
|
||||||
|
|
||||||
|
return schemas
|
||||||
|
|
||||||
|
|
||||||
|
def extract_path_params(path: str) -> List[str]:
|
||||||
|
"""Extract parameter names from a path string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: API path string like '/users/{id}/posts/{post_id}'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of parameter names without braces.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
params = re.findall(r'\{([^}]+)\}', path)
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def get_operation_id(path: str, method: str) -> str:
|
||||||
|
"""Generate an operation ID for a path/method combination.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: API path string.
|
||||||
|
method: HTTP method (get, post, put, delete, etc.).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated operation ID string.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
clean_path = path.lstrip('/').replace('/', '_').replace('-', '_')
|
||||||
|
params = re.findall(r'\{([^}]+)\}', clean_path)
|
||||||
|
for param in params:
|
||||||
|
clean_path = clean_path.replace(f'{{{param}}}', f'_{param}_')
|
||||||
|
method = method.lower()
|
||||||
|
return f"{method}_{clean_path}"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_security_schemes(spec: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Extract security schemes from an OpenAPI spec.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: Parsed OpenAPI specification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping security scheme names to scheme definitions.
|
||||||
|
"""
|
||||||
|
components = spec.get('components', {})
|
||||||
|
return components.get('securitySchemes', {})
|
||||||
|
|
||||||
|
|
||||||
|
def extract_global_security(spec: Dict[str, Any]) -> List[Dict[str, List[str]]]:
|
||||||
|
"""Extract global security requirements from an OpenAPI spec.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: Parsed OpenAPI specification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of security requirement dictionaries.
|
||||||
|
"""
|
||||||
|
return spec.get('security', [])
|
||||||
|
|
||||||
|
|
||||||
|
def get_response_schema(
|
||||||
|
spec: Dict[str, Any],
|
||||||
|
path: str,
|
||||||
|
method: str,
|
||||||
|
status_code: str = '200'
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get the response schema for a specific path/method/status_code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: Parsed OpenAPI specification.
|
||||||
|
path: API path string.
|
||||||
|
method: HTTP method.
|
||||||
|
status_code: Response status code.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response schema definition or None if not found.
|
||||||
|
"""
|
||||||
|
paths = spec.get('paths', {})
|
||||||
|
path_item = paths.get(path, {})
|
||||||
|
operation = path_item.get(method.lower(), {})
|
||||||
|
|
||||||
|
responses = operation.get('responses', {})
|
||||||
|
response = responses.get(status_code, responses.get('default', {}))
|
||||||
|
|
||||||
|
content = response.get('content', {})
|
||||||
|
if 'application/json' in content:
|
||||||
|
media_type = content['application/json']
|
||||||
|
return media_type.get('schema')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_spec_version(spec: Dict[str, Any]) -> str:
|
||||||
|
"""Get the OpenAPI version from the spec.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: Parsed OpenAPI specification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OpenAPI version string (e.g., "3.0.3", "2.0").
|
||||||
|
"""
|
||||||
|
return spec.get('openapi', spec.get('swagger', 'unknown'))
|
||||||
|
|
||||||
|
|
||||||
|
def is_openapi_3(spec: Dict[str, Any]) -> bool:
|
||||||
|
"""Check if the spec is OpenAPI 3.x.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: Parsed OpenAPI specification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if OpenAPI 3.x, False if Swagger 2.0.
|
||||||
|
"""
|
||||||
|
openapi_version = spec.get('openapi', '')
|
||||||
|
return openapi_version.startswith('3.')
|
||||||
Reference in New Issue
Block a user