Skip to content

Commit

Permalink
[open-formulieren/open-forms#3597] Switch to a zod validation for jso…
Browse files Browse the repository at this point in the history
…nLogic
  • Loading branch information
Viicos committed Dec 22, 2023
1 parent 34717dc commit 240ca31
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 35 deletions.
16 changes: 3 additions & 13 deletions src/components/builder/values/items-expression.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {type JSONObject, type JSONValue} from '@open-formulieren/types/lib/types';
import {Field, useFormikContext} from 'formik';
import {useContext} from 'react';
import type {JSONObject} from '@open-formulieren/types/lib/types';
import {useFormikContext} from 'formik';
import {FormattedMessage} from 'react-intl';

import JSONEdit from '@/components/JSONEdit';
import {Component, Description} from '@/components/formio';
import {BuilderContext} from '@/context';

const NAME = 'openForms.itemsExpression';

Expand All @@ -19,8 +17,6 @@ export const ItemsExpression: React.FC = () => {
const {getFieldProps} = useFormikContext();
const {value = ''} = getFieldProps<JSONObject | string | undefined>(NAME);

const {validateLogic} = useContext(BuilderContext);

const htmlId = `editform-${NAME}`;
return (
<Component
Expand All @@ -36,13 +32,7 @@ export const ItemsExpression: React.FC = () => {
}
>
<div>
<Field
name={NAME}
validateOnChange={true}
validate={(logic: JSONValue) => validateLogic(logic, [['', '']])}
>
{() => <JSONEdit name={NAME} data={value} rows={3} id={htmlId} />}
</Field>
<JSONEdit name={NAME} data={value} rows={3} id={htmlId} />
</div>

<Description
Expand Down
11 changes: 6 additions & 5 deletions src/registry/radio/edit-validation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import {IntlShape} from 'react-intl';
import {z} from 'zod';

import {buildCommonSchema, jsonSchema, optionSchema} from '@/registry/validation';
import {BuilderContextType} from '@/context';
import {buildCommonSchema, itemsExpressionSchema, optionSchema} from '@/registry/validation';

// z.object(...).or(z.object(...)) based on openForms.dataSrc doesn't seem to work,
// looks like the union validation only works if the discriminator is in the top level
// object :(
// so we mark each aspect as optional so that *when* it is provided, we can run the
// validation
const buildValuesSchema = (intl: IntlShape) =>
const buildValuesSchema = (intl: IntlShape, builderContext: BuilderContextType) =>
z.object({
values: optionSchema(intl).array().min(1).optional(),
openForms: z.object({
dataSrc: z.union([z.literal('manual'), z.literal('variable')]),
// TODO: wire up infernologic type checking
itemsExpression: jsonSchema.optional(),
itemsExpression: itemsExpressionSchema(builderContext).optional(),
}),
});

const schema = (intl: IntlShape) => buildCommonSchema(intl).and(buildValuesSchema(intl));
const schema = (intl: IntlShape, builderContext: BuilderContextType) =>
buildCommonSchema(intl).and(buildValuesSchema(intl, builderContext));

export default schema;
10 changes: 6 additions & 4 deletions src/registry/selectboxes/edit-validation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import {IntlShape} from 'react-intl';
import {z} from 'zod';

import {buildCommonSchema, jsonSchema, optionSchema} from '@/registry/validation';
import {BuilderContextType} from '@/context';
import {buildCommonSchema, itemsExpressionSchema, optionSchema} from '@/registry/validation';

// z.object(...).or(z.object(...)) based on openForms.dataSrc doesn't seem to work,
// looks like the union validation only works if the discriminator is in the top level
// object :(
// so we mark each aspect as optional so that *when* it is provided, we can run the
// validation
const buildValuesSchema = (intl: IntlShape) =>
const buildValuesSchema = (intl: IntlShape, builderContext: BuilderContextType) =>
z.object({
values: optionSchema(intl).array().min(1).optional(),
openForms: z.object({
dataSrc: z.union([z.literal('manual'), z.literal('variable')]),
// TODO: wire up infernologic type checking
itemsExpression: jsonSchema.optional(),
itemsExpression: itemsExpressionSchema(builderContext).optional(),
}),
});

const schema = (intl: IntlShape) => buildCommonSchema(intl).and(buildValuesSchema(intl));
const schema = (intl: IntlShape, builderContext: BuilderContextType) =>
buildCommonSchema(intl).and(buildValuesSchema(intl, builderContext));

export default schema;
18 changes: 18 additions & 0 deletions src/registry/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import {IntlShape, defineMessages} from 'react-intl';
import {z} from 'zod';

import type {BuilderContextType} from '@/context';

/*
Validation message definitions.
*/
Expand Down Expand Up @@ -136,3 +138,19 @@ export const getErrorMap = (builder: ErrorBuilder): z.ZodErrorMap => {
};
return errorMap;
};

/*
Related to jsonLogic
*/

export const itemsExpressionSchema = (builderContext: BuilderContextType) =>
jsonSchema.superRefine((val, ctx) => {
const result = builderContext.validateLogic(val, [['', '']]);
if (result !== '') {
// TODO adapt once the InferNoLogic API uses exceptions
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: result,
});
}
});
20 changes: 7 additions & 13 deletions src/utils/jsonlogic.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* JsonLogic type checking utility functions
*/
import {infer} from '@open-formulieren/infernologic';
import {type JSONObject, type JSONValue} from '@open-formulieren/types/lib/types';
import {infer} from '@open-formulieren/infernologic/lib';
import type {JSONObject, JSONValue} from '@open-formulieren/types/lib/types';

import {type AnyComponentSchema} from '@/types';
import type {AnyComponentSchema} from '@/types';

/**
* @param logic - JsonLogic expression
Expand Down Expand Up @@ -109,25 +109,19 @@ const dataTypeForVariableDefinition = ({initialValue, dataType}: VariableDefinit
boolean: true,
}[dataType];

const dataTypeForComponent = (
component:
| AnyComponentSchema
| {
type: 'map' | 'editgrid' | 'password' | 'signature'; // TODO remove when these are present in AnyComponentSchema
multiple?: boolean;
defaultValue: JSONValue;
}
): JSONValue => {
const dataTypeForComponent = (component: AnyComponentSchema): JSONValue => {
// For now return example values as accepted by InferNoLogic
// But example values cannot distinguish arrays from tuples!
const value = {
address: {
addressNL: {
postcode: '',
houseNumber: '',
houseLetter: '',
houseNumberAddition: '',
},
currency: 1,
cosign: 'string',
coSign: '', // Actually never
number: 1,
checkbox: true,
//@ts-ignore selectboxes always have component.defaultValue (this does work when rewritten as a lengthy switch/case)
Expand Down

0 comments on commit 240ca31

Please sign in to comment.