重构
This commit is contained in:
@@ -6,7 +6,17 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.conf_models import ConfBaseConfig, ConfBuiltin, ConfGroup, ConfModule, ConfSelector, ConfSource
|
||||
from app.conf_models import (
|
||||
ConfBaseConfig,
|
||||
ConfBuiltin,
|
||||
ConfGroup,
|
||||
ConfInlineRule,
|
||||
ConfModule,
|
||||
ConfPolicyOverride,
|
||||
ConfProfileConfig,
|
||||
ConfSelector,
|
||||
ConfSource,
|
||||
)
|
||||
|
||||
_ENV_PATTERN = re.compile(r"\$\{([A-Z0-9_]+)\}")
|
||||
_SCALAR_KEYS = {
|
||||
@@ -23,6 +33,15 @@ _SCALAR_KEYS = {
|
||||
_BOOL_KEYS = {"allow_lan", "ipv6", "append_userinfo_header"}
|
||||
_MULTI_KEYS = {"source", "selector", "group", "module", "builtin"}
|
||||
_SOURCE_TYPES = {"url", "file", "inline", "base64"}
|
||||
_PROFILE_SCALAR_KEYS = {
|
||||
"name",
|
||||
"enabled",
|
||||
"sources",
|
||||
"include_modules",
|
||||
"exclude_modules",
|
||||
"include_builtins",
|
||||
}
|
||||
_PROFILE_MULTI_KEYS = {"override_policy", "prepend_rule", "append_rule"}
|
||||
|
||||
|
||||
class ConfConfigError(ValueError):
|
||||
@@ -60,6 +79,25 @@ def _parse_csv_fields(value: str, *, path: Path, line_no: int) -> list[str]:
|
||||
return [field.strip() for field in row]
|
||||
|
||||
|
||||
def _split_key_value(raw_line: str, *, path: Path, line_no: int) -> tuple[str, str]:
|
||||
if "=" not in raw_line:
|
||||
raise ConfConfigError(path, line_no, "expected key = value")
|
||||
key, value = [part.strip() for part in raw_line.split("=", 1)]
|
||||
if not key:
|
||||
raise ConfConfigError(path, line_no, "key cannot be empty")
|
||||
return key, value
|
||||
|
||||
|
||||
def _iter_conf_lines(path: Path) -> list[tuple[int, str]]:
|
||||
records: list[tuple[int, str]] = []
|
||||
for line_no, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or line.startswith(";"):
|
||||
continue
|
||||
records.append((line_no, raw_line))
|
||||
return records
|
||||
|
||||
|
||||
def _ensure_named_records(records: list[Any], *, key: str, path: Path) -> None:
|
||||
seen: dict[str, int] = {}
|
||||
for record in records:
|
||||
@@ -175,20 +213,75 @@ def _parse_builtin(value: str, *, path: Path, line_no: int) -> ConfBuiltin:
|
||||
)
|
||||
|
||||
|
||||
def _parse_string_list(value: str, *, path: Path, line_no: int) -> list[str]:
|
||||
fields = _parse_csv_fields(_expand_env(value), path=path, line_no=line_no)
|
||||
return [field for field in fields if field]
|
||||
|
||||
|
||||
def _parse_override_policy(value: str, *, path: Path, line_no: int) -> ConfPolicyOverride:
|
||||
fields = _parse_csv_fields(value, path=path, line_no=line_no)
|
||||
if len(fields) != 2:
|
||||
raise ConfConfigError(path, line_no, "override_policy requires exactly 2 fields: module_key,new_policy")
|
||||
module_key, policy = fields
|
||||
if not module_key or not policy:
|
||||
raise ConfConfigError(path, line_no, "override_policy module key and policy cannot be empty")
|
||||
return ConfPolicyOverride(module_key=module_key, policy=policy, line_no=line_no)
|
||||
|
||||
|
||||
def _parse_inline_rule(value: str, *, path: Path, line_no: int, key: str) -> ConfInlineRule:
|
||||
expanded = _expand_env(value).strip()
|
||||
if not expanded:
|
||||
raise ConfConfigError(path, line_no, f"{key} cannot be empty")
|
||||
return ConfInlineRule(value=expanded, line_no=line_no)
|
||||
|
||||
|
||||
def _validate_profile_references(
|
||||
profile: ConfProfileConfig,
|
||||
base: ConfBaseConfig,
|
||||
*,
|
||||
path: Path,
|
||||
field_lines: dict[str, int],
|
||||
) -> None:
|
||||
source_keys = {item.key for item in base.sources}
|
||||
module_keys = {item.key for item in base.modules}
|
||||
builtin_keys = {item.key for item in base.builtins}
|
||||
|
||||
for source in profile.sources:
|
||||
if source not in source_keys:
|
||||
raise ConfConfigError(path, field_lines.get("sources", 1), f"profile references unknown source: {source}")
|
||||
for module in profile.include_modules:
|
||||
if module not in module_keys:
|
||||
raise ConfConfigError(
|
||||
path,
|
||||
field_lines.get("include_modules", 1),
|
||||
f"profile references unknown module in include_modules: {module}",
|
||||
)
|
||||
for module in profile.exclude_modules:
|
||||
if module not in module_keys:
|
||||
raise ConfConfigError(
|
||||
path,
|
||||
field_lines.get("exclude_modules", 1),
|
||||
f"profile references unknown module in exclude_modules: {module}",
|
||||
)
|
||||
for builtin in profile.include_builtins:
|
||||
if builtin not in builtin_keys:
|
||||
raise ConfConfigError(
|
||||
path,
|
||||
field_lines.get("include_builtins", 1),
|
||||
f"profile references unknown builtin in include_builtins: {builtin}",
|
||||
)
|
||||
for override in profile.override_policies:
|
||||
if override.module_key not in module_keys:
|
||||
raise ConfConfigError(path, override.line_no, f"override_policy references unknown module: {override.module_key}")
|
||||
|
||||
|
||||
def load_conf_base(path: Path) -> ConfBaseConfig:
|
||||
config = ConfBaseConfig()
|
||||
scalar_values: dict[str, Any] = {}
|
||||
config_dir = path.resolve().parent
|
||||
|
||||
for line_no, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or line.startswith(";"):
|
||||
continue
|
||||
if "=" not in raw_line:
|
||||
raise ConfConfigError(path, line_no, "expected key = value")
|
||||
key, value = [part.strip() for part in raw_line.split("=", 1)]
|
||||
if not key:
|
||||
raise ConfConfigError(path, line_no, "key cannot be empty")
|
||||
for line_no, raw_line in _iter_conf_lines(path):
|
||||
key, value = _split_key_value(raw_line, path=path, line_no=line_no)
|
||||
if key in _SCALAR_KEYS:
|
||||
if key in scalar_values:
|
||||
raise ConfConfigError(path, line_no, f"duplicate scalar key: {key}")
|
||||
@@ -220,3 +313,48 @@ def load_conf_base(path: Path) -> ConfBaseConfig:
|
||||
_ensure_named_records(config.modules, key="key", path=path)
|
||||
_ensure_named_records(config.builtins, key="key", path=path)
|
||||
return config
|
||||
|
||||
|
||||
def load_conf_profile(path: Path, *, base_config: ConfBaseConfig) -> ConfProfileConfig:
|
||||
profile = ConfProfileConfig()
|
||||
scalar_values: dict[str, Any] = {}
|
||||
field_lines: dict[str, int] = {}
|
||||
|
||||
for line_no, raw_line in _iter_conf_lines(path):
|
||||
key, value = _split_key_value(raw_line, path=path, line_no=line_no)
|
||||
if key in _PROFILE_SCALAR_KEYS:
|
||||
if key in scalar_values:
|
||||
raise ConfConfigError(path, line_no, f"duplicate scalar key: {key}")
|
||||
field_lines[key] = line_no
|
||||
if key == "enabled":
|
||||
scalar_values[key] = _parse_bool(_expand_env(value), path=path, line_no=line_no, key=key)
|
||||
elif key in {"sources", "include_modules", "exclude_modules", "include_builtins"}:
|
||||
scalar_values[key] = _parse_string_list(value, path=path, line_no=line_no)
|
||||
else:
|
||||
scalar_values[key] = _expand_env(value)
|
||||
continue
|
||||
if key not in _PROFILE_MULTI_KEYS:
|
||||
raise ConfConfigError(path, line_no, f"unknown directive: {key}")
|
||||
if key == "override_policy":
|
||||
profile.override_policies.append(_parse_override_policy(value, path=path, line_no=line_no))
|
||||
elif key == "prepend_rule":
|
||||
profile.prepend_rules.append(_parse_inline_rule(value, path=path, line_no=line_no, key=key))
|
||||
elif key == "append_rule":
|
||||
profile.append_rules.append(_parse_inline_rule(value, path=path, line_no=line_no, key=key))
|
||||
|
||||
for key, parsed_value in scalar_values.items():
|
||||
setattr(profile, key, parsed_value)
|
||||
|
||||
_validate_profile_references(profile, base_config, path=path, field_lines=field_lines)
|
||||
return profile
|
||||
|
||||
|
||||
def load_conf_profiles(directory: Path, *, base_config: ConfBaseConfig) -> dict[str, ConfProfileConfig]:
|
||||
profiles: dict[str, ConfProfileConfig] = {}
|
||||
for path in sorted(directory.glob("*.conf")):
|
||||
profile = load_conf_profile(path, base_config=base_config)
|
||||
profile_key = path.stem
|
||||
if profile_key in profiles:
|
||||
raise ConfConfigError(path, 1, f"duplicate profile file key: {profile_key}")
|
||||
profiles[profile_key] = profile
|
||||
return profiles
|
||||
|
||||
Reference in New Issue
Block a user