將標準查詢解析為JSON子句樹

我想為我的系統構建一個查詢,外部系統可以根據條件使用該查詢進行配置。

在后端,我發現很容易有一個JSON子句樹,它將被遞歸計算。

[
  "AND",
  [
    {
      "operator": "eq",
      "field": "section1.fieldabc",
      "value": "value1"
    },
    [
      "OR",
      {
        "operator": "lt",
        "field": "section2.fieldxyz",
        "value": 5000
      },
      {
        "operator": "gt",
        "field": "section2.fieldxyz",
        "value": 1000
      }
    ]
  ]
]

或者類似的東西(上面我把它描繪成一棵s-expression樹)

問題是,我希望它作為后端的JSON子句樹,但我不希望用戶需要編寫這樣的內容。如果我能創建一個類似JQL(Jira查詢語言)之類的查詢,那就太好了。但是我不想花太多的精力為轉換的語言制作一個完整的證明解析器。

有沒有標準化的方法來實現這一點?可能是一種標準化的查詢語言,使用庫(JS或Java)進行轉換。

從最終用戶的角度來看,我希望上面的查詢是這樣的

section1.fieldabc == value1 AND (section2.fieldxyz<5000 OR section2.fieldxyz>10000)
? 最佳回答:

用TypeScript編寫了一個(相對)簡單的解析器,可以解析二進制運算符(具有正確的操作順序)和常量、處理括號、全局變量和簡單字段訪問:

const BINARY_OPERATORS: Record<string, number> = {
    // AND/OR
    'AND': 1,
    'OR': 0,
    // Equal stuff
    '==': 2,
    '!=': 2,
    '<': 2,
    '<=': 2,
    '>': 2,
    '>=': 2,
}

interface BinaryOperation {
    type: 'binop';
    operator: string;
    left: Expression;
    right: Expression;
}

interface Variable {
    type: 'variable';
    name: string;
}

interface FieldAccess {
    type: 'field';
    object: Expression;
    field: string; // could be Expression
}

interface Constant {
    type: 'constant';
    value: any;
}

interface Brackets {
    type: 'brackets';
    expr: Expression;
}

type Expression = BinaryOperation | Variable | FieldAccess | Constant | Brackets;

function parseConstant(input: string): [number, Constant?] {
    // Numbers (including floats, octals and hexadecimals)
    let match = input.match(/^\s*((?:0[xo]|\d*\.)?\d+)/);
    if (match) {
        const [{ length }, digits] = match;
        if (digits.includes('.')) {
            return [length, { type: 'constant', value: parseFloat(digits) }];
        }
        return [length, { type: 'constant', value: parseInt(digits) }];
    }
    // Strings
    match = input.match(/^(\s*)(["'])/);
    if (match) {
        const [, white, quote] = match;
        let value = '';
        let escape = false;
        for (let i = white.length; i < input.length; i++) {
            const ch = input[i];
            if (ch === '\\' && !escape) {
                escape = true;
            } else if (escape) {
                escape = false;
                value += ch;
            } else if (ch === quote) {
                return [i + 1, { type: 'constant', value }];
            } else {
                value += ch;
            }
        }
        return [white.length];
    }
    // Booleans
    match = input.match(/^\s*(true|false)/);
    if (match) {
        const [{ length }, bool] = match;
        return [length, { type: 'constant', value: bool === 'true' }];
    }
    return [0];
}

function parseVariable(input: string): [number, Variable?] {
    const match = input.match(/^\s*(\w+[\w\d]*)/);
    if (!match) return [0];
    return [match[0].length, { type: 'variable', name: match[1] }];
}

function orderBinaryOperations(expr: BinaryOperation): BinaryOperation {
    const { left, right } = expr;
    const priority = BINARY_OPERATORS[expr.operator];
    if (left.type == 'binop' && BINARY_OPERATORS[left.operator] < priority) {
        // LOP < EXP
        // (leftL LOP leftR) EXP exprR) => leftL LOP (leftR EXP exprR)
        return orderBinaryOperations({
            type: 'binop',
            operator: left.operator,
            left: left.left,
            right: {
                type: 'binop',
                operator: expr.operator,
                left: left.right,
                right: expr.right,
            },
        });
    } else if (right.type === 'binop' && BINARY_OPERATORS[right.operator] <= priority) {
        // EXP >= ROP
        // exprL EXP (rightL ROP rightR) => (exprL EXP rightL) ROP rightR
        return orderBinaryOperations({
            type: 'binop',
            operator: right.operator,
            left: {
                type: 'binop',
                operator: expr.operator,
                left: expr.left,
                right: right.left,
            },
            right: right.right,
        });
    }
    return expr;
}

function parsePostExpression(expr: [number, Expression?], input: string): [number, Expression?] {
    if (!expr[1]) return expr;
    const trimmed = input.trimLeft();
    const white = input.length - trimmed.length;
    // Binary operation
    for (const operator in BINARY_OPERATORS) {
        if (trimmed.startsWith(operator)) {
            const offset = expr[0] + white + operator.length;
            const rightResult = parseExpression(trimmed.slice(operator.length));
            if (!rightResult[1]) throw new Error(`Missing right-hand side expression for ${operator}`);
            return parsePostExpression([
                offset + rightResult[0],
                orderBinaryOperations({
                    type: 'binop',
                    operator,
                    left: expr[1],
                    right: rightResult[1],
                })
            ], trimmed.slice(rightResult[0]));
        }
    }
    // Field access
    const match = input.match(/^\.(\w+[\w\d]*)/);
    if (match) {
        const [{ length }, field] = match;
        return parsePostExpression([
            expr[0] + white + length,
            { type: 'field', object: expr[1], field }
        ], trimmed.slice(length));
    }
    return expr;
}

function parseExpression(input: string): [number, Expression?] {
    // Constants
    let result: [number, Expression?] = parseConstant(input);
    // Variables
    if (!result[1]) result = parseVariable(input);
    // Brackets
    if (!result[1]) {
        const match = input.match(/^\s*\(/);
        if (match) {
            const [{ length }] = match;
            const brackets = parseExpression(input.slice(length));
            if (brackets[1]) {
                const offset = brackets[0] + length;
                const endBracket = input.slice(offset).match(/^\s*\)/);
                if (!endBracket) throw new Error(`Missing ')' in '${input}'`);
                result = [offset + endBracket[0].length, {
                    type: 'brackets', expr: brackets[1]
                }];
            }
        }
    }
    return parsePostExpression(result, input.slice(result[0]));
}

function parse(input: string): Expression {
    const [length, expr] = parseExpression(input);
    if (length === input.length) {
        if (expr) return expr;
        throw new Error(`Unfinished expression`);
    }
    if (!expr) throw new Error(`Unexpected character at ${length}`);
    throw new Error(`Unexpected character at ${length}`);
}

const parsed = parse('(section2.fieldxyz<5000 OR section2.fieldxyz>10000) AND section1.fieldabc == value1');
console.log(JSON.stringify(parsed, null, 4));

function formatExpression(expr: Expression): string {
    if (expr.type === 'binop') {
        // Wrapping in [] so the order of operations is clearly visible
        return `[${formatExpression(expr.left)} ${expr.operator} ${formatExpression(expr.right)}]`;
    } else if (expr.type === 'brackets') {
        return `(${formatExpression(expr.expr)})`;
    } else if (expr.type === 'constant') {
        return JSON.stringify(expr.value);
    } else if (expr.type === 'field') {
        return `${formatExpression(expr.object)}.${expr.field}`;
    } else if (expr.type === 'variable') {
        return expr.name;
    }
    throw new Error(`Unexpected expression type '${(expr as any).type}'`);
}

console.log('=>', formatExpression(parsed));

const BINARY_OPERATORS = {
    // AND/OR
    'AND': 1,
    'OR': 0,
    // Equal stuff
    '==': 2,
    '!=': 2,
    '<': 2,
    '<=': 2,
    '>': 2,
    '>=': 2,
}

function parseConstant(input) {
    // Numbers (including floats, octals and hexadecimals)
    let match = input.match(/^\s*((?:0[xo]|\d*\.)?\d+)/);
    if (match) {
        const [{ length }, digits] = match;
        if (digits.includes('.')) {
            return [length, { type: 'constant', value: parseFloat(digits) }];
        }
        return [length, { type: 'constant', value: parseInt(digits) }];
    }
    // Strings
    match = input.match(/^(\s*)(["'])/);
    if (match) {
        const [, white, quote] = match;
        let value = '';
        let escape = false;
        for (let i = white.length; i < input.length; i++) {
            const ch = input[i];
            if (ch === '\\' && !escape) {
                escape = true;
            } else if (escape) {
                escape = false;
                value += ch;
            } else if (ch === quote) {
                return [i + 1, { type: 'constant', value }];
            } else {
                value += ch;
            }
        }
        return [white.length];
    }
    // Booleans
    match = input.match(/^\s*(true|false)/);
    if (match) {
        const [{ length }, bool] = match;
        return [length, { type: 'constant', value: bool === 'true' }];
    }
    return [0];
}

function parseVariable(input) {
    const match = input.match(/^\s*(\w+[\w\d]*)/);
    if (!match) return [0];
    return [match[0].length, { type: 'variable', name: match[1] }];
}

function orderBinaryOperations(expr) {
    const { left, right } = expr;
    const priority = BINARY_OPERATORS[expr.operator];
    if (left.type == 'binop' && BINARY_OPERATORS[left.operator] < priority) {
        // LOP < EXP
        // (leftL LOP leftR) EXP exprR) => leftL LOP (leftR EXP exprR)
        return orderBinaryOperations({
            type: 'binop',
            operator: left.operator,
            left: left.left,
            right: {
                type: 'binop',
                operator: expr.operator,
                left: left.right,
                right: expr.right,
            },
        });
    } else if (right.type === 'binop' && BINARY_OPERATORS[right.operator] <= priority) {
        // EXP >= ROP
        // exprL EXP (rightL ROP rightR) => (exprL EXP rightL) ROP rightR
        return orderBinaryOperations({
            type: 'binop',
            operator: right.operator,
            left: {
                type: 'binop',
                operator: expr.operator,
                left: expr.left,
                right: right.left,
            },
            right: right.right,
        });
    }
    return expr;
}

function parsePostExpression(expr, input) {
    if (!expr[1]) return expr;
    const trimmed = input.trimLeft();
    const white = input.length - trimmed.length;
    // Binary operation
    for (const operator in BINARY_OPERATORS) {
        if (trimmed.startsWith(operator)) {
            const offset = expr[0] + white + operator.length;
            const rightResult = parseExpression(trimmed.slice(operator.length));
            if (!rightResult[1]) throw new Error(`Missing right-hand side expression for ${operator}`);
            return parsePostExpression([
                offset + rightResult[0],
                orderBinaryOperations({
                    type: 'binop',
                    operator,
                    left: expr[1],
                    right: rightResult[1],
                })
            ], trimmed.slice(rightResult[0]));
        }
    }
    // Field access
    const match = input.match(/^\.(\w+[\w\d]*)/);
    if (match) {
        const [{ length }, field] = match;
        return parsePostExpression([
            expr[0] + white + length,
            { type: 'field', object: expr[1], field }
        ], trimmed.slice(length));
    }
    return expr;
}

function parseExpression(input) {
    // Constants
    let result = parseConstant(input);
    // Variables
    if (!result[1]) result = parseVariable(input);
    // Brackets
    if (!result[1]) {
        const match = input.match(/^\s*\(/);
        if (match) {
            const [{ length }] = match;
            const brackets = parseExpression(input.slice(length));
            if (brackets[1]) {
                const offset = brackets[0] + length;
                const endBracket = input.slice(offset).match(/^\s*\)/);
                if (!endBracket) throw new Error(`Missing ')' in '${input}'`);
                result = [offset + endBracket[0].length, {
                    type: 'brackets', expr: brackets[1]
                }];
            }
        }
    }
    return parsePostExpression(result, input.slice(result[0]));
}

function parse(input) {
    const [length, expr] = parseExpression(input);
    if (length === input.length) {
        if (expr) return expr;
        throw new Error(`Unfinished expression`);
    }
    if (!expr) throw new Error(`Unexpected character at ${length}`);
    throw new Error(`Unexpected character at ${length}`);
}

const parsed = parse('(section2.fieldxyz<5000 OR section2.fieldxyz>10000) AND section1.fieldabc == value1');
console.log(JSON.stringify(parsed, null, 4));

function formatExpression(expr) {
    if (expr.type === 'binop') {
        // Wrapping in [] so the order of operations is clearly visible
        return `[${formatExpression(expr.left)} ${expr.operator} ${formatExpression(expr.right)}]`;
    } else if (expr.type === 'brackets') {
        return `(${formatExpression(expr.expr)})`;
    } else if (expr.type === 'constant') {
        return JSON.stringify(expr.value);
    } else if (expr.type === 'field') {
        return `${formatExpression(expr.object)}.${expr.field}`;
    } else if (expr.type === 'variable') {
        return expr.name;
    }
    throw new Error(`Unexpected expression type '${expr.type}'`);
}

console.log('=>', formatExpression(parsed));

可以立即運行的JS版本的代碼段

轉換為JSON時的輸出示例:

{
    "type": "binop",
    "operator": "AND",
    "left": {
        "type": "brackets",
        "expr": {
            "type": "binop",
            "operator": "OR",
            "left": {
                "type": "binop",
                "operator": "<",
                "left": {
                    "type": "field",
                    "object": {
                        "type": "variable",
                        "name": "section2"
                    },
                    "field": "fieldxyz"
                },
                "right": {
                    "type": "constant",
                    "value": 5000
                }
            },
            "right": {
                "type": "binop",
                "operator": ">",
                "left": {
                    "type": "field",
                    "object": {
                        "type": "variable",
                        "name": "section2"
                    },
                    "field": "fieldxyz"
                },
                "right": {
                    "type": "constant",
                    "value": 10000
                }
            }
        }
    },
    "right": {
        "type": "binop",
        "operator": "==",
        "left": {
            "type": "field",
            "object": {
                "type": "variable",
                "name": "section1"
            },
            "field": "fieldabc"
        },
        "right": {
            "type": "variable",
            "name": "value1"
        }
    }
}

我一直使用帶有type字段的對象,盡管您仍然可以將binop對象轉換為['AND', expr1, expr2]之類的對象。而不是簡單地讓二進制操作總是在一個僅僅是a.b.c.etc字符串的字段上,我的更高級一些。但仍可能增加限制,至少基礎工作已經存在。

我處理這個問題是因為我喜歡寫這類東西。實際上,我建議使用Chandan的建議,即使用jQuery QueryBuilder或react-query-builder,以使它對您的用戶更加容易和友好。

如果你更傾向于使用SQL-like語法的“超級用戶”,我的代碼可能會有所幫助。不過,可能有很多更好的庫可以提供幫助,例如在報告語法錯誤或嘗試訪問non-existant變量/字段時,這些庫可能更健壯。再說一次,由于我的代碼只有大約150行(如果包括類型,則為200行),而且編寫得也不太奇怪,因此如果這更適合您的話,根據您的需要調整代碼應該不會太困難。

主站蜘蛛池模板: 久久99国产精品一区二区| 国产一区在线mmai| 中文字幕一区二区三区5566| 精品国产日韩亚洲一区91| 最美女人体内射精一区二区| 国模一区二区三区| 精品一区二区三人妻视频| 亚洲国产国产综合一区首页| 狠狠综合久久AV一区二区三区| 一区二区国产精品| 国产精品一区二区资源| 国产一区二区草草影院| 五十路熟女人妻一区二区| 国产福利视频一区二区| 无码精品人妻一区二区三区影院| 国产伦精品一区二区三区免费下载 | 91久久精品国产免费一区| 国产在线一区二区在线视频| 三级韩国一区久久二区综合| 精品一区二区三区四区电影| 日韩福利视频一区| 色噜噜一区二区三区| 国产一区二区三区无码免费| 国产suv精品一区二区6| 日本精品一区二区三区四区| 无码人妻一区二区三区免费看| 在线观看午夜亚洲一区| 中文字幕乱码一区二区免费| 久久亚洲AV午夜福利精品一区| 日本中文字幕一区二区有码在线| 国产亚洲无线码一区二区| 亚洲AV综合色区无码一区| 在线播放国产一区二区三区 | 中文字幕av人妻少妇一区二区 | 丝袜美腿一区二区三区| 人妻AV一区二区三区精品 | 天堂一区二区三区精品| 日本v片免费一区二区三区 | 一区二区精品在线观看| 日韩人妻无码一区二区三区 | 亚洲区精品久久一区二区三区|