【问题标题】:Parsing a Standard Query into JSON Clause Tree将标准查询解析为 JSON 子句树
【发布时间】:2021-10-01 18:33:56
【问题描述】:

我想为我的系统构建一个查询,外部系统可以使用它来根据条件进行配置。

在后端,我发现很容易有一个 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 表达式树)

问题是我希望它作为后端的 JSON 子句树,但我不希望用户需要编写这样的东西。 如果我可以创建类似 JQL(Jira 查询语言)之类的查询,那就太好了。 但我不想花很多精力为要转换的语言实际制作一个完整的证明解析器。

是否有任何标准化的方式来实现这一点?也许是一种标准化的查询语言,它可以使用库(在 JS 或 Java 中)进行转换。

从最终用户的角度来看,我希望上述查询类似于

section1.fieldabc == value1 AND (section2.fieldxyz<5000 OR section2.fieldxyz>10000)

【问题讨论】:

  • 为什么选择 JS 或 Java?它们是不同的语言,不能交叉兼容
  • @evolutionxbox 我在 Angular 2 上有 FE,在 Java 中有 BE。因此,从查询到 JSON 的转换要么在 JS/TS 中进行(在发送到 BE 之前,经过预处理),要么在 BE 中进行,我们在将其发送到 QueryEvaluatorEngine 之前将其更改为 JSON(并用于存储在 DB 中(作为 JSONB) )
  • JSON 是一个字符串。如果将当前查询放入字符串中,它将是有效的 JSON。
  • 好吧,也许我不是很清楚。我希望将其转换为 JSON 子句树,以便通过 QueryEvaluator 进行处理。因此,评估者的 i/p 将是 JSON 子句树。用户的输入将采用查询语言。现在用户在基于 JS 的 FE 中设置它,查询评估器在 Java 中。我希望在进入查询评估器或数据库之前将查询转换为 JSON 子句树。请参阅上面的 JSON 子句树示例和相应的查询。
  • 从js你可以试试jQuery-query-builder或者react-query-builder但是结构不完全一样

标签: javascript java json parsing text-parsing


【解决方案1】:

在 TypeScript 中编写了一个(相对)简单的解析器,它可以解析二元运算符(具有正确的操作顺序)和常量,处理括号、全局变量和简单的字段访问。源代码在my GitHub 上提供(将来可能会或可能不会更新),而这里是一个带有 JS 版本的 sn-p:

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));

转换为 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 QueryBuilderreact-query-builder,以使其对您的用户更加轻松和友好。

如果您更倾向于喜欢类似 SQL 的语法的“高级用户”,我的代码可能会有所帮助。不过,可能有许多更好的库可以帮助解决这个问题,例如,它们可能更健壮。报告语法错误或尝试访问不存在的变量/字段。再说一次,由于我的代码只有大约 150 行(如果包含类型,则为 200 行)并且写得不是太奇怪,如果它更适合您,那么根据您的需要调整它应该不会太难。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-25
    • 1970-01-01
    • 1970-01-01
    • 2012-01-29
    • 2023-03-28
    • 1970-01-01
    • 2015-07-14
    相关资源
    最近更新 更多