Add source files: generator, validator, fuzzer modules
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
376
src/core/generator.py
Normal file
376
src/core/generator.py
Normal file
@@ -0,0 +1,376 @@
|
||||
"""Response Generator for creating mock responses from JSON Schema."""
|
||||
|
||||
import random
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
from faker import Faker
|
||||
|
||||
|
||||
class ResponseGeneratorError(Exception):
|
||||
"""Base exception for response generator errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedSchemaType(ResponseGeneratorError):
|
||||
"""Raised when an unsupported schema type is encountered."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ResponseGenerator:
|
||||
"""Generates realistic mock responses based on JSON Schema definitions."""
|
||||
|
||||
TYPE_MAPPING = {
|
||||
"string": "string",
|
||||
"integer": "integer",
|
||||
"number": "number",
|
||||
"boolean": "boolean",
|
||||
"array": "array",
|
||||
"object": "object",
|
||||
"null": "null",
|
||||
}
|
||||
|
||||
FORMAT_MAPPING = {
|
||||
"date": "date",
|
||||
"date-time": "date_time",
|
||||
"time": "time",
|
||||
"email": "email",
|
||||
"uri": "uri",
|
||||
"uuid": "uuid",
|
||||
"hostname": "hostname",
|
||||
"ipv4": "ipv4",
|
||||
"ipv6": "ipv6",
|
||||
"password": "password",
|
||||
}
|
||||
|
||||
def __init__(self, seed: int | None = None) -> None:
|
||||
"""Initialize the response generator.
|
||||
|
||||
Args:
|
||||
seed: Optional seed for reproducible random generation.
|
||||
"""
|
||||
self.faker = Faker()
|
||||
if seed is not None:
|
||||
Faker.seed(seed)
|
||||
random.seed(seed)
|
||||
|
||||
def generate(self, schema: dict[str, Any]) -> Any:
|
||||
"""Generate a mock value based on the given schema.
|
||||
|
||||
Args:
|
||||
schema: JSON Schema definition.
|
||||
|
||||
Returns:
|
||||
Generated mock value.
|
||||
|
||||
Raises:
|
||||
UnsupportedSchemaType: If the schema type is not supported.
|
||||
"""
|
||||
if not schema:
|
||||
return None
|
||||
|
||||
schema_type = schema.get("type")
|
||||
|
||||
if schema_type is None and "$ref" in schema:
|
||||
return self._resolve_ref(schema["$ref"], schema)
|
||||
|
||||
if schema_type is None:
|
||||
return None
|
||||
|
||||
type_key = schema_type.lower()
|
||||
if type_key not in self.TYPE_MAPPING:
|
||||
raise UnsupportedSchemaType(f"Unsupported schema type: {schema_type}")
|
||||
|
||||
generator_method = getattr(
|
||||
self, f"_generate_{type_key}", self._generate_default
|
||||
)
|
||||
return generator_method(schema)
|
||||
|
||||
def _generate_string(self, schema: dict[str, Any]) -> str:
|
||||
"""Generate a string value based on the schema.
|
||||
|
||||
Args:
|
||||
schema: String schema definition.
|
||||
|
||||
Returns:
|
||||
Generated string value.
|
||||
"""
|
||||
fmt = schema.get("format", "")
|
||||
pattern = schema.get("pattern")
|
||||
|
||||
if fmt and fmt.lower() in self.FORMAT_MAPPING:
|
||||
format_method_name = self.FORMAT_MAPPING[fmt.lower()]
|
||||
format_method = getattr(self.faker, format_method_name, None)
|
||||
if format_method:
|
||||
return format_method()
|
||||
|
||||
if pattern:
|
||||
return self._generate_by_pattern(pattern)
|
||||
|
||||
min_length = schema.get("minLength", 0)
|
||||
max_length = schema.get("maxLength", 100)
|
||||
enum_values = schema.get("enum")
|
||||
|
||||
if enum_values:
|
||||
return random.choice(enum_values)
|
||||
|
||||
actual_max = max(min_length, max_length)
|
||||
length = random.randint(min_length, actual_max) if actual_max > 0 else 10
|
||||
length = max(length, 5)
|
||||
|
||||
return self.faker.text(max_nb_chars=length)
|
||||
|
||||
def _generate_by_pattern(self, pattern: str) -> str:
|
||||
"""Generate a string matching a regex pattern.
|
||||
|
||||
Args:
|
||||
pattern: Regex pattern.
|
||||
|
||||
Returns:
|
||||
Generated string matching the pattern.
|
||||
"""
|
||||
if pattern == r"^[a-zA-Z0-9_-]{1,50}$":
|
||||
return self.faker.slug()
|
||||
|
||||
return self.faker.word()
|
||||
|
||||
def _generate_integer(self, schema: dict[str, Any]) -> int:
|
||||
"""Generate an integer value based on the schema.
|
||||
|
||||
Args:
|
||||
schema: Integer schema definition.
|
||||
|
||||
Returns:
|
||||
Generated integer value.
|
||||
"""
|
||||
minimum = schema.get("minimum", 0)
|
||||
maximum = schema.get("maximum", 1000)
|
||||
multiple_of = schema.get("multipleOf", 1)
|
||||
|
||||
value = random.randint(minimum, maximum)
|
||||
|
||||
if multiple_of > 1:
|
||||
value = (value // multiple_of) * multiple_of
|
||||
|
||||
return value
|
||||
|
||||
def _generate_number(self, schema: dict[str, Any]) -> float:
|
||||
"""Generate a number value based on the schema.
|
||||
|
||||
Args:
|
||||
schema: Number schema definition.
|
||||
|
||||
Returns:
|
||||
Generated number value.
|
||||
"""
|
||||
minimum = schema.get("minimum", 0.0)
|
||||
maximum = schema.get("maximum", 1000.0)
|
||||
multiple_of = schema.get("multipleOf", 1.0)
|
||||
|
||||
value = random.uniform(minimum, maximum)
|
||||
|
||||
if multiple_of > 1:
|
||||
value = round(value / multiple_of) * multiple_of
|
||||
|
||||
return round(value, 2)
|
||||
|
||||
def _generate_boolean(self, schema: dict[str, Any]) -> bool:
|
||||
"""Generate a boolean value based on the schema.
|
||||
|
||||
Args:
|
||||
schema: Boolean schema definition.
|
||||
|
||||
Returns:
|
||||
Generated boolean value.
|
||||
"""
|
||||
return random.choice([True, False])
|
||||
|
||||
def _generate_array(self, schema: dict[str, Any]) -> list[Any]:
|
||||
"""Generate an array based on the schema.
|
||||
|
||||
Args:
|
||||
schema: Array schema definition.
|
||||
|
||||
Returns:
|
||||
Generated array of values.
|
||||
"""
|
||||
items_schema = schema.get("items", {})
|
||||
min_items = schema.get("minItems", 0)
|
||||
max_items = schema.get("maxItems", 10)
|
||||
enum_values = schema.get("enum")
|
||||
|
||||
count = random.randint(min_items, max_items)
|
||||
|
||||
if enum_values:
|
||||
return [random.choice(enum_values) for _ in range(count)]
|
||||
|
||||
return [self.generate(items_schema) for _ in range(count)]
|
||||
|
||||
def _generate_object(self, schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Generate an object based on the schema.
|
||||
|
||||
Args:
|
||||
schema: Object schema definition.
|
||||
|
||||
Returns:
|
||||
Generated object as a dictionary.
|
||||
"""
|
||||
properties = schema.get("properties", {})
|
||||
required_fields = schema.get("required", [])
|
||||
additional_properties = schema.get("additionalProperties", True)
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
for prop_name, prop_schema in properties.items():
|
||||
if prop_name in required_fields or additional_properties:
|
||||
value = self.generate(prop_schema)
|
||||
result[prop_name] = self._convert_datetime(value)
|
||||
|
||||
return result
|
||||
|
||||
def _convert_datetime(self, value: Any) -> Any:
|
||||
"""Convert datetime objects to ISO format strings.
|
||||
|
||||
Args:
|
||||
value: The value to convert.
|
||||
|
||||
Returns:
|
||||
Converted value.
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
elif isinstance(value, date):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
def _generate_null(self, schema: dict[str, Any]) -> None:
|
||||
"""Generate a null value.
|
||||
|
||||
Args:
|
||||
schema: Null schema definition.
|
||||
|
||||
Returns:
|
||||
None.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _generate_default(self, schema: dict[str, Any]) -> Any:
|
||||
"""Default generator for unknown types.
|
||||
|
||||
Args:
|
||||
schema: Schema definition.
|
||||
|
||||
Returns:
|
||||
None as default value.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _resolve_ref(
|
||||
self, ref: str, schema: dict[str, Any]
|
||||
) -> Any:
|
||||
"""Resolve a $ref reference.
|
||||
|
||||
Args:
|
||||
ref: The reference string.
|
||||
schema: The current schema containing the reference.
|
||||
|
||||
Returns:
|
||||
The resolved value.
|
||||
"""
|
||||
if ref.startswith("#/components/schemas/"):
|
||||
ref_name = ref.split("/")[-1]
|
||||
return schema.get("resolved_schemas", {}).get(ref_name, {})
|
||||
|
||||
return None
|
||||
|
||||
def set_resolved_schemas(
|
||||
self, schemas: dict[str, Any]
|
||||
) -> None:
|
||||
"""Set resolved schema references.
|
||||
|
||||
Args:
|
||||
schemas: Dictionary of resolved schema names to schema objects.
|
||||
"""
|
||||
self._resolved_schemas = schemas
|
||||
|
||||
def generate_response(
|
||||
self,
|
||||
schema: dict[str, Any],
|
||||
status_code: int = 200,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Generate a complete HTTP response.
|
||||
|
||||
Args:
|
||||
schema: Response schema definition.
|
||||
status_code: HTTP status code.
|
||||
headers: Optional response headers.
|
||||
|
||||
Returns:
|
||||
Complete response dictionary.
|
||||
"""
|
||||
response_body = self.generate(schema)
|
||||
|
||||
response: dict[str, Any] = {
|
||||
"status_code": status_code,
|
||||
"body": response_body,
|
||||
}
|
||||
|
||||
if headers:
|
||||
response["headers"] = headers
|
||||
|
||||
return response
|
||||
|
||||
def generate_list_response(
|
||||
self,
|
||||
item_schema: dict[str, Any],
|
||||
count: int | None = None,
|
||||
status_code: int = 200,
|
||||
) -> dict[str, Any]:
|
||||
"""Generate a list/array response.
|
||||
|
||||
Args:
|
||||
item_schema: Schema for array items.
|
||||
count: Number of items to generate (random if not specified).
|
||||
status_code: HTTP status code.
|
||||
|
||||
Returns:
|
||||
Complete response with array body.
|
||||
"""
|
||||
if count is None:
|
||||
count = random.randint(1, 10)
|
||||
|
||||
items = [self.generate(item_schema) for _ in range(count)]
|
||||
|
||||
return {
|
||||
"status_code": status_code,
|
||||
"body": items,
|
||||
}
|
||||
|
||||
def generate_error_response(
|
||||
self,
|
||||
message: str,
|
||||
status_code: int = 400,
|
||||
error_type: str = "Bad Request",
|
||||
) -> dict[str, Any]:
|
||||
"""Generate an error response.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
status_code: HTTP status code.
|
||||
error_type: Type of error.
|
||||
|
||||
Returns:
|
||||
Error response dictionary.
|
||||
"""
|
||||
return {
|
||||
"status_code": status_code,
|
||||
"body": {
|
||||
"error": {
|
||||
"type": error_type,
|
||||
"message": message,
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user