Source code for pain001.templates.guardrails

# Copyright (C) 2023-2026 Pain001. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
# Licensed under either of the Apache License, Version 2.0 or the MIT
# License, at your option. You may not use this file except in
# compliance with one of those licences. Copies are provided in
# LICENSE-APACHE and LICENSE-MIT.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licences is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the applicable Licence for the specific language
# governing permissions and limitations.

"""Guardrails that detect template/schema drift."""

from __future__ import annotations

import re
from pathlib import Path

from pain001.templates.registry import (
    DEFAULT_TEMPLATE_REGISTRY,
    TemplateMetadata,
    TemplateRegistry,
)


[docs] class SchemaGuardrailError(ValueError): """Raised when template metadata or namespaces drift."""
_MESSAGE_TYPE_PATTERN = re.compile(r"pain\.\d{3}\.\d{3}\.\d{2}") def _extract_message_type(path: Path) -> str: """Extract the pain.NNN.NNN.NN message type referenced inside a file.""" match = _MESSAGE_TYPE_PATTERN.search(path.read_text(encoding="utf-8")) if not match: raise SchemaGuardrailError( f"Could not determine message type from {path}" ) return match.group(0)
[docs] def validate_template_bundle(metadata: TemplateMetadata) -> None: """Validate one template bundle for existence and drift.""" for required_path in [metadata.template_path, metadata.xsd_path]: if not required_path.exists(): raise SchemaGuardrailError( f"Missing required file: {required_path}" ) for candidate in [ metadata.template_path, metadata.xsd_path, metadata.example_xml_path, ]: if candidate is None: continue discovered_message_type = _extract_message_type(candidate) if discovered_message_type != metadata.message_type: raise SchemaGuardrailError( f"Message type drift for {candidate}: " f"expected {metadata.message_type}, " f"found {discovered_message_type}" )
[docs] def validate_registry( registry: TemplateRegistry = DEFAULT_TEMPLATE_REGISTRY, ) -> list[str]: """Validate every registered template and return validated IDs.""" validated: list[str] = [] for metadata in registry.list_templates(): validate_template_bundle(metadata) validated.append(metadata.message_type) return validated