Skip to content

Commit 784dc29

Browse files
committed
feat: Support objects in expressions
1 parent 2bcb9be commit 784dc29

12 files changed

Lines changed: 233 additions & 81 deletions

File tree

schemas/json/layout/expression.schema.v1.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
{ "$ref": "#/definitions/strict-boolean" },
4848
{ "$ref": "#/definitions/strict-number" },
4949
{ "$ref": "#/definitions/strict-list" },
50+
{ "$ref": "#/definitions/strict-object" },
5051
{ "$ref": "#/definitions/func-if" }
5152
]
5253
},
@@ -155,6 +156,17 @@
155156
{ "$ref": "#/definitions/func-list" }
156157
]
157158
},
159+
"object": {
160+
"title": "Any expression returning an object",
161+
"$ref": "#/definitions/strict-object"
162+
},
163+
"strict-object": {
164+
"title": "Any expression returning an object (strict)",
165+
"anyOf": [
166+
{ "$ref": "#/definitions/func-dataModel" },
167+
{ "$ref": "#/definitions/func-object" }
168+
]
169+
},
158170
"func-if": {
159171
"title": "If/else conditional expression",
160172
"description": "This function will evaluate and return the result of either branch. If else is not given, null will be returned instead.",
@@ -702,6 +714,15 @@
702714
],
703715
"additionalItems": { "$ref": "#/definitions/any" }
704716
},
717+
"func-object": {
718+
"title": "Create an object",
719+
"description": "This function creates a list from its arguments.",
720+
"type": "array",
721+
"items": [
722+
{ "const": "object" }
723+
],
724+
"additionalItems": { "$ref": "#/definitions/any" }
725+
},
705726
"compare-operator": {
706727
"title": "Comparison operator",
707728
"enum": ["equals", "greaterThan", "greaterThanEq", "lessThan", "lessThanEq", "isAfter", "isBefore", "isAfterEq", "isBeforeEq", "isSameDay"]

src/codegen/dataTypes/GenerateExpressionOr.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const toTsMap: { [key in ExprVal]: string } = {
1111
[ExprVal.String]: 'ExprValToActualOrExpr<ExprVal.String>',
1212
[ExprVal.Date]: 'ExprValToActualOrExpr<ExprVal.Date>',
1313
[ExprVal.List]: 'ExprValToActualOrExpr<ExprVal.List>',
14+
[ExprVal.Object]: 'ExprValToActualOrExpr<ExprVal.Object>',
1415
};
1516

1617
const toSchemaMap: { [key in ExprVal]: JSONSchema7 } = {
@@ -20,6 +21,7 @@ const toSchemaMap: { [key in ExprVal]: JSONSchema7 } = {
2021
[ExprVal.String]: { $ref: 'expression.schema.v1.json#/definitions/string' },
2122
[ExprVal.Date]: { $ref: 'expression.schema.v1.json#/definitions/string' },
2223
[ExprVal.List]: { $ref: 'expression.schema.v1.json#/definitions/list' },
24+
[ExprVal.Object]: { $ref: 'expression.schema.v1.json#/definitions/object' },
2325
};
2426

2527
type TypeMap<Val extends ExprVal> = Val extends ExprVal.Boolean

src/features/expressions/expression-functions.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
ExprFunctionName,
2222
ExprFunctions,
2323
ExprValToActual,
24+
ValidObject,
2425
ValidValue,
2526
} from 'src/features/expressions/types';
2627
import type { ValidationContext } from 'src/features/expressions/validation';
@@ -326,6 +327,11 @@ export const ExprFunctionDefinitions = {
326327
returns: ExprVal.List,
327328
needs: noSources,
328329
},
330+
object: {
331+
args: args(rest(ExprVal.Any)),
332+
returns: ExprVal.Object,
333+
needs: noSources,
334+
},
329335
_experimentalSelectAndMap: {
330336
args: args(
331337
required(ExprVal.String),
@@ -803,6 +809,21 @@ export const ExprFunctionImplementations: { [K in ExprFunctionName]: Implementat
803809
list(...items): ValidValue[] {
804810
return items;
805811
},
812+
object(...items): ValidObject {
813+
if (items.length % 2 === 1) {
814+
throw new ExprRuntimeError(this.expr, this.path, 'The object function must have an even number of arguments');
815+
}
816+
const keys = extractEvenIndexedArguments(items);
817+
if (!consistsOfStringsOnly(keys)) {
818+
throw new ExprRuntimeError(this.expr, this.path, 'Object keys must be strings');
819+
}
820+
if (!areStringsUnique(keys)) {
821+
throw new ExprRuntimeError(this.expr, this.path, 'Object keys must be unique');
822+
}
823+
const values = extractOddIndexedArguments(items);
824+
const entries = zipArrays<string, ValidValue>(keys, values);
825+
return Object.fromEntries(entries);
826+
},
806827
_experimentalSelectAndMap(path, propertyToSelect, prepend, append, appendToLastElement = true) {
807828
if (path === null || propertyToSelect == null) {
808829
throw new ExprRuntimeError(this.expr, this.path, `Cannot lookup dataModel null`);
@@ -1064,3 +1085,23 @@ function validateDates(this: EvaluateExpressionParams, a: ExprDate, b: ExprDate)
10641085
throw new ExprRuntimeError(this.expr, this.path, `Can not compare timestamps where only one specify timezone`);
10651086
}
10661087
}
1088+
1089+
function extractEvenIndexedArguments(args: ValidValue[]): ValidValue[] {
1090+
return args.filter((_, index) => index % 2 === 0);
1091+
}
1092+
1093+
function extractOddIndexedArguments(args: ValidValue[]): ValidValue[] {
1094+
return args.filter((_, index) => index % 2 === 1);
1095+
}
1096+
1097+
function consistsOfStringsOnly(array: ValidValue[]): array is string[] {
1098+
return array.every((value) => typeof value === 'string');
1099+
}
1100+
1101+
function areStringsUnique(strings: string[]): boolean {
1102+
return strings.length === new Set(strings).size;
1103+
}
1104+
1105+
function zipArrays<V1, V2>(array1: V1[], array2: V2[]): Array<[V1, V2]> {
1106+
return array1.map((value, index) => [value, array2[index]]);
1107+
}

src/features/expressions/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from 'src/features/expressions/errors';
1111
import { ExprFunctionDefinitions, ExprFunctionImplementations } from 'src/features/expressions/expression-functions';
1212
import { ExprVal } from 'src/features/expressions/types';
13-
import { isValidArray } from 'src/features/expressions/validation';
13+
import { isValidArray, isValidObject } from 'src/features/expressions/validation';
1414
import { type ExpressionDataSources } from 'src/utils/layout/useExpressionDataSources';
1515
import type {
1616
ExprConfig,
@@ -308,7 +308,7 @@ export const ExprTypes: {
308308
},
309309
[ExprVal.Any]: {
310310
nullable: true,
311-
accepts: [ExprVal.Boolean, ExprVal.String, ExprVal.Number, ExprVal.Any, ExprVal.List],
311+
accepts: [ExprVal.Boolean, ExprVal.String, ExprVal.Number, ExprVal.Any, ExprVal.List, ExprVal.Object],
312312
impl: (arg) => arg,
313313
},
314314
[ExprVal.Date]: {
@@ -337,6 +337,17 @@ export const ExprTypes: {
337337
}
338338
},
339339
},
340+
[ExprVal.Object]: {
341+
nullable: false,
342+
accepts: [ExprVal.Object, ExprVal.Any],
343+
impl(arg) {
344+
if (isValidObject(arg)) {
345+
return arg;
346+
} else {
347+
throw new UnexpectedType(this.expr, this.path, 'object', arg);
348+
}
349+
},
350+
},
340351
};
341352

342353
/**

src/features/expressions/shared-tests/functions/dataModel/array-is-null.json

Lines changed: 0 additions & 41 deletions
This file was deleted.

src/features/expressions/shared-tests/functions/dataModel/lookup-list.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"booleanList": [true, false],
1313
"nullList": [null, null],
1414
"multidimensionalList": [[[1, 2], [3, 4]], [[5, 6], [7, 8]]],
15+
"objectList": [{ "a": 1 }, { "a": 2 }],
1516
"emptyList": [],
1617
"differentTypesList": [1, "string", true, null, []]
1718
}
@@ -43,6 +44,11 @@
4344
"expression": ["dataModel", "multidimensionalList"],
4445
"expects": [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
4546
},
47+
{
48+
"name": "Object list lookup",
49+
"expression": ["dataModel", "objectList"],
50+
"expects": [{ "a": 1 }, { "a": 2 }]
51+
},
4652
{
4753
"name": "Empty list lookup",
4854
"expression": ["dataModel", "emptyList"],
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"name": "Lookup a list value",
3+
"dataModels": [
4+
{
5+
"dataElement": {
6+
"id": "00dd7417-5b4e-402a-bb73-007537071f1d",
7+
"dataType": "default"
8+
},
9+
"data": {
10+
"simpleCity": {
11+
"name": "Oslo",
12+
"numberOfInhabitants": 724290,
13+
"isCapital": true,
14+
"largestVolcano": null
15+
},
16+
"complexCity": {
17+
"name": "Oslo",
18+
"trainStations": [
19+
{
20+
"name": "Oslo sentralstasjon",
21+
"numberOfPlatforms": 19,
22+
"isUnderground": false,
23+
"expressTrains": ["F1", "F4", "F5", "F6"],
24+
"connectedBusTerminal": "Oslo bussterminal"
25+
},
26+
{
27+
"name": "Nationaltheatret",
28+
"numberOfPlatforms": 4,
29+
"isUnderground": true,
30+
"expressTrains": ["F5"],
31+
"connectedBusTerminal": null
32+
}
33+
]
34+
}
35+
}
36+
}
37+
],
38+
"testCases": [
39+
{
40+
"name": "Simple object lookup",
41+
"expression": ["dataModel", "simpleCity"],
42+
"expects": {
43+
"name": "Oslo",
44+
"numberOfInhabitants": 724290,
45+
"isCapital": true,
46+
"largestVolcano": null
47+
}
48+
},
49+
{
50+
"name": "Complex object lookup",
51+
"expression": ["dataModel", "complexCity"],
52+
"expects": {
53+
"name": "Oslo",
54+
"trainStations": [
55+
{
56+
"name": "Oslo sentralstasjon",
57+
"numberOfPlatforms": 19,
58+
"isUnderground": false,
59+
"expressTrains": ["F1", "F4", "F5", "F6"],
60+
"connectedBusTerminal": "Oslo bussterminal"
61+
},
62+
{
63+
"name": "Nationaltheatret",
64+
"numberOfPlatforms": 4,
65+
"isUnderground": true,
66+
"expressTrains": ["F5"],
67+
"connectedBusTerminal": null
68+
}
69+
]
70+
}
71+
}
72+
]
73+
}

src/features/expressions/shared-tests/functions/dataModel/object-is-null.json

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/features/expressions/shared-tests/functions/list/list.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
"expression": ["list", ["list", 1, 2], ["list", 3, 4]],
2222
"expects": [[1, 2], [3, 4]]
2323
},
24+
{
25+
"name": "Supports objects",
26+
"expression": ["list", ["object", "a", 1], ["object", "b", 2]],
27+
"expects": [{ "a": 1 }, { "b": 2 }]
28+
},
2429
{
2530
"name": "Does not evaluate the final expression when the result happens to be a valid expression",
2631
"expression": ["list", "equals", 1, 1],

0 commit comments

Comments
 (0)