mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
243 lines
6.8 KiB
TypeScript
243 lines
6.8 KiB
TypeScript
import type { BodyRuleCondition, BodyRuleConditionOp } from '@/api/endpoints'
|
|
|
|
export type ConditionSource = 'body' | 'original' | 'request_headers'
|
|
export type ConditionGroupMode = 'all' | 'any'
|
|
|
|
export interface EditableConditionLeaf {
|
|
kind: 'leaf'
|
|
path: string
|
|
op: BodyRuleConditionOp
|
|
value: string
|
|
source: ConditionSource
|
|
}
|
|
|
|
export interface EditableConditionGroup {
|
|
kind: 'group'
|
|
mode: ConditionGroupMode
|
|
children: EditableConditionNode[]
|
|
}
|
|
|
|
export type EditableConditionNode = EditableConditionLeaf | EditableConditionGroup
|
|
|
|
export const CONDITION_OP_OPTIONS: Array<{ value: BodyRuleConditionOp; label: string }> = [
|
|
{ value: 'eq', label: '等于' },
|
|
{ value: 'neq', label: '不等于' },
|
|
{ value: 'gt', label: '大于' },
|
|
{ value: 'lt', label: '小于' },
|
|
{ value: 'gte', label: '大于等于' },
|
|
{ value: 'lte', label: '小于等于' },
|
|
{ value: 'starts_with', label: '开头匹配' },
|
|
{ value: 'ends_with', label: '结尾匹配' },
|
|
{ value: 'contains', label: '包含' },
|
|
{ value: 'matches', label: '正则匹配' },
|
|
{ value: 'exists', label: '存在' },
|
|
{ value: 'not_exists', label: '不存在' },
|
|
{ value: 'in', label: '在列表中' },
|
|
{ value: 'type_is', label: '类型是' },
|
|
]
|
|
|
|
const NUMERIC_OPS = new Set(['gt', 'lt', 'gte', 'lte'])
|
|
const STRING_OPS = new Set(['starts_with', 'ends_with'])
|
|
const TYPE_IS_VALUES = new Set(['string', 'number', 'boolean', 'array', 'object', 'null'])
|
|
|
|
export function createEmptyConditionLeaf(): EditableConditionLeaf {
|
|
return {
|
|
kind: 'leaf',
|
|
path: '',
|
|
op: 'eq',
|
|
value: '',
|
|
source: 'body',
|
|
}
|
|
}
|
|
|
|
export function createConditionGroup(
|
|
mode: ConditionGroupMode = 'all',
|
|
children: EditableConditionNode[] = [createEmptyConditionLeaf()],
|
|
): EditableConditionGroup {
|
|
return {
|
|
kind: 'group',
|
|
mode,
|
|
children,
|
|
}
|
|
}
|
|
|
|
export function cloneEditableCondition(node: EditableConditionNode): EditableConditionNode {
|
|
if (node.kind === 'group') {
|
|
return {
|
|
kind: 'group',
|
|
mode: node.mode,
|
|
children: node.children.map(cloneEditableCondition),
|
|
}
|
|
}
|
|
return { ...node }
|
|
}
|
|
|
|
export function conditionToEditable(condition?: BodyRuleCondition | null): EditableConditionNode | null {
|
|
if (!condition) return null
|
|
if ('all' in condition) {
|
|
return createConditionGroup(
|
|
'all',
|
|
condition.all.map(child => conditionToEditable(child) || createEmptyConditionLeaf()),
|
|
)
|
|
}
|
|
if ('any' in condition) {
|
|
return createConditionGroup(
|
|
'any',
|
|
condition.any.map(child => conditionToEditable(child) || createEmptyConditionLeaf()),
|
|
)
|
|
}
|
|
const source = (condition as { source?: unknown }).source
|
|
return {
|
|
kind: 'leaf',
|
|
path: condition.path || '',
|
|
op: condition.op || 'eq',
|
|
value: condition.value !== undefined
|
|
? (typeof condition.value === 'string' ? condition.value : JSON.stringify(condition.value))
|
|
: '',
|
|
source: source === 'request_headers' || source === 'headers'
|
|
? 'request_headers'
|
|
: source === 'original'
|
|
? 'original'
|
|
: 'body',
|
|
}
|
|
}
|
|
|
|
export function editableConditionToApi(node: EditableConditionNode | null): BodyRuleCondition | undefined {
|
|
if (!node) return undefined
|
|
|
|
if (node.kind === 'group') {
|
|
const children = node.children
|
|
.map(child => editableConditionToApi(child))
|
|
.filter((child): child is BodyRuleCondition => !!child)
|
|
if (!children.length) return undefined
|
|
return node.mode === 'all' ? { all: children } : { any: children }
|
|
}
|
|
|
|
const path = node.path.trim()
|
|
if (!path) return undefined
|
|
|
|
const base = {
|
|
path,
|
|
op: node.op,
|
|
...(node.source === 'request_headers'
|
|
? { source: 'request_headers' as const }
|
|
: node.source === 'original'
|
|
? { source: 'original' as const }
|
|
: {}),
|
|
}
|
|
|
|
if (node.op === 'exists' || node.op === 'not_exists') {
|
|
return base
|
|
}
|
|
|
|
const raw = node.value.trim()
|
|
if (!raw) {
|
|
return { ...base, value: '' }
|
|
}
|
|
|
|
try {
|
|
return { ...base, value: JSON.parse(raw) }
|
|
} catch {
|
|
return { ...base, value: raw }
|
|
}
|
|
}
|
|
|
|
export function isConditionValueRequired(op: BodyRuleConditionOp): boolean {
|
|
return op !== 'exists' && op !== 'not_exists'
|
|
}
|
|
|
|
export function getConditionValuePlaceholder(op: BodyRuleConditionOp): string {
|
|
if (op === 'in') return '["a", "b"]'
|
|
if (op === 'type_is') return 'string/number/boolean/...'
|
|
return '值'
|
|
}
|
|
|
|
export function getBodyRuleConditionPathPlaceholder(path: string): string {
|
|
return path.includes('[*]') || /\[\d+-\d+\]/.test(path) ? '$item.字段名' : '字段路径'
|
|
}
|
|
|
|
export function conditionEquals(
|
|
left: EditableConditionNode | null,
|
|
right: EditableConditionNode | null,
|
|
): boolean {
|
|
if (left === right) return true
|
|
if (!left || !right) return false
|
|
if (left.kind !== right.kind) return false
|
|
|
|
if (left.kind === 'group' && right.kind === 'group') {
|
|
if (left.mode !== right.mode) return false
|
|
if (left.children.length !== right.children.length) return false
|
|
return left.children.every((child, i) => conditionEquals(child, right.children[i]))
|
|
}
|
|
|
|
if (left.kind === 'leaf' && right.kind === 'leaf') {
|
|
return left.path === right.path
|
|
&& left.op === right.op
|
|
&& left.value === right.value
|
|
&& left.source === right.source
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
export function validateEditableCondition(node: EditableConditionNode | null): string | null {
|
|
if (!node) return null
|
|
|
|
if (node.kind === 'group') {
|
|
if (!node.children.length) return '组合条件至少需要一个子条件'
|
|
for (let i = 0; i < node.children.length; i += 1) {
|
|
const err = validateEditableCondition(node.children[i])
|
|
if (err) return `子条件 ${i + 1}: ${err}`
|
|
}
|
|
return null
|
|
}
|
|
|
|
const path = node.path.trim()
|
|
if (!path) return '条件路径不能为空'
|
|
|
|
if (!isConditionValueRequired(node.op)) return null
|
|
|
|
const raw = node.value.trim()
|
|
let parsed: unknown = raw
|
|
if (raw) {
|
|
try {
|
|
parsed = JSON.parse(raw)
|
|
} catch {
|
|
parsed = raw
|
|
}
|
|
}
|
|
|
|
if (NUMERIC_OPS.has(node.op)) {
|
|
if (typeof parsed !== 'number' || Number.isNaN(parsed)) return '数值条件必须填写数字'
|
|
return null
|
|
}
|
|
|
|
if (STRING_OPS.has(node.op)) {
|
|
if (typeof parsed !== 'string') return '该条件值必须为字符串'
|
|
return null
|
|
}
|
|
|
|
if (node.op === 'matches') {
|
|
if (typeof parsed !== 'string' || !parsed) return '正则条件值不能为空'
|
|
try {
|
|
new RegExp(parsed)
|
|
return null
|
|
} catch (error: unknown) {
|
|
return `正则表达式无效:${error instanceof Error ? error.message : String(error)}`
|
|
}
|
|
}
|
|
|
|
if (node.op === 'in') {
|
|
if (!Array.isArray(parsed)) return 'in 条件值必须是 JSON 数组'
|
|
return null
|
|
}
|
|
|
|
if (node.op === 'type_is') {
|
|
if (typeof parsed !== 'string' || !TYPE_IS_VALUES.has(parsed)) {
|
|
return 'type_is 仅支持 string/number/boolean/array/object/null'
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|