114 lines
4.2 KiB
Python
114 lines
4.2 KiB
Python
"""Schema validation helpers for ComfyUI JSON nodes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from .constants import SUPPORTED_SCHEMA_TYPES
|
|
from .errors import push_error, truncate_repr
|
|
|
|
|
|
def type_matches(value: Any, expected: str) -> bool:
|
|
if expected == "string":
|
|
return isinstance(value, str)
|
|
if expected == "int":
|
|
return isinstance(value, int) and not isinstance(value, bool)
|
|
if expected == "float":
|
|
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
if expected == "boolean":
|
|
return isinstance(value, bool)
|
|
if expected == "array":
|
|
return isinstance(value, list)
|
|
if expected == "object":
|
|
return isinstance(value, dict)
|
|
return expected == "any"
|
|
|
|
|
|
def validate_schema(value: Any, schema: dict[str, Any], path: str, strict: bool, errors: list[dict[str, str]]) -> None:
|
|
if not isinstance(schema, dict):
|
|
push_error(errors, path, "Schema node must be an object", "invalid_schema")
|
|
return
|
|
|
|
expected_type = schema.get("type", "any")
|
|
if expected_type not in SUPPORTED_SCHEMA_TYPES:
|
|
push_error(errors, path, f"Unsupported schema type '{expected_type}'", "invalid_schema")
|
|
return
|
|
|
|
if not type_matches(value, expected_type):
|
|
push_error(
|
|
errors,
|
|
path,
|
|
f"Expected type '{expected_type}', got '{type(value).__name__}' (value: {truncate_repr(value)})",
|
|
"type_mismatch",
|
|
)
|
|
return
|
|
|
|
if "enum" in schema:
|
|
enum_values = schema["enum"]
|
|
if not isinstance(enum_values, list):
|
|
push_error(errors, path, "`enum` must be an array", "invalid_schema")
|
|
return
|
|
if value not in enum_values:
|
|
push_error(
|
|
errors,
|
|
path,
|
|
f"Value {truncate_repr(value)} not in allowed values {truncate_repr(enum_values)}",
|
|
"enum_mismatch",
|
|
)
|
|
|
|
if expected_type == "string" and "pattern" in schema:
|
|
pattern = schema["pattern"]
|
|
if not isinstance(pattern, str):
|
|
push_error(errors, path, "`pattern` must be a string", "invalid_schema")
|
|
return
|
|
try:
|
|
matches = re.search(pattern, value) is not None
|
|
except re.error as exc:
|
|
push_error(errors, path, f"Invalid regex pattern '{pattern}': {exc}", "invalid_schema")
|
|
return
|
|
if not matches:
|
|
push_error(
|
|
errors,
|
|
path,
|
|
f"Value {truncate_repr(value)} does not match pattern '{pattern}'",
|
|
"pattern_mismatch",
|
|
)
|
|
|
|
if expected_type == "object":
|
|
properties = schema.get("properties", {})
|
|
required = schema.get("required", [])
|
|
|
|
if not isinstance(properties, dict):
|
|
push_error(errors, path, "`properties` must be an object", "invalid_schema")
|
|
return
|
|
if not isinstance(required, list) or not all(isinstance(item, str) for item in required):
|
|
push_error(errors, path, "`required` must be an array of strings", "invalid_schema")
|
|
return
|
|
|
|
for required_key in required:
|
|
if required_key not in value:
|
|
push_error(errors, f"{path}.{required_key}", "Required key is missing", "required_missing")
|
|
|
|
if strict:
|
|
for key in value:
|
|
if key not in properties:
|
|
push_error(errors, f"{path}.{key}", "Key is not allowed in strict mode", "unexpected_key")
|
|
|
|
for key, sub_schema in properties.items():
|
|
if not isinstance(sub_schema, dict):
|
|
push_error(errors, f"{path}.{key}", "Property schema must be an object", "invalid_schema")
|
|
continue
|
|
if key in value:
|
|
validate_schema(value[key], sub_schema, f"{path}.{key}", strict, errors)
|
|
|
|
elif expected_type == "array":
|
|
item_schema = schema.get("items")
|
|
if item_schema is None:
|
|
return
|
|
if not isinstance(item_schema, dict):
|
|
push_error(errors, path, "`items` must be an object", "invalid_schema")
|
|
return
|
|
for index, item in enumerate(value):
|
|
validate_schema(item, item_schema, f"{path}[{index}]", strict, errors)
|