Skip to content

Commit

Permalink
Add JsonLogic Export for SwitchCase (#1013)
Browse files Browse the repository at this point in the history
* Add JsonLogic Export for SwitchCase

Adding a simple approach for JsonLogic export for switch case.
This should allow future adaption of also imports etc.

Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>

* fixes

fix

.

* fix

* fix

* fix lint

---------

Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: ukrbublik <ukrbublik@gmail.com>
  • Loading branch information
aepfli and ukrbublik committed May 22, 2024
1 parent 40f3b1b commit f8db3b8
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 52 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Changelog
- 6.6.0
- Add JsonLogic Export for SwitchCase (PR #1013)
- 6.5.2
- Updated dependencies. `@babel/runtime` is now dep for core package (PR #1051) (issue #964)
- 6.5.1
Expand Down
5 changes: 4 additions & 1 deletion packages/core/modules/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,10 @@ const widgets = {
},
spelImportValue: (val) => {
return [val.value, []];
}
},
jsonLogic: function (val) {
return val === "" ? null : val;
},
}
};

Expand Down
105 changes: 91 additions & 14 deletions packages/core/modules/export/jsonLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ export const jsonLogicFormat = (item, config) => {
usedFields: [],
errors: []
};

const extendedConfig = extendConfig(config, undefined, false);
const logic = formatItem(item, extendedConfig, meta, true);

// build empty data
const {errors, usedFields} = meta;
const {fieldSeparator} = extendedConfig.settings;
Expand Down Expand Up @@ -49,7 +49,7 @@ export const jsonLogicFormat = (item, config) => {
}
}
}

return {
errors,
logic,
Expand All @@ -69,21 +69,24 @@ const formatItem = (item, config, meta, isRoot, parentField = null) => {
ret = formatGroup(item, config, meta, isRoot, parentField);
} else if (type === "rule") {
ret = formatRule(item, config, meta, parentField);
} else if (type == "switch_group") {
ret = formatSwitch(item, config, meta);
} else if (type == "case_group") {
ret = formatCase(item, config, meta, parentField);
}
if (isLocked && ret && lockedOp) {
ret = { [lockedOp] : ret };
}
return ret;
};


const formatGroup = (item, config, meta, isRoot, parentField = null) => {
const type = item.get("type");
const properties = item.get("properties") || new Map();
const mode = properties.get("mode");
const children = item.get("children1") || new List();
const field = properties.get("field");

let conjunction = properties.get("conjunction");
if (!conjunction)
conjunction = defaultConjunction(config);
Expand All @@ -101,7 +104,7 @@ const formatGroup = (item, config, meta, isRoot, parentField = null) => {
const list = children
.map((currentChild) => formatItem(currentChild, config, meta, false, groupField))
.filter((currentChild) => typeof currentChild !== "undefined");

if (isRuleGroup && mode != "struct" && !isGroup0) {
// "count" rule can have no "having" children, but should have number value
if (formattedValue == undefined)
Expand All @@ -116,7 +119,7 @@ const formatGroup = (item, config, meta, isRoot, parentField = null) => {
resultQuery = list.first();
else
resultQuery[conj] = list.toList().toJS();

// revert
if (not) {
resultQuery = { "!": resultQuery };
Expand All @@ -136,7 +139,7 @@ const formatGroup = (item, config, meta, isRoot, parentField = null) => {
};
} else {
// there is rule for count
const filter = !list.size
const filter = !list.size
? formattedField
: {
"filter": [
Expand All @@ -154,7 +157,7 @@ const formatGroup = (item, config, meta, isRoot, parentField = null) => {
resultQuery = formatLogic(config, properties, count, formattedValue, groupOperator);
}
}

return resultQuery;
};

Expand Down Expand Up @@ -204,19 +207,93 @@ const formatRule = (item, config, meta, parentField = null) => {
return formatLogic(config, properties, formattedField, formattedValue, operator, operatorOptions, fieldDefinition, isRev);
};

const formatSwitch = (item, config, meta) => {
const children = item.get("children1");
if (!children)
return undefined;
const cases = children
.map((currentChild) => formatCase(currentChild, config, meta, null))
.filter((currentChild) => typeof currentChild !== "undefined")
.valueSeq().toArray();

let filteredCases = [];
for (let i = 0 ; i < cases.length ; i++) {
if (i !== (cases.length - 1) && !cases[i][0]) {
meta.errors.push(`No condition for case ${i}`);
} else {
filteredCases.push(cases[i]);
if (i === (cases.length - 1) && cases[i][0]) {
// no default - add null as default
filteredCases.push([undefined, null]);
}
}
}

const formatItemValue = (config, properties, meta, operator, parentField) => {
const field = properties.get("field");
if (!filteredCases.length)
return undefined;

if (filteredCases.length === 1) {
// only 1 case without condition
let [_cond, defVal] = filteredCases[0];
if (defVal == undefined)
defVal = null;
return defVal;
}

const ret = { if: [] };
let ifArgs = ret.if;
const [_, defVal] = filteredCases[filteredCases.length - 1];
for (let i = 0 ; i < filteredCases.length - 1 ; i++) {
const isLastIf = i === (filteredCases.length - 2);
let [cond, value] = filteredCases[i];
if (value == undefined)
value = null;
if (cond == undefined)
cond = true;
ifArgs.push(cond); // if
ifArgs.push(value); // then
if (isLastIf) {
ifArgs.push(defVal); // else
} else {
// elif..
ifArgs.push({ if: [] });
ifArgs = ifArgs[ifArgs.length - 1].if;
}
}
return ret;
};

const formatCase = (item, config, meta, parentField = null) => {
const type = item.get("type");
if (type != "case_group") {
meta.errors.push(`Unexpected child of type ${type} inside switch`);
return undefined;
}
const properties = item.get("properties") || new Map();

const cond = formatGroup(item, config, meta, parentField);

const formattedItem = formatItemValue(
config, properties, meta, null, parentField, "!case_value"
);
return [cond, formattedItem];
};

const formatItemValue = (config, properties, meta, operator, parentField, expectedValueType = null) => {
let field = properties.get("field");
const iValueSrc = properties.get("valueSrc");
const iValueType = properties.get("valueType");
if (expectedValueType == "!case_value" || iValueType && iValueType.get(0) == "case_value") {
field = "!case_value";
}
const fieldDefinition = getFieldConfig(config, field) || {};
const operatorDefinition = getOperatorConfig(config, operator, field) || {};
const cardinality = getOpCardinality(operatorDefinition);
const iValue = properties.get("value");
const asyncListValues = properties.get("asyncListValues");
if (iValue == undefined)
return undefined;

let valueSrcs = [];
let valueTypes = [];
let oldUsedFields = meta.usedFields;
Expand Down Expand Up @@ -423,8 +500,8 @@ const formatLogic = (config, properties, formattedField, formattedValue, operato
const field = properties.get("field");
//const fieldSrc = properties.get("fieldSrc");
const operatorDefinition = getOperatorConfig(config, operator, field) || {};
let fn = typeof operatorDefinition.jsonLogic == "function"
? operatorDefinition.jsonLogic
let fn = typeof operatorDefinition.jsonLogic == "function"
? operatorDefinition.jsonLogic
: buildFnToFormatOp(operator, operatorDefinition, formattedField, formattedValue);
const args = [
formattedField,
Expand Down
1 change: 1 addition & 0 deletions packages/core/modules/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ interface ConfigUtils {
interface ExportUtils {
spelEscape(val: any): string;
spelFormatConcat(parts: SpelConcatParts): string;
jsonLogicFormatConcat(parts: SpelConcatParts): any;
spelImportConcat(val: SpelConcatValue): [SpelConcatParts | undefined, Array<string>];
}
interface ListUtils {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/modules/utils/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ export const spelEscape = (val, numberToFloat = false, arrayToArray = false) =>
}
};

export const jsonLogicFormatConcat = (parts) => {
if (parts && Array.isArray(parts) && parts.length) {
return parts
.map(part => part?.value ?? part)
.filter(r => r != undefined);
} else {
return undefined;
}
};

export const spelFormatConcat = (parts) => {
if (parts && Array.isArray(parts) && parts.length) {
return parts
Expand Down
1 change: 1 addition & 0 deletions packages/examples/demo_switch/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default (): Config => {
...InitialConfig.widgets.case_value,
spelFormatValue: QbUtils.ExportUtils.spelFormatConcat,
spelImportValue: QbUtils.ExportUtils.spelImportConcat,
jsonLogic: QbUtils.ExportUtils.jsonLogicFormatConcat,
factory: ({value, setValue, id}: WidgetProps) =>
<ReactSelect
value={value as SpelConcatPart[]}
Expand Down
95 changes: 74 additions & 21 deletions packages/examples/demo_switch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,26 +75,6 @@ const Demo: React.FC = () => {
/>
);

const renderSpelOutput = () => (
<div className="query-builder-result">
Output SpEL:
<pre>
{QbUtils.spelFormat(state.tree, state.config)}
</pre>
Values:
<pre>
{JSON.stringify(QbUtils.getSwitchValues(state.tree), undefined, 2)}
</pre>
<br/>
<hr/>
<br/>
Tree:
<pre>
{JSON.stringify(QbUtils.getTree(state.tree), undefined, 2)}
</pre>
</div>
);

const renderSpelInput = () => (
<div className="query-import-spel">
Input SpEL:
Expand All @@ -109,11 +89,84 @@ const Demo: React.FC = () => {
</div>
);

const renderSpelBlock = () => {
const [spel, spelErrors] = QbUtils._spelFormat(state.tree, state.config);

return (
<>
<div>
spelFormat:
{ spelErrors.length > 0
&& <pre style={preErrorStyle}>
{JSON.stringify(spelErrors, undefined, 2)}
</pre>
}
<pre style={preStyle}>
{JSON.stringify(spel, undefined, 2)}
</pre>
Values:
<pre>
{JSON.stringify(QbUtils.getSwitchValues(state.tree), undefined, 2)}
</pre>
</div>
<hr/>
</>
);
};

const renderJsTreeBlock = () => {
const treeJs = QbUtils.getTree(state.tree);

return (
<>
Tree:
<pre>
{JSON.stringify(treeJs, undefined, 2)}
</pre>
<br/>
<hr/>
<br/>
</>
);
};

const renderJsonLogicBlock = () => {
const {logic, data: logicData, errors: logicErrors} = QbUtils.jsonLogicFormat(state.tree, state.config);

return (
<>
<div>
<a href="http://jsonlogic.com/play.html" target="_blank" rel="noopener noreferrer">jsonLogicFormat</a>:
{ (logicErrors?.length || 0) > 0
&& <pre style={preErrorStyle}>
{JSON.stringify(logicErrors, undefined, 2)}
</pre>
}
{ !!logic
&& <pre style={preStyle}>
{"// Rule"}:<br />
{JSON.stringify(logic, undefined, 2)}
<br />
<hr />
{"// Data"}:<br />
{JSON.stringify(logicData, undefined, 2)}
</pre>
}
</div>
<hr/>
</>
);
};

return (
<div>
{renderSpelInput()}
{renderQueryBuilder()}
{renderSpelOutput()}
<div className="query-builder-result">
{renderSpelBlock()}
{renderJsonLogicBlock()}
{renderJsTreeBlock()}
</div>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/tests/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ let reporters;
if (isCI) {
reporters = ["mocha", "junit", "coverage"];
} else if (useCoverage) {
reporters = ["progress", "coverage"];
reporters = ["coverage", "mocha"]; // "progress"
} else {
reporters = ["mocha"];
}
Expand Down
Loading

0 comments on commit f8db3b8

Please sign in to comment.