import { isNumber, isPlainObject, isString } from "es-toolkit"; import type { ConfigValidationIssue } from "../../schema/issues"; import type { CheckerValidationInput } from "../types"; import { normalizeExpectMatchers } from "../../expect/normalize"; import { validateValueMatcher } from "../../expect/validate-matcher"; import { issue, joinPath } from "../../schema/issues"; export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; const defaults = input.defaults["icmp"]; if (defaults !== undefined && defaults !== null) { const targetName = "defaults.icmp"; if (!isPlainObject(defaults)) { issues.push(issue("invalid-type", "defaults.icmp", "必须为对象", targetName)); } else { const icmpDefaults = defaults as Record; for (const key of Object.keys(icmpDefaults)) { issues.push(issue("unknown-field", joinPath("defaults.icmp", key), "是未知字段", targetName)); } } } for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; if (!isPlainObject(target)) continue; const targetRecord = target as Record; if (targetRecord["type"] !== "icmp") continue; issues.push(...validatePingTarget(targetRecord, `targets[${i}]`)); } return issues; } function getTargetName(target: Record): string | undefined { if (isString(target["name"])) return target["name"]; return isString(target["id"]) ? target["id"] : undefined; } function validatePingExpect(target: Record, path: string): ConfigValidationIssue[] { const rawExpect = target["expect"]; if (rawExpect === undefined || rawExpect === null || !isPlainObject(rawExpect)) return []; const expect = rawExpect as Record; const issues: ConfigValidationIssue[] = []; const targetName = getTargetName(target); const expectPath = joinPath(path, "expect"); normalizeExpectMatchers(expect, ["packetLossPercent", "avgLatencyMs", "maxLatencyMs", "durationMs"]); if (expect["alive"] !== undefined && typeof expect["alive"] !== "boolean") { issues.push(issue("invalid-type", joinPath(expectPath, "alive"), "必须为布尔值", targetName)); } for (const key of ["packetLossPercent", "avgLatencyMs", "maxLatencyMs", "durationMs"]) { if (expect[key] !== undefined) { issues.push(...validateValueMatcher(expect[key], joinPath(expectPath, key), targetName)); } } const allowedKeys = new Set(["alive", "avgLatencyMs", "durationMs", "maxLatencyMs", "packetLossPercent"]); for (const key of Object.keys(expect)) { if (!allowedKeys.has(key)) { issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName)); } } return issues; } function validatePingTarget(target: Record, path: string): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; const targetName = getTargetName(target); const rawIcmp = target["icmp"]; if (!isPlainObject(rawIcmp)) { issues.push(issue("required", joinPath(path, "icmp"), "缺少 icmp 配置分组", targetName)); issues.push(...validatePingExpect(target, path)); return issues; } const icmp = rawIcmp as Record; if (!isString(icmp["host"]) || icmp["host"].trim() === "") { issues.push(issue("required", joinPath(joinPath(path, "icmp"), "host"), "缺少 icmp.host 字段", targetName)); } if (icmp["count"] !== undefined) { const count = icmp["count"]; if (!isNumber(count) || !Number.isInteger(count) || count < 1 || count > 100) { issues.push( issue("invalid-value", joinPath(joinPath(path, "icmp"), "count"), "必须为 1-100 的正整数", targetName), ); } } if (icmp["packetSize"] !== undefined) { const packetSize = icmp["packetSize"]; if (!isNumber(packetSize) || !Number.isInteger(packetSize) || packetSize < 1 || packetSize > 65500) { issues.push( issue("invalid-value", joinPath(joinPath(path, "icmp"), "packetSize"), "必须为 1-65500 的正整数", targetName), ); } } const allowedIcmpKeys = new Set(["count", "host", "packetSize"]); for (const key of Object.keys(icmp)) { if (!allowedIcmpKeys.has(key)) { issues.push(issue("unknown-field", joinPath(joinPath(path, "icmp"), key), "是未知字段", targetName)); } } issues.push(...validatePingExpect(target, path)); return issues; }