/*
|
* Licensed to the Apache Software Foundation (ASF) under one
|
* or more contributor license agreements. See the NOTICE file
|
* distributed with this work for additional information
|
* regarding copyright ownership. The ASF licenses this file
|
* to you under the Apache License, Version 2.0 (the
|
* "License"); you may not use this file except in compliance
|
* with the License. You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing,
|
* software distributed under the License is distributed on an
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* KIND, either express or implied. See the License for the
|
* specific language governing permissions and limitations
|
* under the License.
|
*/
|
|
import { OptionDataValue, DimensionLoose, Dictionary } from './types';
|
import {
|
keys, isArray, map, isObject, isString, HashMap, isRegExp, isArrayLike, hasOwn
|
} from 'zrender/src/core/util';
|
import { throwError, makePrintable } from './log';
|
import {
|
RawValueParserType, getRawValueParser,
|
RelationalOperator, FilterComparator, createFilterComparator
|
} from '../data/helper/dataValueHelper';
|
|
|
// PENDING:
|
// (1) Support more parser like: `parser: 'trim'`, `parser: 'lowerCase'`, `parser: 'year'`, `parser: 'dayOfWeek'`?
|
// (2) Support piped parser ?
|
// (3) Support callback parser or callback condition?
|
// (4) At present do not support string expression yet but only stuctured expression.
|
|
|
/**
|
* The structured expression considered:
|
* (1) Literal simplicity
|
* (2) Sementic displayed clearly
|
*
|
* Sementic supports:
|
* (1) relational expression
|
* (2) logical expression
|
*
|
* For example:
|
* ```js
|
* {
|
* and: [{
|
* or: [{
|
* dimension: 'Year', gt: 2012, lt: 2019
|
* }, {
|
* dimension: 'Year', '>': 2002, '<=': 2009
|
* }]
|
* }, {
|
* dimension: 'Product', eq: 'Tofu'
|
* }]
|
* }
|
*
|
* { dimension: 'Product', eq: 'Tofu' }
|
*
|
* {
|
* or: [
|
* { dimension: 'Product', value: 'Tofu' },
|
* { dimension: 'Product', value: 'Biscuit' }
|
* ]
|
* }
|
*
|
* {
|
* and: [true]
|
* }
|
* ```
|
*
|
* [PARSER]
|
* In an relation expression object, we can specify some built-in parsers:
|
* ```js
|
* // Trim if string
|
* {
|
* parser: 'trim',
|
* eq: 'Flowers'
|
* }
|
* // Parse as time and enable arithmetic relation comparison.
|
* {
|
* parser: 'time',
|
* lt: '2012-12-12'
|
* }
|
* // Normalize number-like string and make '-' to Null.
|
* {
|
* parser: 'time',
|
* lt: '2012-12-12'
|
* }
|
* // Normalize to number:
|
* // + number-like string (like ' 123 ') can be converted to a number.
|
* // + where null/undefined or other string will be converted to NaN.
|
* {
|
* parser: 'number',
|
* eq: 2011
|
* }
|
* // RegExp, include the feature in SQL: `like '%xxx%'`.
|
* {
|
* reg: /^asdf$/
|
* }
|
* {
|
* reg: '^asdf$' // Serializable reg exp, will be `new RegExp(...)`
|
* }
|
* ```
|
*
|
*
|
* [EMPTY_RULE]
|
* (1) If a relational expression set value as `null`/`undefined` like:
|
* `{ dimension: 'Product', lt: undefined }`,
|
* The result will be `false` rather than `true`.
|
* Consider the case like "filter condition", return all result when null/undefined
|
* is probably not expected and even dangours.
|
* (2) If a relational expression has no operator like:
|
* `{ dimension: 'Product' }`,
|
* An error will be thrown. Because it is probably a mistake.
|
* (3) If a logical expression has no children like
|
* `{ and: undefined }` or `{ and: [] }`,
|
* An error will be thrown. Because it is probably an mistake.
|
* (4) If intending have a condition that always `true` or always `false`,
|
* Use `true` or `flase`.
|
* The entire condition can be `true`/`false`,
|
* or also can be `{ and: [true] }`, `{ or: [false] }`
|
*/
|
|
|
// --------------------------------------------------
|
// --- Relational Expression --------------------------
|
// --------------------------------------------------
|
|
/**
|
* Date string and ordinal string can be accepted.
|
*/
|
interface RelationalExpressionOptionByOp extends Record<RelationalOperator, OptionDataValue> {
|
reg?: RegExp | string; // RegExp
|
};
|
const RELATIONAL_EXPRESSION_OP_ALIAS_MAP = {
|
value: 'eq',
|
|
// PENDING: not good for literal semantic?
|
'<': 'lt',
|
'<=': 'lte',
|
'>': 'gt',
|
'>=': 'gte',
|
'=': 'eq',
|
'!=': 'ne',
|
'<>': 'ne'
|
|
// Might mileading for sake of the different between '==' and '===',
|
// So dont support them.
|
// '==': 'eq',
|
// '===': 'seq',
|
// '!==': 'sne'
|
|
// PENDING: Whether support some common alias "ge", "le", "neq"?
|
// ge: 'gte',
|
// le: 'lte',
|
// neq: 'ne',
|
} as const;
|
type RelationalExpressionOptionByOpAlias = Record<keyof typeof RELATIONAL_EXPRESSION_OP_ALIAS_MAP, OptionDataValue>;
|
|
interface RelationalExpressionOption extends
|
RelationalExpressionOptionByOp, RelationalExpressionOptionByOpAlias {
|
dimension?: DimensionLoose;
|
parser?: RawValueParserType;
|
}
|
|
type RelationalExpressionOpEvaluate = (tarVal: unknown, condVal: unknown) => boolean;
|
|
|
class RegExpEvaluator implements FilterComparator {
|
private _condVal: RegExp;
|
|
constructor(rVal: unknown) {
|
// Support condVal: RegExp | string
|
const condValue = this._condVal = isString(rVal) ? new RegExp(rVal)
|
: isRegExp(rVal) ? rVal as RegExp
|
: null;
|
if (condValue == null) {
|
let errMsg = '';
|
if (__DEV__) {
|
errMsg = makePrintable('Illegal regexp', rVal, 'in');
|
}
|
throwError(errMsg);
|
}
|
}
|
|
evaluate(lVal: unknown): boolean {
|
const type = typeof lVal;
|
return type === 'string' ? this._condVal.test(lVal as string)
|
: type === 'number' ? this._condVal.test(lVal + '')
|
: false;
|
}
|
}
|
|
|
|
|
// --------------------------------------------------
|
// --- Logical Expression ---------------------------
|
// --------------------------------------------------
|
|
|
interface LogicalExpressionOption {
|
and?: LogicalExpressionSubOption[];
|
or?: LogicalExpressionSubOption[];
|
not?: LogicalExpressionSubOption;
|
}
|
type LogicalExpressionSubOption =
|
LogicalExpressionOption | RelationalExpressionOption | TrueFalseExpressionOption;
|
|
|
|
// -----------------------------------------------------
|
// --- Conditional Expression --------------------------
|
// -----------------------------------------------------
|
|
|
export type TrueExpressionOption = true;
|
export type FalseExpressionOption = false;
|
export type TrueFalseExpressionOption = TrueExpressionOption | FalseExpressionOption;
|
|
export type ConditionalExpressionOption =
|
LogicalExpressionOption
|
| RelationalExpressionOption
|
| TrueFalseExpressionOption;
|
|
type ValueGetterParam = Dictionary<unknown>;
|
export interface ConditionalExpressionValueGetterParamGetter<VGP extends ValueGetterParam = ValueGetterParam> {
|
(relExpOption: RelationalExpressionOption): VGP
|
}
|
export interface ConditionalExpressionValueGetter<VGP extends ValueGetterParam = ValueGetterParam> {
|
(param: VGP): OptionDataValue
|
}
|
|
interface ParsedConditionInternal {
|
evaluate(): boolean;
|
}
|
class ConstConditionInternal implements ParsedConditionInternal {
|
value: boolean;
|
evaluate(): boolean {
|
return this.value;
|
}
|
}
|
class AndConditionInternal implements ParsedConditionInternal {
|
children: ParsedConditionInternal[];
|
evaluate() {
|
const children = this.children;
|
for (let i = 0; i < children.length; i++) {
|
if (!children[i].evaluate()) {
|
return false;
|
}
|
}
|
return true;
|
}
|
}
|
class OrConditionInternal implements ParsedConditionInternal {
|
children: ParsedConditionInternal[];
|
evaluate() {
|
const children = this.children;
|
for (let i = 0; i < children.length; i++) {
|
if (children[i].evaluate()) {
|
return true;
|
}
|
}
|
return false;
|
}
|
}
|
class NotConditionInternal implements ParsedConditionInternal {
|
child: ParsedConditionInternal;
|
evaluate() {
|
return !this.child.evaluate();
|
}
|
}
|
class RelationalConditionInternal implements ParsedConditionInternal {
|
valueGetterParam: ValueGetterParam;
|
valueParser: ReturnType<typeof getRawValueParser>;
|
// If no parser, be null/undefined.
|
getValue: ConditionalExpressionValueGetter;
|
subCondList: FilterComparator[];
|
|
evaluate() {
|
const needParse = !!this.valueParser;
|
// Call getValue with no `this`.
|
const getValue = this.getValue;
|
const tarValRaw = getValue(this.valueGetterParam);
|
const tarValParsed = needParse ? this.valueParser(tarValRaw) : null;
|
|
// Relational cond follow "and" logic internally.
|
for (let i = 0; i < this.subCondList.length; i++) {
|
if (!this.subCondList[i].evaluate(needParse ? tarValParsed : tarValRaw)) {
|
return false;
|
}
|
}
|
return true;
|
}
|
}
|
|
function parseOption(
|
exprOption: ConditionalExpressionOption,
|
getters: ConditionalGetters
|
): ParsedConditionInternal {
|
if (exprOption === true || exprOption === false) {
|
const cond = new ConstConditionInternal();
|
cond.value = exprOption as boolean;
|
return cond;
|
}
|
|
let errMsg = '';
|
if (!isObjectNotArray(exprOption)) {
|
if (__DEV__) {
|
errMsg = makePrintable(
|
'Illegal config. Expect a plain object but actually', exprOption
|
);
|
}
|
throwError(errMsg);
|
}
|
|
if ((exprOption as LogicalExpressionOption).and) {
|
return parseAndOrOption('and', exprOption as LogicalExpressionOption, getters);
|
}
|
else if ((exprOption as LogicalExpressionOption).or) {
|
return parseAndOrOption('or', exprOption as LogicalExpressionOption, getters);
|
}
|
else if ((exprOption as LogicalExpressionOption).not) {
|
return parseNotOption(exprOption as LogicalExpressionOption, getters);
|
}
|
|
return parseRelationalOption(exprOption as RelationalExpressionOption, getters);
|
}
|
|
function parseAndOrOption(
|
op: 'and' | 'or',
|
exprOption: LogicalExpressionOption,
|
getters: ConditionalGetters
|
): ParsedConditionInternal {
|
const subOptionArr = exprOption[op] as ConditionalExpressionOption[];
|
let errMsg = '';
|
if (__DEV__) {
|
errMsg = makePrintable(
|
'"and"/"or" condition should only be `' + op + ': [...]` and must not be empty array.',
|
'Illegal condition:', exprOption
|
);
|
}
|
if (!isArray(subOptionArr)) {
|
throwError(errMsg);
|
}
|
if (!(subOptionArr as []).length) {
|
throwError(errMsg);
|
}
|
const cond = op === 'and' ? new AndConditionInternal() : new OrConditionInternal();
|
cond.children = map(subOptionArr, subOption => parseOption(subOption, getters));
|
if (!cond.children.length) {
|
throwError(errMsg);
|
}
|
return cond;
|
}
|
|
function parseNotOption(
|
exprOption: LogicalExpressionOption,
|
getters: ConditionalGetters
|
): ParsedConditionInternal {
|
const subOption = exprOption.not as ConditionalExpressionOption;
|
let errMsg = '';
|
if (__DEV__) {
|
errMsg = makePrintable(
|
'"not" condition should only be `not: {}`.',
|
'Illegal condition:', exprOption
|
);
|
}
|
if (!isObjectNotArray(subOption)) {
|
throwError(errMsg);
|
}
|
const cond = new NotConditionInternal();
|
cond.child = parseOption(subOption, getters);
|
if (!cond.child) {
|
throwError(errMsg);
|
}
|
return cond;
|
}
|
|
function parseRelationalOption(
|
exprOption: RelationalExpressionOption,
|
getters: ConditionalGetters
|
): ParsedConditionInternal {
|
let errMsg = '';
|
|
const valueGetterParam = getters.prepareGetValue(exprOption);
|
|
const subCondList = [] as RelationalConditionInternal['subCondList'];
|
const exprKeys = keys(exprOption);
|
|
const parserName = exprOption.parser;
|
const valueParser = parserName ? getRawValueParser(parserName) : null;
|
|
for (let i = 0; i < exprKeys.length; i++) {
|
const keyRaw = exprKeys[i];
|
if (keyRaw === 'parser' || getters.valueGetterAttrMap.get(keyRaw)) {
|
continue;
|
}
|
|
const op: keyof RelationalExpressionOptionByOp = hasOwn(RELATIONAL_EXPRESSION_OP_ALIAS_MAP, keyRaw)
|
? RELATIONAL_EXPRESSION_OP_ALIAS_MAP[keyRaw as keyof RelationalExpressionOptionByOpAlias]
|
: (keyRaw as keyof RelationalExpressionOptionByOp);
|
const condValueRaw = exprOption[keyRaw];
|
const condValueParsed = valueParser ? valueParser(condValueRaw) : condValueRaw;
|
const evaluator = createFilterComparator(op, condValueParsed)
|
|| (op === 'reg' && new RegExpEvaluator(condValueParsed));
|
|
if (!evaluator) {
|
if (__DEV__) {
|
errMsg = makePrintable(
|
'Illegal relational operation: "' + keyRaw + '" in condition:', exprOption
|
);
|
}
|
throwError(errMsg);
|
}
|
|
subCondList.push(evaluator);
|
}
|
|
if (!subCondList.length) {
|
if (__DEV__) {
|
errMsg = makePrintable(
|
'Relational condition must have at least one operator.',
|
'Illegal condition:', exprOption
|
);
|
}
|
// No relational operator always disabled in case of dangers result.
|
throwError(errMsg);
|
}
|
|
const cond = new RelationalConditionInternal();
|
cond.valueGetterParam = valueGetterParam;
|
cond.valueParser = valueParser;
|
cond.getValue = getters.getValue;
|
cond.subCondList = subCondList;
|
|
return cond;
|
}
|
|
function isObjectNotArray(val: unknown): boolean {
|
return isObject(val) && !isArrayLike(val);
|
}
|
|
|
class ConditionalExpressionParsed {
|
|
private _cond: ParsedConditionInternal;
|
|
constructor(
|
exprOption: ConditionalExpressionOption,
|
getters: ConditionalGetters
|
) {
|
this._cond = parseOption(exprOption, getters);
|
}
|
|
evaluate(): boolean {
|
return this._cond.evaluate();
|
}
|
};
|
|
interface ConditionalGetters<VGP extends ValueGetterParam = ValueGetterParam> {
|
prepareGetValue: ConditionalExpressionValueGetterParamGetter<VGP>;
|
getValue: ConditionalExpressionValueGetter<VGP>;
|
valueGetterAttrMap: HashMap<boolean, string>;
|
}
|
|
export function parseConditionalExpression<VGP extends ValueGetterParam = ValueGetterParam>(
|
exprOption: ConditionalExpressionOption,
|
getters: ConditionalGetters<VGP>
|
): ConditionalExpressionParsed {
|
return new ConditionalExpressionParsed(exprOption, getters);
|
}
|