Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/core/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import type AJV from 'ajv';
import type { ErrorObject } from 'ajv';
import type { ValidatorOption } from '../util/formValidator';
import { JsonSchema, UISchemaElement } from '../models';
import { generateDefaultUISchema, generateJsonSchema } from '../generators';

Expand Down Expand Up @@ -145,7 +146,15 @@ export interface UpdateCoreAction {
}

export interface InitActionOptions {
/** The AJV instance to validate with. Ignored for form validation when `validator` is set. */
ajv?: AJV;
/**
* A custom Form Validator: a factory producing one per schema, or a
* validator already bound to the schema. Takes precedence over `ajv`.
* Passing `undefined` explicitly switches back to AJV; leaving the key out
* keeps the validator currently in use.
*/
validator?: ValidatorOption;
validationMode?: ValidationMode;
additionalErrors?: ErrorObject[];
}
Expand Down
193 changes: 157 additions & 36 deletions packages/core/src/reducers/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,20 @@ import {
UpdateCoreAction,
} from '../actions';
import { JsonFormsCore, Reducer, ValidationMode } from '../store';
import type { JsonSchema } from '../models';
import type Ajv from 'ajv';
import type { ErrorObject } from 'ajv';
import isFunction from 'lodash/isFunction';
import { createAjv, validate } from '../util';
import { createAjv, validate } from '../util/validator';
import {
compiledAjvValidator,
createAjvValidator,
FormValidator,
isAjvFormValidator,
issuesToErrors,
toFormValidatorFactory,
ValidatorOption,
} from '../util/formValidator';

export const initState: JsonFormsCore = {
data: {},
Expand All @@ -56,6 +66,8 @@ export const initState: JsonFormsCore = {
ajv: undefined,
validationMode: 'ValidateAndShow',
additionalErrors: [],
formValidator: undefined,
validatorOption: undefined,
};

export const getValidationMode = (
Expand Down Expand Up @@ -119,20 +131,91 @@ const hasAjvOption = (option: any): option is InitActionOptions => {
return false;
};

const hasValidatorOption = (option: any): option is InitActionOptions =>
!!option && !isFunction(option.compile) && 'validator' in option;

/**
* The custom `validator` option in effect for an action: the one given in the
* action's options (an explicit `validator: undefined` switches back to AJV),
* or the one already stored in state when the options do not mention a
* validator at all. Returns `undefined` when AJV performs the validation.
*/
export const getValidatorOption = (
state: JsonFormsCore,
action?: InitAction | UpdateCoreAction
): ValidatorOption | undefined => {
if (action && hasValidatorOption(action.options)) {
return action.options.validator;
}
return state.validatorOption;
};

/**
* Creates the Form Validator for `schema`: through the custom option when one
* is configured, otherwise through the built-in AJV adapter.
*/
const createFormValidator = (
option: ValidatorOption | undefined,
ajv: Ajv | undefined,
schema: JsonSchema,
validationMode: ValidationMode
): FormValidator | undefined => {
if (validationMode === 'NoValidation') {
return undefined;
}
const factory =
option === undefined
? createAjvValidator(ajv)
: toFormValidatorFactory(option);
return factory(schema);
};

/**
* The Form Validator to validate with: the cached one, or, for states that
* only carry a compiled AJV function (e.g. constructed by hand), that
* function wrapped as a Form Validator.
*/
const currentFormValidator = (
state: JsonFormsCore
): FormValidator | undefined =>
state.formValidator ??
(state.validator ? compiledAjvValidator(state.validator) : undefined);

const runValidation = (
formValidator: FormValidator | undefined,
data: any,
schema: JsonSchema
): ErrorObject[] => {
if (formValidator === undefined) {
return [];
}
if (isAjvFormValidator(formValidator)) {
// AJV errors already have the stored shape; hand them over untouched.
return validate(formValidator.validateFn, data);
}
return issuesToErrors(formValidator.validate(data), schema);
};

/** The compiled AJV function behind a Form Validator, for `state.validator`. */
const legacyValidateFn = (formValidator: FormValidator | undefined) =>
isAjvFormValidator(formValidator) ? formValidator.validateFn : undefined;

export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
state = initState,
action
) => {
switch (action.type) {
case INIT: {
const thisAjv = getOrCreateAjv(state, action);

const validatorOption = getValidatorOption(state, action);
const validationMode = getValidationMode(state, action);
const v =
validationMode === 'NoValidation'
? undefined
: thisAjv.compile(action.schema);
const e = validate(v, action.data);
const formValidator = createFormValidator(
validatorOption,
thisAjv,
action.schema,
validationMode
);
const e = runValidation(formValidator, action.data, action.schema);
const additionalErrors = getAdditionalErrors(state, action);

return {
Expand All @@ -142,30 +225,37 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
uischema: action.uischema,
additionalErrors,
errors: e,
validator: v,
validator: legacyValidateFn(formValidator),
formValidator,
validatorOption,
ajv: thisAjv,
validationMode,
};
}
case UPDATE_CORE: {
const thisAjv = getOrCreateAjv(state, action);
const validatorOption = getValidatorOption(state, action);
const validationMode = getValidationMode(state, action);
let validator = state.validator;
let formValidator = currentFormValidator(state);
let errors = state.errors;
if (
state.schema !== action.schema ||
state.validationMode !== validationMode ||
state.ajv !== thisAjv
state.ajv !== thisAjv ||
state.validatorOption !== validatorOption
) {
// revalidate only if necessary
validator =
validationMode === 'NoValidation'
? undefined
: thisAjv.compile(action.schema);
errors = validate(validator, action.data);
formValidator = createFormValidator(
validatorOption,
thisAjv,
action.schema,
validationMode
);
errors = runValidation(formValidator, action.data, action.schema);
} else if (state.data !== action.data) {
errors = validate(validator, action.data);
errors = runValidation(formValidator, action.data, action.schema);
}
const validator = legacyValidateFn(formValidator);
const additionalErrors = getAdditionalErrors(state, action);

const stateChanged =
Expand All @@ -175,6 +265,8 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
state.ajv !== thisAjv ||
state.errors !== errors ||
state.validator !== validator ||
state.formValidator !== formValidator ||
state.validatorOption !== validatorOption ||
state.validationMode !== validationMode ||
state.additionalErrors !== additionalErrors;
return stateChanged
Expand All @@ -185,35 +277,49 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
uischema: action.uischema,
ajv: thisAjv,
errors: isEqual(errors, state.errors) ? state.errors : errors,
validator: validator,
validator,
formValidator,
validatorOption,
validationMode: validationMode,
additionalErrors,
}
: state;
}
case SET_AJV: {
const currentAjv = action.ajv;
const validator =
state.validationMode === 'NoValidation'
? undefined
: currentAjv.compile(state.schema);
const errors = validate(validator, state.data);
const formValidator = createFormValidator(
state.validatorOption,
currentAjv,
state.schema,
state.validationMode
);
const errors = runValidation(formValidator, state.data, state.schema);
return {
...state,
validator,
ajv: currentAjv,
validator: legacyValidateFn(formValidator),
formValidator,
errors,
};
}
case SET_SCHEMA: {
const needsNewValidator =
action.schema && state.ajv && state.validationMode !== 'NoValidation';
const v = needsNewValidator
? state.ajv.compile(action.schema)
: state.validator;
const errors = validate(v, state.data);
action.schema &&
(state.ajv || state.validatorOption) &&
state.validationMode !== 'NoValidation';
const formValidator = needsNewValidator
? createFormValidator(
state.validatorOption,
state.ajv,
action.schema,
state.validationMode
)
: currentFormValidator(state);
const errors = runValidation(formValidator, state.data, action.schema);
return {
...state,
validator: v,
validator: legacyValidateFn(formValidator),
formValidator,
schema: action.schema,
errors,
};
Expand All @@ -230,7 +336,11 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
} else if (action.path === '') {
// empty path is ok
const result = action.updater(cloneDeep(state.data));
const errors = validate(state.validator, result);
const errors = runValidation(
currentFormValidator(state),
result,
state.schema
);
return {
...state,
data: result,
Expand All @@ -253,7 +363,11 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
action.path
);
}
const errors = validate(state.validator, newState);
const errors = runValidation(
currentFormValidator(state),
newState,
state.schema
);
return {
...state,
data: newState,
Expand All @@ -272,19 +386,26 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
return state;
}
if (action.validationMode === 'NoValidation') {
const errors = validate(undefined, state.data);
return {
...state,
errors,
errors: [],
validator: undefined,
formValidator: undefined,
validationMode: action.validationMode,
};
}
if (state.validationMode === 'NoValidation') {
const validator = state.ajv.compile(state.schema);
const errors = validate(validator, state.data);
const formValidator = createFormValidator(
state.validatorOption,
state.ajv,
state.schema,
action.validationMode
);
const errors = runValidation(formValidator, state.data, state.schema);
return {
...state,
validator,
validator: legacyValidateFn(formValidator),
formValidator,
errors,
validationMode: action.validationMode,
};
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/store/jsonFormsCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ErrorObject } from 'ajv';
import { JsonSchema, UISchemaElement } from '../models';
import get from 'lodash/get';
import { errorsAt } from '../util';
import type { FormValidator } from '../util/formValidator';
import {
JsonFormsCellRendererRegistryEntry,
JsonFormsCore,
Expand Down Expand Up @@ -55,6 +56,10 @@ export const getUiSchema = (state: JsonFormsState): UISchemaElement =>
extractUiSchema(get(state, 'jsonforms.core'));
export const getAjv = (state: JsonFormsState): Ajv =>
extractAjv(get(state, 'jsonforms.core'));
/** The Form Validator currently bound to the form schema, if validation is on. */
export const getValidator = (
state: JsonFormsState
): FormValidator | undefined => extractValidator(get(state, 'jsonforms.core'));
export const getRenderers = (
state: JsonFormsState
): JsonFormsRendererRegistryEntry[] => get(state, 'jsonforms.renderers');
Expand All @@ -69,5 +74,8 @@ export const extractData = (state: JsonFormsCore) => get(state, 'data');
export const extractSchema = (state: JsonFormsCore) => get(state, 'schema');
export const extractUiSchema = (state: JsonFormsCore) => get(state, 'uischema');
export const extractAjv = (state: JsonFormsCore) => get(state, 'ajv');
export const extractValidator = (
state: JsonFormsCore
): FormValidator | undefined => get(state, 'formValidator');

export const getConfig = (state: JsonFormsState) => state.jsonforms.config;
10 changes: 10 additions & 0 deletions packages/core/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { RankedTester, UISchemaTester } from '../testers';
import { JsonSchema, UISchemaElement } from '../models';
import type Ajv from 'ajv';
import type { ErrorObject, ValidateFunction } from 'ajv';
import type { FormValidator, ValidatorOption } from '../util/formValidator';
import { JsonFormsI18nState } from './i18nTypes';

/**
Expand Down Expand Up @@ -89,9 +90,18 @@ export interface JsonFormsCore {
uischema: UISchemaElement;
errors?: ErrorObject[];
additionalErrors?: ErrorObject[];
/**
* The compiled AJV validate function behind the current Form Validator.
* Only set while AJV performs the validation; kept for backwards
* compatibility, prefer `formValidator`.
*/
validator?: ValidateFunction;
ajv?: Ajv;
validationMode?: ValidationMode;
/** The Form Validator bound to `schema`. Not consulted while validation is off. */
formValidator?: FormValidator;
/** The `validator` option in effect when a custom Form Validator is configured. */
validatorOption?: ValidatorOption;
}

export interface JsonFormsRendererRegistryEntry {
Expand Down
Loading