import cloneDeep from 'lodash/cloneDeep' ; import setFp from 'lodash/fp/set' ; import get from 'lodash/get' ; import filter from 'lodash/filter' ; import isEqual from 'lodash/isEqual' ; import isFunction from 'lodash/isFunction' ; import Ajv , { ErrorObject , ValidateFunction } from 'ajv' ; import { CoreActions , INIT , InitAction , InitActionOptions , SET_AJV , SET_SCHEMA , SET_UISCHEMA , SET_VALIDATION_MODE , UPDATE_DATA , UPDATE_ERRORS , UPDATE_CORE , UpdateCoreAction } from '../actions' ; import { createAjv , Reducer } from '../util' ; import { JsonSchema , UISchemaElement } from '../models' ; export const validate = ( validator , data ) : ErrorObject [ ] => { if ( validator === undefined ) { return [ ] ; } const valid = validator ( data ) ; if ( valid ) { return [ ] ; } return validator . errors ; } ; export type ValidationMode = 'ValidateAndShow' | 'ValidateAndHide' | 'NoValidation' ; export interface JsonFormsCore { data : any ; schema : JsonSchema ; uischema : UISchemaElement ; errors ? : ErrorObject [ ] ; validator ? : ValidateFunction ; ajv ? : Ajv ; validationMode ? : ValidationMode ; } const initState = { data : { } , schema : { } , uischema : undefined , errors : [ ] , validator : undefined , ajv : undefined , validationMode : 'ValidateAndShow' , } ; const reuseAjvForSchema = ( ajv , schema ) : Ajv => { if ( schema . hasOwnProperty ( 'id' ) || schema . hasOwnProperty ( '$id' ) ) { ajv . removeSchema ( schema ) ; } return ajv ; } ; const getOrCreateAjv = ( state , action ? ) : Ajv => { if ( action ) { if ( hasAjvOption ( action . options ) ) { return action . options . ajv ; } else if ( action . options !== undefined ) { if ( isFunction ( action . options . compile ) ) { return action . options ; } } } if ( state . ajv ) { return action ?. schema ? reuseAjvForSchema ( state . ajv , action . schema ) : state . ajv ; } return createAjv ( ) ; } ; const hasAjvOption = ( option ) : option is InitActionOptions => { if ( option ) { return option . ajv !== undefined ; } return false ; } ; const getValidationMode = ( state , action ? ) : ValidationMode => { if ( action && hasValidationModeOption ( action . options ) ) { return action . options . validationMode ; } return state . validationMode ; } ; const hasValidationModeOption = ( option ) : option is InitActionOptions => { if ( option ) { return option . validationMode !== undefined ; } return false ; } ; export const coreReducer = ( state = initState , action ) => { switch ( action . type ) { case INIT : { const thisAjv = getOrCreateAjv ( state , action ) ; const validationMode = getValidationMode ( state , action ) ; const v = validationMode === 'NoValidation' ? undefined : thisAjv . compile ( action . schema ) ; const e = validate ( v , action . data ) ; return { ... state , data : action . data , schema : action . schema , uischema : action . uischema , errors : e , validator : v , ajv : thisAjv , validationMode , } ; } case UPDATE_CORE : { const thisAjv = getOrCreateAjv ( state , action ) ; const validationMode = getValidationMode ( state , action ) ; let validator = state . validator ; let errors = state . errors ; if ( state . schema !== action . schema || state . validationMode !== validationMode || state . ajv !== thisAjv ) { validator = validationMode === 'NoValidation' ? undefined : thisAjv . compile ( action . schema ) ; errors = validate ( validator , action . data ) ; } else if ( state . data !== action . data ) { errors = validate ( validator , action . data ) ; } const stateChanged = state . data !== action . data || state . schema !== action . schema || state . uischema !== action . uischema || state . ajv !== thisAjv || state . errors !== errors || state . validator !== validator || state . validationMode !== validationMode ; return stateChanged ? { ... state , data : action . data , schema : action . schema , uischema : action . uischema , ajv : thisAjv , errors : isEqual ( errors , state . errors ) ? state . errors : errors , validator : validator , validationMode : validationMode , } : state ; } case SET_AJV : { const currentAjv = action . ajv ; const validator = state . validationMode === 'NoValidation' ? undefined : currentAjv . compile ( state . schema ) ; const errors = validate ( validator , state . data ) ; return { ... state , validator , errors } ; } case SET_SCHEMA : { const needsNewValidator = action . schema && state . ajv && state . validationMode !== 'NoValidation' ; const v = needsNewValidator ? reuseAjvForSchema ( state . ajv , action . schema ) . compile ( action . schema ) : state . validator ; const errors = validate ( v , state . data ) ; return { ... state , validator : v , schema : action . schema , errors } ; } case SET_UISCHEMA : { return { ... state , uischema : action . uischema } ; } case UPDATE_DATA : { if ( action . path === undefined || action . path === null ) { return state ; } else if ( action . path === '' ) { const result = action . updater ( cloneDeep ( state . data ) ) ; const errors = validate ( state . validator , result ) ; return { ... state , data : result , errors } ; } else { const oldData = get ( state . data , action . path ) ; const newData = action . updater ( cloneDeep ( oldData ) ) ; const newState = setFp ( action . path , newData , state . data === undefined ? { } : state . data ) ; const errors = validate ( state . validator , newState ) ; return { ... state , data : newState , errors } ; } } case UPDATE_ERRORS : { return { ... state , errors : action . errors } ; } case SET_VALIDATION_MODE : { if ( state . validationMode === action . validationMode ) { return state ; } if ( action . validationMode === 'NoValidation' ) { const errors = validate ( undefined , state . data ) ; return { ... state , errors , validationMode : action . validationMode } ; } if ( state . validationMode === 'NoValidation' ) { const validator = reuseAjvForSchema ( state . ajv , state . schema ) . compile ( state . schema ) ; const errors = validate ( validator , state . data ) ; return { ... state , validator , errors , validationMode : action . validationMode } ; } return { ... state , validationMode : action . validationMode } ; } default : return state ; } } ; export const extractData = ( state ) => get ( state , 'data' ) ; export const extractSchema = ( state ) => get ( state , 'schema' ) ; export const extractUiSchema = ( state ) => get ( state , 'uischema' ) ; export const extractAjv = ( state ) => get ( state , 'ajv' ) ; const getInvalidProperty = ( error ) : string | undefined => { switch ( error . keyword ) { case 'required' : case 'dependencies' : return error . params . missingProperty ; case 'additionalProperties' : return error . params . additionalProperty ; default : return undefined ; } } ; export const getControlPath = ( error ) => { const dataPath = ( error as any ) . dataPath ; if ( dataPath ) { return dataPath . replace ( /\//g , '.' ) . substr ( 1 ) ; } var controlPath = error . instancePath ; controlPath = controlPath . replace ( /\//g , '.' ) ; const invalidProperty = getInvalidProperty ( error ) ; if ( invalidProperty !== undefined && ! controlPath . endsWith ( invalidProperty ) ) { controlPath = `${ controlPath }.${ invalidProperty }` ; } controlPath = controlPath . replace ( /^./ , '' ) ; return controlPath ; } ; export const errorsAt = ( instancePath , schema , matchPath ) => ( errors ) : ErrorObject [ ] => { const combinatorPaths = filter ( errors , error => error . keyword === 'oneOf' || error . keyword === 'anyOf' ) . map ( error => getControlPath ( error ) ) ; return filter ( errors , error => { if ( filteredErrorKeywords . indexOf ( error . keyword ) !== - 1 ) { return false ; } const controlPath = getControlPath ( error ) ; let result = matchPath ( controlPath ) ; const parentSchema = error . parentSchema ; if ( result && ! isObjectSchema ( parentSchema ) && combinatorPaths . findIndex ( p => instancePath . startsWith ( p ) ) !== - 1 ) { result = result && isEqual ( parentSchema , schema ) ; } return result ; } ) ; } ; returns const isObjectSchema = ( schema ? ) : boolean => { return schema ?. type === 'object' || ! ! schema ?. properties ; } ; /**
 * The error-type of an AJV error is defined by its `keyword` property.
 * Certain errors are filtered because they don't fit to any rendered control.
 * All of them have in common that we don't want to show them in the UI
 * because controls will show the actual reason why they don't match their correponding sub schema.
 * - additionalProperties: Indicates that a property is present that is not defined in the schema.
 *      Jsonforms only allows to edit defined properties. These errors occur if an oneOf doesn't match.
 * - allOf: Indicates that not all of the allOf definitions match as a whole.
 * - anyOf: Indicates that an anyOf definition itself is not valid because none of its subschemas matches.
 * - oneOf: Indicates that an oneOf definition itself is not valid because not exactly one of its subschemas matches.
 */ const filteredErrorKeywords = [ 'additionalProperties' , 'allOf' , 'anyOf' , 'oneOf' ] ; const getErrorsAt = ( instancePath , schema , matchPath ) => ( state ) : ErrorObject [ ] => errorsAt ( instancePath , schema , matchPath ) ( state . validationMode === 'ValidateAndHide' ? [ ] : state . errors ) ; export const errorAt = ( instancePath , schema ) => getErrorsAt ( instancePath , schema , path => path === instancePath ) ; export const subErrorsAt = ( instancePath , schema ) => getErrorsAt ( instancePath , schema , path => path . startsWith ( instancePath ) ) ;