import { Schema , SchemaTypes , VueJsonSchemaConfig } from './types' ; import jsonpointer from 'jsonpointer' ; import union from 'lodash.union' ; import mergeAllOf from 'json-schema-merge-allof' ; import { validateData } from './validator' ; export function optionsList ( schema ) { if ( schema . enum ) { return schema . enum . map ( ( value , i ) => { const key = ( schema . enumNames && schema . enumNames [ i ] ) || String ( value ) ; return { key , value } ; } ) ; } return [ ] ; } export function resolveSchema ( schema , rootSchema = { } , formData = { } ) { if ( hasOwnProperty ( schema , '$ref' ) ) { return resolveReference ( schema , rootSchema , formData ) ; } else if ( hasOwnProperty ( schema , 'dependencies' ) ) { const resolvedSchema = resolveDependencies ( schema , rootSchema , formData ) ; return retrieveSchema ( resolvedSchema , rootSchema , formData ) ; } else if ( hasOwnProperty ( schema , 'allOf' ) && Array . isArray ( schema . allOf ) ) { return { ... schema , allOf : ( schema . allOf as any ) . map ( ( allOfSubschema ) => retrieveSchema ( allOfSubschema , rootSchema , formData ) ) , } ; } else { return schema ; } } export function retrieveSchema ( schema , rootSchema = { } , formData = { } ) { if ( ! isObject ( schema ) ) { return { } as Schema ; } let resolvedSchema = resolveSchema ( schema , rootSchema , formData ) ; if ( 'allOf' in schema ) { try { resolvedSchema = mergeAllOf ( { ... resolvedSchema , allOf : resolvedSchema . allOf , } as any ) as Schema ; } catch ( e ) { console . warn ( 'could not merge subschemas in allOf:\n' + e ) ; const { allOf , ... resolvedSchemaWithoutAllOf } = resolvedSchema ; return resolvedSchemaWithoutAllOf ; } } const hasAdditionalProperties = resolvedSchema . hasOwnProperty ( 'additionalProperties' ) && resolvedSchema . additionalProperties !== false ; if ( hasAdditionalProperties ) { return stubExistingAdditionalProperties ( resolvedSchema , rootSchema , formData ) ; } return resolvedSchema ; } export const ADDITIONAL_PROPERTY_FLAG = '__additional_property' ; export function stubExistingAdditionalProperties ( schema , rootSchema = { } , formData = { } ) { schema = { ... schema , properties : { ... schema . properties } , } as any ; Object . keys ( formData ) . forEach ( key => { if ( ( schema as any ) . properties . hasOwnProperty ( key ) ) { return ; } let additionalProperties ; if ( schema . additionalProperties ! . hasOwnProperty ( '$ref' ) ) { additionalProperties = retrieveSchema ( { $ref : ( schema . additionalProperties as any ) . $ref } , rootSchema , formData ) ; } else if ( schema . additionalProperties ! . hasOwnProperty ( 'type' ) ) { additionalProperties = { ... ( schema . additionalProperties as any ) } ; } else { additionalProperties = { type : guessType ( formData [ key ] ) } ; } ; ( schema as any ) . properties [ key ] = additionalProperties ; ( schema as any ) . properties [ key ] [ ADDITIONAL_PROPERTY_FLAG ] = true ; } ) ; return schema ; } function resolveReference ( schema , rootSchema , formData ) { const $refSchema = findSchemaDefinition ( schema . $ref , rootSchema ) ; const { $ref , ... localSchema } = schema ; return retrieveSchema ( { ... $refSchema , ... localSchema } , rootSchema , formData ) ; } export function findSchemaDefinition ( $ref , rootSchema = { } ) { const origRef = $ref ; if ( $ref . startsWith ( '#' ) ) { $ref = decodeURIComponent ( $ref . substring ( 1 ) ) ; } else { throw new Error ( `Could not find a definition for ${ origRef }.` ) ; } const current = jsonpointer . get ( rootSchema , $ref ) ; if ( current === undefined ) { throw new Error ( `Could not find a definition for ${ origRef }.` ) ; } if ( hasOwnProperty ( current , '$ref' ) ) { return findSchemaDefinition ( current . $ref , rootSchema ) ; } return current ; } function resolveDependencies ( schema , rootSchema , formData ) { let { dependencies = { } , ... resolvedSchema } = schema ; if ( 'oneOf' in resolvedSchema ) { resolvedSchema = resolvedSchema . oneOf [ getMatchingOption ( formData , resolvedSchema . oneOf , rootSchema ) ] ; } else if ( 'anyOf' in resolvedSchema ) { resolvedSchema = resolvedSchema . anyOf [ getMatchingOption ( formData , resolvedSchema . anyOf , rootSchema ) ] ; } return processDependencies ( dependencies , resolvedSchema , rootSchema , formData ) ; } function processDependencies ( dependencies , resolvedSchema , rootSchema , formData ) { for ( const dependencyKey in dependencies ) { if ( formData [ dependencyKey ] === undefined ) { continue ; } if ( resolvedSchema . properties && ! ( dependencyKey in resolvedSchema . properties ) ) { continue ; } const { [ dependencyKey ] : dependencyValue , ... remainingDependencies } = dependencies ; if ( Array . isArray ( dependencyValue ) ) { resolvedSchema = withDependentProperties ( resolvedSchema , dependencyValue ) ; } else if ( isObject ( dependencyValue ) ) { resolvedSchema = withDependentSchema ( resolvedSchema , rootSchema , formData , dependencyKey , dependencyValue ) ; } return processDependencies ( remainingDependencies , resolvedSchema , rootSchema , formData ) ; } return resolvedSchema ; } function withDependentProperties ( schema , additionallyRequired ) { if ( ! additionallyRequired ) { return schema ; } const required = Array . isArray ( schema . required ) ? Array . from ( new Set ( [ ... schema . required , ... additionallyRequired ] ) ) : additionallyRequired ; return { ... schema , required : required } ; } function withDependentSchema ( schema , rootSchema , formData , dependencyKey , dependencyValue ) { let { oneOf , ... dependentSchema } = retrieveSchema ( dependencyValue , rootSchema , formData ) ; schema = mergeSchemas ( schema , dependentSchema ) ; if ( oneOf === undefined ) { return schema ; } else if ( ! Array . isArray ( oneOf ) ) { throw new Error ( `invalid: it is some ${ typeof oneOf } instead of an array` ) ; } const resolvedOneOf = ( oneOf as any ) . map ( ( subschema ) => hasOwnProperty ( subschema , '$ref' ) ? resolveReference ( subschema , rootSchema , formData ) : subschema ) ; return withExactlyOneSubschema ( schema , rootSchema , formData , dependencyKey , resolvedOneOf ) ; } function withExactlyOneSubschema ( schema , rootSchema , formData , dependencyKey , oneOf ) { const validSubschemas = oneOf . filter ( ( subschema ) => { if ( ! subschema . properties ) { return false ; } const { [ dependencyKey ] : conditionPropertySchema } = subschema . properties ; if ( conditionPropertySchema ) { const conditionSchema = { type : 'object' , properties : { [ dependencyKey ] : conditionPropertySchema , } , } ; const { errors } = validateData ( conditionSchema , formData ) ; return ! errors || errors . length === 0 ; } } ) ; if ( validSubschemas . length !== 1 ) { console . warn ( "ignoring oneOf in dependencies because there isn't exactly one subschema that is valid" ) ; return schema ; } const subschema = validSubschemas [ 0 ] ; const { [ dependencyKey ] : conditionPropertySchema , ... dependentSubschema } = subschema . properties ; const dependentSchema = { ... subschema , properties : dependentSubschema } ; return mergeSchemas ( schema , retrieveSchema ( dependentSchema , rootSchema , formData ) ) ; } export function mergeSchemas ( obj1 , obj2 ) { var acc = Object . assign ( { } , obj1 ) ; return Object . keys ( obj2 ) . reduce ( ( acc , key ) => { const left = obj1 ? obj1 [ key ] : { } , right = obj2 [ key ] ; if ( obj1 && hasOwnProperty ( obj1 , key ) && isObject ( right ) ) { acc [ key ] = mergeSchemas ( left , right ) ; } else if ( obj1 && obj2 && ( getSchemaType ( obj1 ) === 'object' || getSchemaType ( obj2 ) === 'object' ) && key === 'required' && Array . isArray ( left ) && Array . isArray ( right ) ) { acc [ key ] = union ( left , right ) ; } else { acc [ key ] = right ; } return acc ; } , acc ) ; } export function getVJSFConfig ( schema , uiSchema ) { if ( uiSchema ) return uiSchema ; return schema . vjsf || { } ; } export function isObject ( thing ) { return typeof thing === 'object' && thing !== null && ! Array . isArray ( thing ) ; } export function isEmptyObject ( thing ) { return isObject ( thing ) && Object . keys ( thing ) . length === 0 ; } export function hasOwnProperty ( obj , key ) { /**
     * 直接调用`obj.hasOwnProperty`有可能会因为
     * obj 覆盖了 prototype 上的 hasOwnProperty 而产生错误
     */ return Object . prototype . hasOwnProperty . call ( obj , key ) ; } export function getSchemaType ( schema ) { let { type } = schema ; if ( ! type && schema . const ) { return guessType ( schema . const ) ; } if ( ! type && schema . enum ) { return 'string' ; } if ( ! type && ( schema . properties || schema . additionalProperties ) ) { return 'object' ; } const t = type ; if ( t instanceof Array && t . length === 2 && t . includes ( 'null' ) ) { return t . find ( type => type !== 'null' ) ; } return type as any ; } export const guessType = function guessType ( value ) { if ( Array . isArray ( value ) ) { return 'array' ; } else if ( typeof value === 'string' ) { return 'string' ; } else if ( value == null ) { return 'null' ; } else if ( typeof value === 'boolean' ) { return 'boolean' ; } else if ( ! isNaN ( value ) ) { return 'number' ; } else if ( typeof value === 'object' ) { return 'object' ; } return 'string' ; } ; export function isConstant ( schema ) { return ( ( Array . isArray ( schema . enum ) && schema . enum . length === 1 ) || schema . hasOwnProperty ( 'const' ) ) ; } export function isSelect ( _schema , rootSchema = { } ) { const schema = retrieveSchema ( _schema , rootSchema ) ; const altSchemas = schema . oneOf || schema . anyOf ; if ( Array . isArray ( schema . enum ) ) { return true ; } else if ( Array . isArray ( altSchemas ) ) { return ( altSchemas as any ) . every ( ( as ) => isConstant ( as ) ) ; } return false ; } export function isMultiSelect ( schema , rootSchema = { } ) { if ( ! schema . uniqueItems || ! schema . items ) { return false ; } return isSelect ( schema . items , rootSchema ) ; } export function getMatchingOption ( formData , options , isValid ) { for ( let i = 0 ; i < options . length ; i ++ ) { const option = options [ i ] ; if ( option . properties ) { const requiresAnyOf = { anyOf : Object . keys ( option . properties ) . map ( key => ( { required : [ key ] , } ) ) , } ; let augmentedSchema ; if ( option . anyOf ) { const { ... shallowClone } = option ; if ( ! shallowClone . allOf ) { shallowClone . allOf = [ ] ; } else { shallowClone . allOf = shallowClone . allOf . slice ( ) ; } shallowClone . allOf . push ( requiresAnyOf ) ; augmentedSchema = shallowClone ; } else { augmentedSchema = Object . assign ( { } , option , requiresAnyOf ) ; } delete augmentedSchema . required ; if ( isValid ( augmentedSchema , formData ) ) { return i ; } } else if ( isValid ( options [ i ] , formData ) ) { return i ; } } return 0 ; } export function mergeDefaultsWithFormData ( defaults , formData ) { if ( Array . isArray ( formData ) ) { if ( ! Array . isArray ( defaults ) ) { defaults = [ ] ; } return formData . map ( ( value , idx ) => { if ( defaults [ idx ] ) { return mergeDefaultsWithFormData ( defaults [ idx ] , value ) ; } return value ; } ) ; } else if ( isObject ( formData ) ) { const acc = Object . assign ( { } , defaults ) ; return Object . keys ( formData ) . reduce ( ( acc , key ) => { acc [ key ] = mergeDefaultsWithFormData ( defaults ? defaults [ key ] : { } , formData [ key ] ) ; return acc ; } , acc ) ; } else { return formData ; } } export function getDefaultFormState ( _schema , formData ) { if ( ! isObject ( _schema ) ) { throw new Error ( 'Invalid schema: ' + _schema ) ; } const defaults = _schema . default ; if ( typeof formData === 'undefined' ) { return defaults ; } if ( isObject ( formData ) || Array . isArray ( formData ) ) { return mergeDefaultsWithFormData ( defaults , formData ) ; } if ( formData === 0 || formData === false || formData === '' ) { return formData ; } return formData || defaults ; }