blob: 2d9b752989d92f081dff791ba1690c1ffc19ff6c [file] [log] [blame]
/*
* 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);
}