"""统一UI字典的启动期加载、严格校验与只读序列化。""" from __future__ import annotations import json import re from dataclasses import dataclass from enum import StrEnum from pathlib import Path from types import MappingProxyType from typing import Any, Mapping from dms.common.enums import ( AllowedAction, AllowedModule, AttachmentType, AuditAction, AuditResult, AuditTarget, CategoryType, DocumentStatus, DocumentType, EnabledStatus, RoleCode, SecurityLevel, SubjectType, VisibilityType, ) DICTIONARY_ENUMS: Mapping[str, type[StrEnum]] = MappingProxyType( { "roleCodes": RoleCode, "allowedModules": AllowedModule, "documentTypes": DocumentType, "documentStatuses": DocumentStatus, "securityLevels": SecurityLevel, "visibilityTypes": VisibilityType, "attachmentTypes": AttachmentType, "subjectTypes": SubjectType, "allowedActions": AllowedAction, "categoryTypes": CategoryType, "enabledStatuses": EnabledStatus, "auditResults": AuditResult, "auditActions": AuditAction, "auditTargets": AuditTarget, } ) _VERSION_PATTERN = re.compile(r"^[0-9]{4}\.[0-9]+$") _LOCALE_PATTERN = re.compile(r"^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$") class UiDictionaryConfigError(RuntimeError): """不包含文件路径或原始内容的UI字典配置错误。""" @dataclass(frozen=True, slots=True) class DictionaryOption: code: str label: str sort_no: int def to_dict(self) -> dict[str, str | int]: return { "code": self.code, "label": self.label, "sortNo": self.sort_no, } @dataclass(frozen=True, slots=True) class UiDictionaryConfig: version: str locale: str dictionaries: Mapping[str, tuple[DictionaryOption, ...]] def to_dict(self) -> dict[str, Any]: return { "version": self.version, "locale": self.locale, "dictionaries": { name: [option.to_dict() for option in options] for name, options in self.dictionaries.items() }, } def _invalid(reason: str) -> UiDictionaryConfigError: return UiDictionaryConfigError(f"UI字典配置无效:{reason}") def _read_json(config_path: Path) -> Any: try: with config_path.open("r", encoding="utf-8") as source: return json.load(source) except FileNotFoundError: raise UiDictionaryConfigError("UI字典配置文件不存在") from None except UnicodeDecodeError: raise UiDictionaryConfigError( "UI字典配置不是有效的UTF-8文件" ) from None except json.JSONDecodeError: raise UiDictionaryConfigError("UI字典配置不是有效的JSON") from None except OSError: raise UiDictionaryConfigError("UI字典配置文件无法读取") from None def _validate_option( dictionary_name: str, raw_option: Any, seen_codes: set[str], ) -> DictionaryOption: if not isinstance(raw_option, dict): raise _invalid(f"{dictionary_name}的字典项必须是对象") if set(raw_option) != {"code", "label", "sortNo"}: raise _invalid(f"{dictionary_name}的字典项字段不合法") code = raw_option["code"] label = raw_option["label"] sort_no = raw_option["sortNo"] if not isinstance(code, str) or not code: raise _invalid(f"{dictionary_name}包含非法code") if code in seen_codes: raise _invalid(f"{dictionary_name}包含重复code") if not isinstance(label, str) or not label.strip(): raise _invalid(f"{dictionary_name}包含空label") if type(sort_no) is not int or sort_no < 0: raise _invalid(f"{dictionary_name}包含非法sortNo") seen_codes.add(code) return DictionaryOption(code=code, label=label.strip(), sort_no=sort_no) def load_ui_dictionary_config( config_path: str | Path, ) -> UiDictionaryConfig: """读取并严格校验一份完整UI字典配置。""" raw_config = _read_json(Path(config_path)) if not isinstance(raw_config, dict): raise _invalid("根节点必须是对象") if set(raw_config) != {"version", "locale", "dictionaries"}: raise _invalid("根节点字段不完整或包含未知字段") version = raw_config["version"] locale = raw_config["locale"] raw_dictionaries = raw_config["dictionaries"] if not isinstance(version, str) or not _VERSION_PATTERN.fullmatch(version): raise _invalid("version格式不合法") if not isinstance(locale, str) or not _LOCALE_PATTERN.fullmatch(locale): raise _invalid("locale格式不合法") if not isinstance(raw_dictionaries, dict): raise _invalid("dictionaries必须是对象") expected_names = set(DICTIONARY_ENUMS) actual_names = set(raw_dictionaries) if actual_names != expected_names: raise _invalid("dictionary名称不完整或包含未知名称") validated: dict[str, tuple[DictionaryOption, ...]] = {} for dictionary_name, enum_type in DICTIONARY_ENUMS.items(): raw_options = raw_dictionaries[dictionary_name] if not isinstance(raw_options, list): raise _invalid(f"{dictionary_name}必须是数组") seen_codes: set[str] = set() options = tuple( _validate_option(dictionary_name, option, seen_codes) for option in raw_options ) expected_codes = {member.value for member in enum_type} if seen_codes != expected_codes: raise _invalid(f"{dictionary_name}的code集合与服务端枚举不一致") validated[dictionary_name] = tuple( sorted(options, key=lambda option: (option.sort_no, option.code)) ) return UiDictionaryConfig( version=version, locale=locale, dictionaries=MappingProxyType(validated), ) __all__ = [ "DICTIONARY_ENUMS", "DictionaryOption", "UiDictionaryConfig", "UiDictionaryConfigError", "load_ui_dictionary_config", ]