通用规则树引擎解决方案

2024-09-19  本文已影响0人  StonyBlue

背景

因业务需求要做一个条件可视化组件, 这个组件我设计了草图(图1)需要支持逻辑运算(OR, AND), 关系运算(大于,小于,等于,包含,区间等).
截屏2024-09-20 13.55.28.png 截屏2024-09-20 13.55.45.png

规则树组件

简单调研了下蚂蚁的RuleTree, 当前内部已有的RuleView. 根据当前的需求已经现有工程的依赖, 还有部分定制功能来衡量选择.
组件 逻辑AND 逻辑OR 右值跟随左值类型变化 关系运算 功能完整 antd4支持 定制难度
RuleTree 中等
RuleView 简单

简单

从定制难度上来说RuleView简单容易上手, 而且内部有多个场景在使用, RuleTree很老都是使用的是antd3, 和现有的技术栈也不搭.

改造后的RuleView, 支持逻辑切换OR, AND

import React, { useEffect } from 'react';
import classnames from 'classnames';

const OPERATOR_AND = 'AND';
const OPERATOR_OR = 'OR';
const OPERATORS = [OPERATOR_AND, OPERATOR_OR];

export const RuleView = ({
  value,
  onChange,
  children,
  isSubTree,
}: {
  value?: string;
  // defaultValue: 'AND' | 'OR';
  onChange?: (v: string) => void;
  children: React.ReactNode;
  isSubTree?: boolean;
}) => {
  const [op, setOp] = React.useState(value || 'OR');
  useEffect(() => {
    if (value) {
      setOp(value);
    }
  }, [value]);
  const toggleOperator = () => {
    const nextIndex = (OPERATORS.indexOf(op) + 1) % OPERATORS.length;
    const newOp = OPERATORS[nextIndex];
    // setOp(newOp);
    onChange?.(newOp);
  };
  return (
    <div className="flex items-center w-full">
      <div
        className={classnames(
          'relative after:content-[""] after:block after:absolute after:w-7 after:top-1/2 after:bottom-0 after:border-t-2 after:left-full',
          { 'pl-7': isSubTree },
        )}
      >
        <div
          onDoubleClick={toggleOperator}
          className={classnames(
            'flex pl-2 pr-2 items-center rounded text-white h-8',
            {
              'bg-sky-400': op === OPERATOR_AND,
              'bg-green-400': op === OPERATOR_OR,
            },
          )}
        >
          {op}
        </div>
      </div>
      <div className="ml-7 flex">
        <div>{children}</div>
      </div>
    </div>
  );
};
export const RuleViewItem = ({
  children,
  isFirst,
  isLast,
}: {
  children: React.ReactNode;
  isFirst?: boolean;
  isLast?: boolean;
}) => {
  return (
    <div
      className={classnames(
        'relative before:block before:content-[""] before:absolute before:w-0.5 before:border-l-2',
        {
          'before:top-1/2 before:bottom-0': !isLast && isFirst,
          'before:top-0 before:bottom-1/2': !isFirst && isLast,
          'before:top-0 before:bottom-0': !isFirst && !isLast,
        },
        'after:block after:content-[""] after:absolute after:top-1/2 after:bottom-0 after:w-7 after:border-t-2',
      )}
    >
      {children}
    </div>
  );
};

样式依赖Tailwind CSS

npm install tailwindcss postcss autoprefixer css-loader style-loader --save-dev
npx tailwindcss init -p

这样会得到一个tailwind.config.js, postcss.config.js文件
检查文件postcss.config.js内容:
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
在全局index.css中添加
@tailwind base;
@tailwind components;
@tailwind utilities;

规则树的数据结构定义

export interface ConditionValue {
  operation: string;
  children?: ConditionValue[];
  left?: DataValue;
  right?: DataValue;
}
export interface DataValue {
  kind?: string;
  type?: string;
  value?: any;
  format?: string;
}

规则组件定义

const ConditionView = ({disabled}:{disabled?:boolean}) => {
    return <div className="flex justify-center overflow-x-auto">
        <Form.Item label="条件" name={['condition','operation']} noStyle>
            <RuleView >
                <Form.List
                    name={['condition','children']}
                    rules={[
                        {
                            validator: (r, v) => {
                                console.log('root condition >>>> ', r, v);
                                if (!v || !v?.length || v?.length === 0) {
                                    return Promise.reject(new Error('条件组不能为空'));
                                }
                                const errors = v.map((child: ConditionValue) =>
                                    validatorRuleTreeItem(child),
                                );
                                return Promise.all(errors)
                                    .then(() => Promise.resolve()) // 如果所有子项验证通过
                                    .catch(err => Promise.reject(err)); // 如果有任何一个子项验证失败
                            },
                        },
                    ]}
                >
                    {(fields, {add, remove}, {errors}) => (
                        <>
                            {fields.map((field, index) => {
                                return (<div key={field.key}>
                                        <Form.Item label="节点" name={[field.name]} noStyle>
                                            <ConditionNodeView
                                                key={field.key}
                                                field={field}
                                                fields={fields}
                                                add={add}
                                                remove={remove}
                                                index={index}
                                            />
                                        </Form.Item>
                                    </div>
                                );
                            })}
                            <RuleViewItem isLast>
                                <div className="h-10 relative flex items-center pl-7">
                                    <Tooltip title="添加一组条件">
                                        <Button
                                            icon={<PlusOutlined />}
                                            disabled={disabled}
                                            onClick={() => add(initialFieldValue)}
                                        />
                                    </Tooltip>
                                </div>
                            </RuleViewItem>
                            <Form.ErrorList errors={errors} />
                        </>
                    )}
                </Form.List>
            </RuleView>
        </Form.Item>
    </div>;
};

export const ConditionNodeView = ({
                               value,
                               onChange,
                               field,
                               fields,
                               add,
                               remove,
                               index,
                               disabled,
                           }: {
    value?: ConditionValue;
    onChange?: (v: ConditionValue) => void;
    field: FormListFieldData;
    fields: FormListFieldData[];
    add: (defaultValue?: any, insertIndex?: number | undefined) => void;
    remove: (index: number | number[]) => void;
    index: number;
    disabled?: boolean;
}) => {
    // console.log('ConditionNodeView >>>> ', value);
    if(value?.operation === 'OR' || value?.operation === 'AND') {
        return (
            <>
                <RuleViewItem key={field.key} isFirst={index === 0}>
                    <Form.Item label="条件" name={[field.name, 'operation']} noStyle>
                        <RuleView isSubTree>
                            <Form.List name={[field.name, 'children']}>
                                {(subFields, { add: addSubField, remove: removeSubField }, {errors:subErrors}) =>
                                    <>
                                        {subFields.map((subField, subIndex) => {
                                            return (<div key={subField.key}>
                                                    <Form.Item label="节点" name={[subField.name]} noStyle>
                                                        <ConditionNodeView
                                                            key={subField.key}
                                                            field={subField}
                                                            fields={subFields}
                                                            add={addSubField}
                                                            remove={removeSubField}
                                                            index={subIndex}
                                                        />
                                                    </Form.Item>
                                                </div>
                                            );
                                        })}
                                        <RuleViewItem isLast>
                                            <div className="h-10 relative flex items-center pl-7">
                                                <Tooltip title="添加一组条件">
                                                    <Button
                                                        type="dashed"
                                                        icon={<PlusOutlined />}
                                                        disabled={disabled}
                                                        onClick={() => addSubField(initialFieldValue)}
                                                    />
                                                </Tooltip>
                                                <Popconfirm
                                                    title="确认删除该组条件?"
                                                    disabled={fields?.length <= 1 || disabled}
                                                    onConfirm={() => { remove(field.name); }}
                                                >
                                                    <Tooltip
                                                        title={
                                                            fields?.length !== 1 ? '删除该组条件' : undefined
                                                        }
                                                    >
                                                        <Button
                                                            type="dashed"
                                                            icon={
                                                                <DeleteOutlined
                                                                    style={{
                                                                        color: fields?.length <= 1 || disabled ? 'gray' : '#f50',
                                                                    }}
                                                                />
                                                            }
                                                            disabled={fields.length <= 1 || disabled}
                                                        />
                                                    </Tooltip>
                                                </Popconfirm>
                                                <Divider type="vertical" />
                                                <Tooltip title="添加一个条件">
                                                    <Button shape="circle"
                                                            type="dashed"
                                                            icon={<PlusOutlined />}
                                                            style={{
                                                                color: !disabled ? '#f50' : 'gray',
                                                            }}
                                                            onClick={() => !disabled && addSubField(initialSubFieldValue)}/>
                                                </Tooltip>
                                            </div>
                                        </RuleViewItem>
                                        <Form.ErrorList errors={subErrors} />
                                    </>
                                }
                            </Form.List>
                        </RuleView>
                    </Form.Item>
                </RuleViewItem>
            </>
        );
    }
    return (
        <>
            <RuleViewItem key={field.key} isFirst={index === 0}>
                <div className="flex items-center ml-7 pb-1">
                    <div className="flex pl-3 pr-3 pt-3 bg-slate-100">
                        <Form.Item
                            key={field.key}
                            name={[field.name]}
                            validateFirst
                            rules={[
                                { required: true },
                                {
                                    validator: (r, v) => {
                                        console.log('RuleViewItem condition >>>> ', r, v);
                                        if (!v) {
                                            return Promise.reject(new Error('条件不能为空'));
                                        }
                                        return validatorRuleTreeItem(v);
                                    },
                                },
                            ]}
                        >
                            <RuleTreeItem
                                disabled={disabled} />
                        </Form.Item>
                        <div className="mt-1.5 ml-3" data-desc="添加单个条件">
                            <CopyOutlined
                                style={{
                                    color: !disabled ? '#f50' : 'gray',
                                }}
                                onClick={() => !disabled && add(value || initialSubFieldValue)}
                            />
                        </div>
                        <div className="mt-1.5 ml-3" data-desc="删除单个条件">
                            <DeleteOutlined
                                style={{
                                    color: fields.length !== 1 && !disabled ? '#f50' : 'gray',
                                }}
                                onClick={() => fields.length !== 1 && !disabled && remove(field.name)}
                            />
                        </div>
                    </div>
                </div>
            </RuleViewItem>
        </>
    );
};
// 初始字段值
export const initialFieldValue = {
    operation: 'AND',
    children: [
        {
            operation: OPERATOR.EQ,
            left: { kind: 'REF', type: 'STRING', operation: '' },
            right: { kind: 'CONST', type: 'STRING', operation: '' },
        },
    ],
};
export const initialSubFieldValue = {
    operation: OPERATOR.EQ,
    left: { kind: 'REF', type: 'STRING', operation: '' },
    right: { kind: 'CONST', type: 'STRING', operation: '' },
}

规则树左右值选中组件

interface RuleTreeItemProps {
    value?: ConditionValue;
    onChange?: (v?: ConditionValue) => void;
    disabled?: boolean;
    mapping?: ModelMapping[];
}

export const RuleTreeItem: React.FC<RuleTreeItemProps> = ({
                                                                     value,
                                                                     onChange,
                                                                     disabled = false,
                                                                 }) => {
    const modelMappingContext = useContext(ModelMappingContext);

    return <>
        <RuleTreeItemWarp value={value} onChange={onChange} mapping={modelMappingContext?.mapping} disabled={disabled}/>
    </>;
}

// 自定义 组件
const RuleTreeItemWarp: React.FC<RuleTreeItemProps> = ({
                                                       value,
                                                       onChange,
                                                       disabled = false,
                                                       mapping,
                                                   }) => {
 
    const handleLeftChange = useCallback(
        (v?: string) => {
            const map = mapping?.find(it => it.identifier === v);
            const v1 = {
                operation: value?.operation || '',
                left: {
                    value: v,
                    type: map?.type,
                    kind: map?.kind,
                    format: map?.format,
                },
                right: {
                    value: null,
                    type: value?.right?.type,
                    kind: value?.right?.kind,
                    format: value?.right?.format,
                },
            };
            onChange && onChange(v1);
        },
        [value],
    );

    const handleOperatorChange = useCallback(
        (v?: string) => {
            const v1 = {
                operation: v || '',
                left: value?.left,
                right: {
                    value: null,
                    type: value?.right?.type,
                    kind: value?.right?.kind,
                    format: value?.right?.format,
                },
            };
            onChange && onChange(v1);
        },
        [value],
    );

    const handleRightKindChange = useCallback(
        (v?: string) => {
            const v1 = {
                operation: value?.operation || '',
                left: value?.left,
                right: {
                    value: null,
                    type: value?.right?.type,
                    kind: v,
                    format: value?.right?.format,
                },
            };

            onChange && onChange(v1);
        },
        [value],
    );

    const handleRightSelectChange = useCallback(
        (v?: string) => {
            const map = mapping?.find(it => it.identifier === v);
            const v1 = {
                operation: value?.operation || '',
                left: value?.left,
                right: {
                    value: v,
                    type: map?.type,
                    kind: value?.right?.kind,
                    format: map?.format,
                },
            };
            onChange && onChange(v1);
        },
        [value],
    );

    const handleRightChange = useCallback(
        (v?: string) => {
            const v1 = {
                operation: value?.operation || '',
                left: value?.left,
                right: {
                    value: v,
                    type: value?.left?.type,
                    kind: value?.right?.kind,
                    format: value?.left?.format,
                },
            };
            onChange && onChange(v1);
        },
        [value],
    );

    const mappingOptions = useMemo(() => {
        if (!mapping) return [];
        const ops =
            (mapping
                ?.filter(it => {
                    if (it.invisible) {
                        return !it.invisible;
                    }
                    return true;
                })
                .map(it => ({
                    label: it.name,
                    value: it.identifier,
                })) as Option[]) ?? [];
        // console.log('mappingOptions', ops);
        return ops;
    }, [value]);

    return (
        <>
            <Space>
                <Select
                    showSearch
                    filterOption={(input, option) =>
                        (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
                        (option?.value ?? '').toString().toLowerCase().includes(input.toLowerCase())
                    }
                    placeholder="模型数据"
                    defaultValue={value?.left?.value}
                    options={mappingOptions}
                    onChange={handleLeftChange}
                    style={{ minWidth: '120px' }}
                    disabled={disabled}
                />
                <Select
                    value={value?.operation}
                    style={{ minWidth: '120px' }}
                    onChange={handleOperatorChange}
                    options={
                        value?.left?.type === DATA_TYPE.BOOLEAN
                            ? [OPERATOR.EQ].map(it => ({
                            label: OPERATOR_MAP.get(it)?.label ?? it,
                            value: it,
                        })) ?? []
                            : [
                            OPERATOR.EQ,
                            OPERATOR.NEQ,
                            OPERATOR.GT,
                            OPERATOR.GTE,
                            OPERATOR.LT,
                            OPERATOR.LTE,
                            OPERATOR.BETWEEN,
                            OPERATOR.RANGE,
                            OPERATOR.IN,
                            OPERATOR.NOT_IN,
                        ].map(it => ({
                            label: OPERATOR_MAP.get(it)?.label ?? it,
                            value: it,
                        })) ?? []
                    }
                />

                <Select
                    placeholder="种类"
                    value={value?.right?.kind || DATA_KIND.CONST}
                    options={[
                        { label: '引用', value: DATA_KIND.REF },
                        { label: '输入', value: DATA_KIND.CONST },
                    ]}
                    onChange={handleRightKindChange}
                    style={{ width: '100px' }}
                    disabled={disabled}
                />
                {DATA_KIND.REF === value?.right?.kind ? (
                    <Select
                        showSearch
                        filterOption={(input, option) =>
                            (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
                            (option?.value ?? '')
                                .toString()
                                .toLowerCase()
                                .includes(input.toLowerCase())
                        }
                        placeholder="模型数据"
                        defaultValue={value?.right?.value}
                        options={mappingOptions}
                        onChange={handleRightSelectChange}
                        style={{ minWidth: '120px' }}
                        disabled={disabled}
                    />
                ) : (
                    <RuleTreeItemRight
                        value={value}
                        onChange={handleRightChange}
                        disabled={disabled}
                    />
                )}
            </Space>
        </>
    );
};

const RuleTreeItemRight = ({
                               value,
                               onChange,
                               disabled,
                           }: {
    disabled?: boolean;
    value?: ConditionValue;
    onChange?: (v?: any) => void;
}): React.ReactElement => {
    
    if (
        value?.left?.type === DATA_TYPE.DATE ||
        value?.left?.type === DATA_TYPE.SECOND ||
        value?.left?.type === DATA_TYPE.MILLIS
    ) {
        if (
            value?.operation === OPERATOR.RANGE ||
            value?.operation === OPERATOR.BETWEEN
        ) {
            return (
                <DateTimeRangePicker
                    value={value?.right?.value}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        } else if (
            value?.operation === OPERATOR.IN ||
            value?.operation === OPERATOR.NOT_IN
        ) {
            return (
                <DateTimeMultiPicker
                    value={value?.right?.value}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        } else {
            return (
                <DateTimePicker
                    value={value?.right?.value}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        }
    } else if (value?.left?.type === DATA_TYPE.NUMBER) {
        if (
            value?.operation === OPERATOR.RANGE ||
            value?.operation === OPERATOR.BETWEEN
        ) {
            return (
                <NumberRangePicker
                    value={value?.right?.value}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        } else if (
            value?.operation === OPERATOR.IN ||
            value?.operation === OPERATOR.NOT_IN
        ) {
            return (
                <Select
                    mode="tags"
                    allowClear
                    style={{ minWidth: '280px' }}
                    placeholder="Please select"
                    defaultValue={value?.right?.value || []}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        } else {
            return (
                <InputNumber
                    style={{ minWidth: '240px' }}
                    defaultValue={value?.right?.value}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        }
    } else if (value?.left?.type === DATA_TYPE.BOOLEAN) {
        return (
            <Select
                allowClear
                style={{ minWidth: '160px' }}
                placeholder="Please select true or false"
                defaultValue={value?.right?.value}
                options={[
                    { label: '真', value: true },
                    { label: '假', value: false },
                ]}
                onChange={v => {
                    onChange?.(v);
                }}
            />
        );
    }
    if (
        value?.operation === OPERATOR.IN ||
        value?.operation === OPERATOR.NOT_IN
    ) {
        return (
            <Select
                mode="tags"
                allowClear
                style={{ minWidth: '240px' }}
                placeholder="Please select"
                defaultValue={value?.right?.value || []}
                onChange={v => {
                    onChange?.(v);
                }}
            />
        );
    }
    return (
        <Input
            style={{ minWidth: '160px' }}
            disabled={disabled}
            defaultValue={value?.right?.value}
            onChange={e => {
                onChange?.(e.target.value);
            }}
        />
    );
};

export const validatorRuleTreeItem = (v: ConditionValue): Promise<any> => {
    if (v.operation === 'OR' || v.operation === 'AND') {
        if (!v.children || v.children.length === 0) {
            return Promise.reject(new Error('条件不能为空'));
        }
        const errors = v.children.map(child => validatorRuleTreeItem(child));
        return Promise.all(errors)
            .then(() => Promise.resolve()) // 如果所有子项验证通过
            .catch(err => Promise.reject(err)); // 如果有任何一个子项验证失败
    } else {
        if (!v.left || !v.right) {
            return Promise.reject(new Error('条件不能为空'));
        }
        if (!v.left.value || v.left.value === '') {
            return Promise.reject(new Error('条件左值不能为空'));
        }
        if (!v.right.value || v.right.value === '') {
            if (DATA_TYPE.BOOLEAN === v.right?.type) {
                if (v.right?.value === null || undefined === v.right?.value) {
                    console.log('条件右值不能为空:', v);
                    return Promise.reject(new Error(`条件右值不能为空`));
                } else {
                    return Promise.resolve();
                }
            }
            console.log('条件右值不能为空:', v);
            return Promise.reject(new Error(`条件右值不能为空`));
        }
    }
    return Promise.resolve();
};

规则树效果预览

截屏2024-09-20 14.47.54.png

规则树数据预览

{
    "operation": "OR",
    "children": [
        {
            "children": [
                {
                    "left": {
                        "type": "NUMBER",
                        "value": "orderStatus",
                        "kind": "REF"
                    },
                    "operation": "EQ",
                    "right": {
                        "kind": "CONST",
                        "type": "NUMBER",
                        "value": 5300
                    }
                },
                {
                    "children": [
                        {
                            "children": [
                                {
                                    "left": {
                                        "value": "bizType",
                                        "kind": "REF",
                                        "type": "NUMBER"
                                    },
                                    "operation": "EQ",
                                    "right": {
                                        "kind": "CONST",
                                        "type": "NUMBER",
                                        "value": 24
                                    }
                                },
                                {
                                    "right": {
                                        "kind": "CONST",
                                        "type": "STRING",
                                        "value": [
                                            "24001",
                                            "2486"
                                        ]
                                    },
                                    "left": {
                                        "type": "STRING",
                                        "value": "cpCode",
                                        "kind": "REF"
                                    },
                                    "operation": "IN"
                                }
                            ],
                            "operation": "AND"
                        },
                        {
                            "children": [
                                {
                                    "left": {
                                        "type": "NUMBER",
                                        "value": "bizType",
                                        "kind": "REF"
                                    },
                                    "operation": "EQ",
                                    "right": {
                                        "kind": "CONST",
                                        "type": "NUMBER",
                                        "value": 56
                                    }
                                },
                                {
                                    "left": {
                                        "type": "STRING",
                                        "value": "cpCode",
                                        "kind": "REF"
                                    },
                                    "operation": "IN",
                                    "right": {
                                        "kind": "CONST",
                                        "type": "STRING",
                                        "value": [
                                            "5644",
                                            "5625"
                                        ]
                                    }
                                }
                            ],
                            "operation": "AND"
                        }
                    ],
                    "operation": "OR"
                },
                {
                    "left": {
                        "format": "yyyy-MM-dd HH:mm:ss",
                        "kind": "REF",
                        "type": "MILLIS",
                        "value": "expireTime"
                    },
                    "operation": "BETWEEN",
                    "right": {
                        "type": "MILLIS",
                        "value": [
                            "2024-09-01 00:00:00",
                            "2024-10-31 00:00:00"
                        ],
                        "format": "yyyy-MM-dd HH:mm:ss",
                        "kind": "CONST"
                    }
                },
                {
                    "left": {
                        "kind": "REF",
                        "type": "NUMBER",
                        "value": "orderStatus"
                    },
                    "operation": "NEQ",
                    "right": {
                        "kind": "REF",
                        "type": "NUMBER",
                        "value": "beforeOrderStatus"
                    }
                }
            ],
            "operation": "AND"
        }
    ]
}

规则引擎

规则树转换
规则树需要将数据转换成, 扁平的逻辑表达式, 例如:

(cpCode in [5644, 5625] and bizType == 56) or (cpCode in [2433, 2401] and bizType == 24)

定义实体:

使用递归将规则树转换成逻辑表达式

public class ConditionValue  {
    private String operation;
    private List<ConditionValue> children;
    // 左右值, 比较类型, 非OR,AND时候有值
    private DataValue left;
    private DataValue right;
     public String expression() {
        if (Operation.isLogical(operation) || operation == null) {
            return children.stream().map(it -> it.expression()).collect(Collectors.joining(Operation.ofSymbol(operation), " ( ", " ) "));
        }
        if(Operation.NOT_IN.name().equals(operation)) {
            return " (!(" + left.getValue() +  " in " + right.getValue() + ")) ";
        } else if (Operation.IN.name().equals(operation)) {
            return " (" + left.getValue() +  " in " + right.getValue() + ") ";
        } else if (Operation.RANGE.name().equals(operation)) {
            return " (" + left.getValue() + " >= " + right.getValue() + "[0] && " + left.getValue() + " <= " + right.getValue() + "[1]" + ") ";
        } else if (Operation.BETWEEN.name().equals(operation)) {
            return " (" + left.getValue() + " > " + right.getValue() + "[0] && " + left.getValue() + " < " + right.getValue() + "[1]" + ") ";
        }
        return " (" + left.getValue() + " " + Operation.ofSymbol(operation) + " " + right.getValue() + ") ";
    }
}

规则引擎设计

引擎使用antlr4定义一个简单脚本, 弱类型语法, 使用groovy、golang、javascript的一些常见语法组成, antlr4天然支持多语言, 表达式执行就天然支持多语言, 不受受限于语言.
a. 支持逻辑运行OR、AND
b. 支持弱类型关系比较自动类型转换, 100 == "100"
c. 支持in关系, uid in ["4042344", "512009"]
d. 使用antlr4的visitor模式实现
parser grammar 表达式 一部分定义:

expression
    :
    FUNC  LPAREN formalParameterList? RPAREN functionBody                                   # FunctionDeclExpression
    | arrowFunctionParameters ARROW arrowFunctionBody                                       # ArrowFunctionDeclExpression
    | member=expression LBRACK min=expression? COLON max=expression? RBRACK                 # MemberSubsetExpression
    | member=expression opt=(OPTIONAL_CHAINING | QUESTION)? LBRACK index=expression RBRACK  # MemberIndexExpression
    | member=expression opt=QUESTION? DOT method=identifier                                 # MemberCallExpression
    | expression   argumentList                                                             # ArgumentsExpression
    | expression op=(INC | DEC)                                                             # PostIncrementExpression //后置递增运算符
    | op=(INC | DEC) expression                                                             # PreIncrementExpression  //前置递增运算符
    | op=(ADD | SUB) expression                                                             # UnaryExpression
    | op=(TILDE | BANG) expression                                                          # NotExpression
    | lhs=expression op=(MUL | DIV | MOD) rhs=expression                                    # MultiplicativeExpression
    | lhs=expression op=(ADD | SUB) rhs=expression                                          # AdditiveExpression
    | lhs=expression op=(LE | GE | GT | LT | EQUAL | NOTEQUAL | IN) rhs=expression          # RelationalExpression
    | lhs=expression op=(AND | OR) rhs=expression                                           # LogicalExpression
    | cond=expression '?' lhs=expression ':' rhs=expression                                 # TernaryExpression
    | lhs=expression '?:' rhs=expression                                                    # ElvisExpression
    | <assoc = right> lhs=expression '=' rhs=expression                                     # AssignmentExpression
    | identifier                                                                            # IdentifierExpression
    | literal                                                                               # LiteralExpression
    | arrayLiteral                                                                          # ArrayLiteralExpression
    | objectLiteral                                                                         # ObjectLiteralExpression
    | LPAREN expression RPAREN                                                              # ParenthesizedExpression
    ;

关系运算实现

func (this *MainVisitor) VisitRelationalExpression(ctx *RelationalExpressionContext) interface{} {
    logrus.Debugln("VisitRelationalExpression:", ctx.GetText())
    left, right := this.Visit(ctx.GetLhs()), this.Visit(ctx.GetRhs())
    //检查
    left = ExpressionCallIfNecessary(left, []interface{}{})
    right = ExpressionCallIfNecessary(right, []interface{}{})
    logrus.Debugln("VisitRelationalExpression ctx.GetOp(): ", ctx.GetOp().GetText())

    switch ctx.GetOp().GetTokenType() {
    case FlyParserEQUAL:
        return func_eq(left, right)
    case FlyParserNOTEQUAL:
        switch left.(type) {
        case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
            switch left.(type) {
            case float64, float32:
                return cast.ToFloat64(left) != cast.ToFloat64(right)
            }
            return cast.ToInt64(left) != cast.ToInt64(right)
        case float64, float32:
            return cast.ToFloat64(left) != cast.ToFloat64(right)
        }
        return left != right
    case FlyParserGT:
        switch left.(type) {
        case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
            switch left.(type) {
            case float64, float32:
                return cast.ToFloat64(left) > cast.ToFloat64(right)
            }
            return cast.ToInt64(left) > cast.ToInt64(right)
        case float64, float32:
            return cast.ToFloat64(left) > cast.ToFloat64(right)
        }
        panic("not support Relational left type")
    case FlyParserGE:
        switch left.(type) {
        case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
            switch left.(type) {
            case float64, float32:
                return cast.ToFloat64(left) >= cast.ToFloat64(right)
            }
            return cast.ToInt64(left) >= cast.ToInt64(right)
        case float64, float32:
            return cast.ToFloat64(left) >= cast.ToFloat64(right)
        }
        panic("not support Relational left type")
    case FlyParserLT:
        switch left.(type) {
        case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
            switch left.(type) {
            case float64, float32:
                return cast.ToFloat64(left) < cast.ToFloat64(right)
            }
            return cast.ToInt64(left) < cast.ToInt64(right)
        case float64, float32:
            return cast.ToFloat64(left) < cast.ToFloat64(right)
        }
        panic("not support Relational left type")
    case FlyParserLE:
        switch left.(type) {
        case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
            switch left.(type) {
            case float64, float32:
                return cast.ToFloat64(left) <= cast.ToFloat64(right)
            }
            return cast.ToInt64(left) <= cast.ToInt64(right)
        case float64, float32:
            return cast.ToFloat64(left) <= cast.ToFloat64(right)
        }
        panic("not support Relational left type")
    case FlyParserIN:
        if isNil(right) {
            return false
        }
        list, ok := right.([]interface{})
        if !ok {
            panic("not support Relational right type")
        }
        for _, v := range list {
            if func_eq(left, v) {
                return true
            }
        }
        return false
    }
    panic("not support Relational expression")
}
func func_eq(left interface{}, right interface{}) bool {
    switch left.(type) {
    case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
        switch left.(type) {
        case float64, float32:
            return cast.ToFloat64(left) == cast.ToFloat64(right)
        }
        return cast.ToInt64(left) == cast.ToInt64(right)
    case float64, float32:
        return cast.ToFloat64(left) == cast.ToFloat64(right)
    case string:
        if rightStr, ok := right.(string); ok {
            return left.(string) == rightStr
        }
        return left.(string) == cast.ToString(right)
    }
    return left == right
}

规则表达式执行测试

func TestExpression(t *testing.T) {
    type tt struct {
        expression string
        value      interface{}
        recover    bool
        scope      map[string]interface{}
    }
    caseList := [...]tt{
        {
            scope: map[string]interface{}{
                "orderEnter":  1693875200,
                "orderTime":   1693875200,
                "orderStatus": 5300,
                "uid":         40488888,
            },
            expression: "( ( (orderEnter >= orderTime)&&(orderStatus == 5300)&&(orderStatus in [5300,5400]) )||( (orderEnter < orderTime)&&( (orderEnter > orderTime) ) ) && uid in [\"40488888\", \"2075114\"] )",
            value:      true,
        },
        {
            scope: map[string]interface{}{},
            expression: "var x = 6;" +
                "y,z := 2,4;" +
                "x == y+z",
            value: true,
        },
        {
            scope: map[string]interface{}{},
            expression: "var x = [6,7,8];" +
                "y := \"7\";" +
                "y in x",
            value: true,
        },
    }
    checkCase := func(t *testing.T, c tt) (ex error) {
        defer func() {
            if r := recover(); r != nil {
                ex = fmt.Errorf("panic: %v", r)
            }
        }()
        val := calc(c.expression, c.scope)
        if val != nil && reflect.TypeOf(val).Kind() == reflect.Map {
            str, err := jsoniter.MarshalToString(val)
            fmt.Println("result str: ", str)
            assert.Nil(t, err)
            assert.JSONEq(t, c.value.(string), str)
            return
        }
        assert.Equal(t, val, c.value)
        return ex
    }
    for _, c := range caseList {
        err := checkCase(t, c)
        if c.recover {
            fmt.Println(err)
            assert.NotNil(t, err)
        } else {
            assert.Nil(t, err)
        }
    }
}

func calc(expression string, scope map[string]interface{}) interface{} {
    input := antlr.NewInputStream(expression)
    lexer := NewFlyLexer(input)

    stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)

    parser := NewFlyParser(stream)
    parser.BuildParseTrees = true
    parser.RemoveErrorListeners()
    parser.AddErrorListener(&VerboseListener{})
    tree := parser.Program()

    visitor := NewMainVisitor(nil, scope)

    var result = visitor.Execute(tree)
    return result
}
上一篇下一篇

猜你喜欢

热点阅读