| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- """统一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",
- ]
|