{{breadcrumbs}}
← BACK TO HUB
HUB / lib_bejson_validator.py

lib_bejson_validator.py

Runtime
Python
Category
Core
Path
/storage/emulated/0/Projects/Management/Libraries/py/Core/lib_bejson_validator.py
FILE // lib_bejson_validator.py
"""
Library:     lib_bejson_validator.py
MFDB Version: 1.3.1
Format_Creator: Elton Boehnen
Status:      OFFICIAL - v1.3.1
Date:        2026-05-06
"""
"""
Library:     lib_bejson_validator.py
Family:      Core
Jurisdiction: ["PYTHON", "BEJSON_LIBRARIES"]
Status:      OFFICIAL — Core-Command/Lib (v1.21)
Author:      Elton Boehnen
Version:     1.3 OFFICIAL
Date:        2026-05-01
Description: BEJSON validator — schema validation for 104, 104a, 104db.
"""
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

E_INVALID_JSON = 1
E_MISSING_MANDATORY_KEY = 2
E_INVALID_FORMAT = 3
E_INVALID_VERSION = 4
E_INVALID_RECORDS_TYPE = 5
E_INVALID_FIELDS = 6
E_INVALID_VALUES = 7
E_TYPE_MISMATCH = 8
E_RECORD_LENGTH_MISMATCH = 9
E_RESERVED_KEY_COLLISION = 10
E_INVALID_RECORD_TYPE_PARENT = 11
E_NULL_VIOLATION = 12
E_FILE_NOT_FOUND = 13
E_PERMISSION_DENIED = 14

VALID_VERSIONS = {"104", "104a", "104db"}
MANDATORY_KEYS = ("Format", "Format_Version", "Format_Creator", "Records_Type", "Fields", "Values")

@dataclass
class ValidationState:
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    current_file: str = ""
    def reset(self):
        self.errors.clear()
        self.warnings.clear()
        self.current_file = ""

_state = ValidationState()

class BEJSONValidationError(Exception):
    def __init__(self, message: str, code: int):
        super().__init__(message)
        self.code = code

def bejson_validator_reset_state(): _state.reset()
def bejson_validator_get_errors(): return _state.errors
def bejson_validator_error_count(): return len(_state.errors)
def bejson_validator_has_errors(): return len(_state.errors) > 0
def bejson_validator_get_warnings(): return _state.warnings
def bejson_validator_warning_count(): return len(_state.warnings)
def bejson_validator_has_warnings(): return len(_state.warnings) > 0

def bejson_validator_check_json_syntax(input_, is_file=False):
    if is_file:
        path = Path(input_)
        if not path.exists(): raise BEJSONValidationError(f"File not found: {input_}", E_FILE_NOT_FOUND)
        text = path.read_text(encoding="utf-8")
        _state.current_file = str(path)
    else: text = input_
    if isinstance(text, dict): return text
    try: return json.loads(text)
    except Exception as e: raise BEJSONValidationError(f"Invalid JSON: {e}", E_INVALID_JSON)

def bejson_validator_check_mandatory_keys(doc):
    for key in MANDATORY_KEYS:
        if key not in doc: raise BEJSONValidationError(f"Missing key: {key}", E_MISSING_MANDATORY_KEY)
    if doc["Format"] != "BEJSON": raise BEJSONValidationError("Invalid Format", E_INVALID_FORMAT)
    if doc["Format_Creator"] != "Elton Boehnen":
        raise BEJSONValidationError("Invalid Format_Creator: Must be 'Elton Boehnen'", E_INVALID_FORMAT)
    version = doc.get("Format_Version", "")
    if version not in VALID_VERSIONS: raise BEJSONValidationError(f"Invalid version: {version}", E_INVALID_VERSION)
    return version

def bejson_validator_check_records_type(doc, version):
    rt = doc["Records_Type"]
    if not isinstance(rt, list):
        raise BEJSONValidationError("Records_Type must be a list", E_INVALID_RECORDS_TYPE)
    count = len(rt)
    if version in ("104", "104a"):
        if count != 1:
            raise BEJSONValidationError(f"BEJSON {version} must have exactly 1 record type. Found {count}.", E_INVALID_RECORDS_TYPE)
    elif version == "104db":
        if count < 2:
            raise BEJSONValidationError("104db requires 2+ types", E_INVALID_RECORDS_TYPE)

def bejson_validator_check_record_type_parent(doc, version):
    """Specific check for Record_Type_Parent consistency in 104db."""
    if version != "104db":
        return True
    
    fields = doc["Fields"]
    if not fields or fields[0].get("name") != "Record_Type_Parent":
        raise BEJSONValidationError("104db first field must be 'Record_Type_Parent'", E_INVALID_RECORD_TYPE_PARENT)
    
    valid_types = set(doc["Records_Type"])
    for i, record in enumerate(doc["Values"]):
        rtp = record[0]
        if rtp not in valid_types:
            raise BEJSONValidationError(f"Invalid Record_Type_Parent '{rtp}' at row {i}", E_INVALID_RECORD_TYPE_PARENT)
    return True

def bejson_validator_check_fields_structure(doc, version):
    fields = doc["Fields"]
    for i, f in enumerate(fields):
        fname = f.get("name")
        ftype = f.get("type")
        if not fname or not ftype:
            raise BEJSONValidationError(f"Field {i} missing name or type", E_INVALID_FIELDS)
        
        if version == "104a" and ftype in ("array", "object"):
            raise BEJSONValidationError(f"104a forbids complex type: {ftype}", E_INVALID_FIELDS)
            
        if version == "104db":
            if fname != "Record_Type_Parent" and "Record_Type_Parent" not in f:
                 raise BEJSONValidationError(f"Field '{fname}' missing Record_Type_Parent in 104db", E_INVALID_RECORD_TYPE_PARENT)
    return len(fields)

def bejson_validator_check_values(doc, version, fields_count):
    fields = doc["Fields"]
    for i, record in enumerate(doc["Values"]):
        if len(record) != fields_count:
            raise BEJSONValidationError(f"Length mismatch at row {i}", E_RECORD_LENGTH_MISMATCH)
        
        # Type validation
        for j, val in enumerate(record):
            ftype = fields[j].get("type")
            if val is None:
                continue # Nulls are generally allowed unless specified otherwise
            
            if ftype == "string" and not isinstance(val, str):
                 raise BEJSONValidationError(f"Type mismatch at row {i}, col {j}: expected string", E_TYPE_MISMATCH)
            if ftype == "integer" and not isinstance(val, int):
                 raise BEJSONValidationError(f"Type mismatch at row {i}, col {j}: expected integer", E_TYPE_MISMATCH)
            if ftype == "number" and not isinstance(val, (int, float)):
                 raise BEJSONValidationError(f"Type mismatch at row {i}, col {j}: expected number", E_TYPE_MISMATCH)
            if ftype == "boolean" and not isinstance(val, bool):
                 raise BEJSONValidationError(f"Type mismatch at row {i}, col {j}: expected boolean", E_TYPE_MISMATCH)

def bejson_validator_check_dependencies(doc):
    """Stub for dependency checking - to be implemented if needed by spec."""
    return True

def bejson_validator_check_custom_headers(doc, version):
    mandatory_set = set(MANDATORY_KEYS)
    for key in doc:
        if key in mandatory_set or key == "Parent_Hierarchy": continue
        if version in ("104", "104db"):
            raise BEJSONValidationError(f"Custom key '{key}' forbidden in {version}", E_RESERVED_KEY_COLLISION)

def bejson_validator_validate_string(json_string):
    bejson_validator_reset_state()
    doc = bejson_validator_check_json_syntax(json_string)
    version = bejson_validator_check_mandatory_keys(doc)
    bejson_validator_check_custom_headers(doc, version)
    bejson_validator_check_records_type(doc, version)
    bejson_validator_check_record_type_parent(doc, version)
    fields_count = bejson_validator_check_fields_structure(doc, version)
    bejson_validator_check_values(doc, version, fields_count)
    bejson_validator_check_dependencies(doc)
    return True

def bejson_validator_validate_file(file_path):
    text = Path(file_path).read_text(encoding="utf-8")
    return bejson_validator_validate_string(text)

def bejson_validator_get_report(json_string, is_file=False):
    valid = False
    try:
        valid = bejson_validator_validate_file(json_string) if is_file else bejson_validator_validate_string(json_string)
    except: pass
    rep = [f"Status: {'VALID' if valid else 'INVALID'}", f"Errors: {len(_state.errors)}"]
    if _state.errors: rep.extend(_state.errors)
    return "\n".join(rep)