from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True)
class ValidationIssue:
code: str
path: str
message: str
@dataclass
class ValidationResult:
file: str
valid: bool = True
issues: list[ValidationIssue] = field(default_factory=list)
def add_issue(self, code: str, path: str, message: str) -> None:
self.valid = False
self.issues.append(ValidationIssue(code=code, path=path, message=message))
def sort_issues(self) -> None:
self.issues.sort(key=lambda issue: (issue.path, issue.code, issue.message))
SUPPORTED_TOP_LEVEL_FIELDS = {
"name",
"version",
"description",
"executablePath",
"requirePermissions",
"eventTypes",
"eventSchemas",
"hasSubCommand",
"inputSchema",
"outputSchema",
"subcommands",
}
SUPPORTED_SUBCOMMAND_FIELDS = {
"description",
"requirePermissions",
"inputSchema",
"outputSchema",
"eventTypes",
"eventSchemas",
}
SUPPORTED_SCHEMA_TYPES = {"string", "number", "integer", "boolean", "array", "object"}
SUPPORTED_STRING_FORMATS = {
"ipv4",
"ipv6",
"hostname",
"email",
"uri",
"date",
"date-time",
}
SCHEMA_ALLOWED_KEYS = {
"string": {"type", "description", "default", "enum", "minLength", "maxLength", "pattern", "format"},
"number": {
"type",
"description",
"default",
"enum",
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
},
"integer": {
"type",
"description",
"default",
"enum",
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
},
"boolean": {"type", "description", "default"},
"array": {"type", "description", "default", "items", "minItems", "maxItems", "uniqueItems"},
"object": {"type", "description", "default", "properties", "required"},
}
RESERVED_EVENT_TYPES = {"result"}
def is_integer_value(value: Any) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def is_number_value(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool)
def json_pointer(parent: str, child: str) -> str:
base = parent.rstrip("/")
escaped = child.replace("~", "~0").replace("/", "~1")
return f"{base}/{escaped}" if base else f"/{escaped}"