1import Ajv from 'ajv';
2import i18n from '../i18n';
3import retrieveSchema from './retriev';
4
5import {
6    isObject, deepEquals
7} from '../utils';
8import { getUserErrOptions } from '../formUtils';
9
10let ajv = createAjvInstance();
11
12let formerCustomFormats = null;
13let formerMetaSchema = null;
14
15// 创建实例
16function createAjvInstance() {
17    const ajvInstance = new Ajv({
18        errorDataPath: 'property',
19        allErrors: true,
20        multipleOfPrecision: 8,
21        schemaId: 'auto',
22        unknownFormats: 'ignore',
23    });
24
25    // 添加base-64 format
26    ajvInstance.addFormat(
27        'data-url',
28        /^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/
29    );
30
31    // 添加color format
32    ajvInstance.addFormat(
33        'color',
34        // eslint-disable-next-line max-len
35        /^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/
36    );
37    return ajvInstance;
38}
39
40/**
41 * 将错误输出从ajv转换为jsonschema使用的格式
42 * At some point, components should be updated to support ajv.
43 */
44function transformAjvErrors(errors = []) {
45    if (errors === null) {
46        return [];
47    }
48
49    return errors.map((e) => {
50        const {
51            dataPath, keyword, message, params, schemaPath
52        } = e;
53        const property = `${dataPath}`;
54
55        // put data in expected format
56        return {
57            name: keyword,
58            property,
59            message,
60            params, // specific to ajv
61            stack: `${property} ${message}`.trim(),
62            schemaPath,
63        };
64    });
65}
66
67/**
68 * 通过 schema校验formData并返回错误信息
69 * @param formData 校验的数据
70 * @param schema
71 * @param transformErrors function - 转换错误, 如个性化的配置
72 * @param additionalMetaSchemas 数组 添加 ajv metaSchema
73 * @param customFormats 添加 ajv 自定义 formats
74 * @returns {{errors: ([]|{stack: string, schemaPath: *, name: *, property: string, message: *, params: *}[])}}
75 */
76export function ajvValidateFormData({
77    formData,
78    schema,
79    transformErrors,
80    additionalMetaSchemas = [],
81    customFormats = {}
82} = {}) {
83    const hasNewMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas);
84    const hasNewFormats = !deepEquals(formerCustomFormats, customFormats);
85
86    // 变更了 Meta或者调整了format配置重置新的实例
87    if (hasNewMetaSchemas || hasNewFormats) {
88        ajv = createAjvInstance();
89    }
90
91    // 添加更多要验证的模式
92    if (
93        additionalMetaSchemas
94        && hasNewMetaSchemas
95        && Array.isArray(additionalMetaSchemas)
96    ) {
97        ajv.addMetaSchema(additionalMetaSchemas);
98        formerMetaSchema = additionalMetaSchemas;
99    }
100
101    // 注册自定义的 formats - 没有变更只会注册一次 - 否则重新创建实例
102    if (customFormats && hasNewFormats && isObject(customFormats)) {
103        Object.keys(customFormats).forEach((formatName) => {
104            ajv.addFormat(formatName, customFormats[formatName]);
105        });
106
107        formerCustomFormats = customFormats;
108    }
109
110    let validationError = null;
111    try {
112        ajv.validate(schema, formData);
113    } catch (err) {
114        validationError = err;
115    }
116
117    // ajv 默认多语言处理
118    i18n.getCurrentLocalize()(ajv.errors);
119
120    let errors = transformAjvErrors(ajv.errors);
121
122    // 清除错误
123    ajv.errors = null;
124
125    // 处理异常
126    const noProperMetaSchema = validationError
127        && validationError.message
128        && typeof validationError.message === 'string'
129        && validationError.message.includes('no schema with key or ref ');
130
131    if (noProperMetaSchema) {
132        errors = [
133            ...errors,
134            {
135                stack: validationError.message,
136            },
137        ];
138    }
139
140    // 转换错误, 如传入自定义的错误
141    if (typeof transformErrors === 'function') {
142        errors = transformErrors(errors);
143    }
144
145    return {
146        errors
147    };
148}
149
150// 校验formData 并转换错误信息
151export function validateFormDataAndTransformMsg({
152    formData,
153    schema,
154    uiSchema,
155    transformErrors,
156    additionalMetaSchemas = [],
157    customFormats = {},
158    errorSchema = {},
159    required = false,
160    propPath = '',
161    isOnlyFirstError = true, // 只取第一条错误信息
162} = {}) {
163    // 是否过滤根节点错误 固定只能根
164    const filterRootNodeError = true;
165
166    // 校验required信息 isEmpty 校验
167    // 如果数组类型针对配置了 format 的特殊处理
168    const emptyArray = (schema.type === 'array' && Array.isArray(formData) && formData.length === 0);
169    const isEmpty = formData === undefined || emptyArray;
170
171    if (required) {
172        if (isEmpty) {
173            const requireErrObj = {
174                keyword: 'required',
175                params: {
176                    missingProperty: propPath
177                }
178            };
179
180            // 用户设置校验信息
181            const errSchemaMsg = getUserErrOptions({
182                schema,
183                uiSchema,
184                errorSchema
185            }).required;
186            if (errSchemaMsg) {
187                requireErrObj.message = errSchemaMsg;
188            } else {
189                // 处理多语言require提示信息 (ajv 修改原引用)
190                i18n.getCurrentLocalize()([requireErrObj]);
191            }
192            return [requireErrObj];
193        }
194    } else if (isEmpty && !emptyArray) {
195        // 非required 为空 校验通过
196        return [];
197    }
198
199    // 校验ajv错误信息
200    let ajvErrors = ajvValidateFormData({
201        formData,
202        schema,
203        transformErrors,
204        additionalMetaSchemas,
205        customFormats,
206    }).errors;
207
208    // 过滤顶级错误
209    if (filterRootNodeError) {
210        ajvErrors = ajvErrors.filter(
211            item => (item.property === ''
212                && (!item.schemaPath.includes('#/anyOf/') && !item.schemaPath.includes('#/oneOf/')))
213            || item.name === 'additionalProperties'
214        );
215    }
216
217    const userErrOptions = getUserErrOptions({
218        schema,
219        uiSchema,
220        errorSchema
221    });
222
223    return (isOnlyFirstError && ajvErrors.length > 0 ? [ajvErrors[0]] : ajvErrors).reduce((preErrors, errorItem) => {
224        // 优先获取 errorSchema 配置
225        errorItem.message = userErrOptions[errorItem.name] !== undefined ? userErrOptions[errorItem.name] : errorItem.message;
226        preErrors.push(errorItem);
227        return preErrors;
228    }, []);
229}
230
231/**
232 * 根据模式验证数据,如果数据有效则返回true,否则返回* false。如果模式无效,那么这个函数将返回* false。
233 * @param schema
234 * @param data
235 * @returns {boolean|PromiseLike<any>}
236 */
237export function isValid(schema, data) {
238    try {
239        return ajv.validate(schema, data);
240    } catch (e) {
241        return false;
242    }
243}
244
245// ajv valida
246export function ajvValid(schema, data) {
247    return ajv.validate(schema, data);
248}
249
250// 如果查找不到
251// return -1
252export function getMatchingIndex(formData, options, rootSchema, haveAllFields = false) {
253    // eslint-disable-next-line no-plusplus
254    for (let i = 0; i < options.length; i++) {
255        const option = retrieveSchema(options[i], rootSchema, formData);
256
257        // If the schema describes an object then we need to add slightly more
258        // strict matching to the schema, because unless the schema uses the
259        // "requires" keyword, an object will match the schema as long as it
260        // doesn't have matching keys with a conflicting type. To do this we use an
261        // "anyOf" with an array of requires. This augmentation expresses that the
262        // schema should match if any of the keys in the schema are present on the
263        // object and pass validation.
264        if (option.properties) {
265            // Create an "anyOf" schema that requires at least one of the keys in the
266            // "properties" object
267            const requiresAnyOf = {
268                // 如果后代节点存在 $ref 需要正常引用
269                ...(rootSchema.definitions ? {
270                    definitions: rootSchema.definitions
271                } : {}),
272                anyOf: Object.keys(option.properties).map(key => ({
273                    required: [key],
274                })),
275            };
276
277            let augmentedSchema;
278
279            // If the "anyOf" keyword already exists, wrap the augmentation in an "allOf"
280            if (option.anyOf) {
281                // Create a shallow clone of the option
282                const { ...shallowClone } = option;
283
284                if (!shallowClone.allOf) {
285                    shallowClone.allOf = [];
286                } else {
287                    // If "allOf" already exists, shallow clone the array
288                    shallowClone.allOf = shallowClone.allOf.slice();
289                }
290
291                shallowClone.allOf.push(requiresAnyOf);
292
293                augmentedSchema = shallowClone;
294            } else {
295                augmentedSchema = Object.assign({}, option, requiresAnyOf);
296            }
297
298            // Remove the "required" field as it's likely that not all fields have
299            // been filled in yet, which will mean that the schema is not valid
300
301            // 如果编辑回填数据的场景 可直接使用 required 判断
302            if (!haveAllFields) delete augmentedSchema.required;
303
304
305            if (isValid(augmentedSchema, formData)) {
306                return i;
307            }
308        } else if (isValid(options[i], formData)) {
309            return i;
310        }
311    }
312
313    // 尝试查找const 配置
314    if (options[0] && options[0].properties) {
315        const constProperty = Object.keys(options[0].properties).find(k => options[0].properties[k].const);
316        if (constProperty) {
317            // eslint-disable-next-line no-plusplus
318            for (let i = 0; i < options.length; i++) {
319                if (
320                    options[i].properties
321                    && options[i].properties[constProperty]
322                    && options[i].properties[constProperty].const === formData[constProperty]) {
323                    return i;
324                }
325            }
326        }
327    }
328    return -1;
329}
330
331// oneOf anyOf 通过formData的值来找到当前匹配项索引
332export function getMatchingOption(formData, options, rootSchema, haveAllFields = false) {
333    const index = getMatchingIndex(formData, options, rootSchema, haveAllFields);
334    return index === -1 ? 0 : index;
335}
336