Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: update the validation for new schema #1988

Merged
merged 3 commits into from
Feb 13, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
79 changes: 41 additions & 38 deletions Composer/packages/lib/indexers/src/dialogUtils/dialogChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
// Licensed under the MIT License.

import get from 'lodash/get';
import { ExpressionEngine } from 'botframework-expressions';
import formatMessage from 'format-message';
import { SDKTypes, FieldNames } from '@bfc/shared';
import { FieldNames } from '@bfc/shared';

import { Diagnostic } from '../diagnostic';

import { ExpressionType } from './validation';
import { CheckerFunc } from './types';

const ExpressionParser = new ExpressionEngine();
import { validate } from './validation';

const createPath = (path: string, type: string): string => {
const steps = [FieldNames.Events, FieldNames.Actions, FieldNames.ElseActions];
Expand All @@ -24,27 +22,7 @@ const createPath = (path: string, type: string): string => {
return `${list[0]}${focused}#${type}#${list[1]}`;
};

export const checkExpression = (exp: string, required: boolean, path: string, type: string): Diagnostic | null => {
let message = '';
if (!exp && required) {
message = formatMessage(`is missing or empty`);
} else {
try {
ExpressionParser.parse(exp);
} catch (error) {
message = `${formatMessage('must be an expression:')} ${error})`;
}
}
if (message) {
const diagnostic = new Diagnostic(message, '');
diagnostic.path = createPath(path, type);
return diagnostic;
}

return null;
};

function findAllRequiredType(schema: any): { [key: string]: boolean } {
function findAllRequiredProperties(schema: any): { [key: string]: boolean } {
if (!schema) return {};
const types = schema.anyOf?.filter(x => x.title === 'Type');
const required = {};
Expand All @@ -62,17 +40,38 @@ function findAllRequiredType(schema: any): { [key: string]: boolean } {
return required;
}

function findAllTypes(schema: any): string[] {
if (!schema) return [];
let types: string[] = [];
if (schema.type) {
if (Array.isArray(schema.type)) {
types = [...types, ...schema.type];
} else {
types.push(schema.type);
}
} else {
types = schema.oneOf?.filter(item => !!ExpressionType[item.type]).map(item => item.type);
}

return types;
}

export const IsExpression: CheckerFunc = (path, value, type, schema) => {
if (!schema) return [];
const diagnostics: Diagnostic[] = [];
const requiredTypes = findAllRequiredType(schema);
const requiredProperties = findAllRequiredProperties(schema);
Object.keys(value).forEach(key => {
const property = value[key];
if (Array.isArray(property)) {
const itemsSchema = get(schema, ['properties', key, 'items'], null);
if (itemsSchema?.$role === 'expression') {
property.forEach((child, index) => {
const diagnostic = checkExpression(child, !!requiredTypes[key], `${path}.${key}[${index}]`, type);
const diagnostic = validate(
child,
!!requiredProperties[key],
createPath(`${path}.${key}[${index}]`, type),
findAllTypes(itemsSchema)
);
if (diagnostic) diagnostics.push(diagnostic);
});
} else if (itemsSchema?.type === 'object') {
Expand All @@ -82,24 +81,28 @@ export const IsExpression: CheckerFunc = (path, value, type, schema) => {
});
}
} else if (get(schema.properties[key], '$role') === 'expression') {
const diagnostic = checkExpression(property, !!requiredTypes[key], `${path}.${key}`, type);
const diagnostic = validate(
property,
!!requiredProperties[key],
createPath(`${path}.${key}`, type),
findAllTypes(schema.properties[key])
);
if (diagnostic) diagnostics.push(diagnostic);
}
});
return diagnostics;
};

//the type of 'Microsoft.ChoiceInput' has anyof schema in choices
export const checkChoices: CheckerFunc = (path, value, type, schema) => {
const choices = value.choices;
if (typeof choices === 'string') {
const diagnostic = checkExpression(choices, false, `${path}.choices`, type);
if (diagnostic) return [diagnostic];
}
return null;
};
// export const checkChoices: CheckerFunc = (path, value, type) => {
// const choices = value.choices;
// if (typeof choices === 'string') {
// const diagnostic = checkExpression(choices, false, `${path}.choices`, type);
// if (diagnostic) return [diagnostic];
// }
// return null;
// };

export const checkerFuncs: { [type: string]: CheckerFunc[] } = {
'.': [IsExpression], //this will check all types
[SDKTypes.ChoiceInput]: [checkChoices],
};
78 changes: 78 additions & 0 deletions Composer/packages/lib/indexers/src/dialogUtils/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { ExpressionEngine, ReturnType } from 'botframework-expressions';
import formatMessage from 'format-message';

import { Diagnostic } from '../diagnostic';

export const ExpressionType = {
number: 'number',
integer: 'integer',
boolean: 'boolean',
string: 'string',
};

const ExpressionParser = new ExpressionEngine();

const isExpression = (value: string | boolean | number, types: string[]): boolean => {
if (typeof value === 'string' && value[0] === '=') return true;
//StringExpression always assumes string interpolation unless prefixed with =, producing a string
if (types.length === 1 && types[0] === ExpressionType.string) return false;
return true;
};

//The return type should match the schema type
const checkReturnType = (returnType: ReturnType, types: string[]): string => {
if (types.indexOf(returnType) > -1) return '';
if (returnType === ReturnType.Number && types.indexOf(ExpressionType.integer) > -1) {
return '';
}
return formatMessage('the expression type is not match');
};

export const checkExpression = (exp: string | boolean | number, required: boolean, types: string[]): string => {
let message = '';
if (!exp && required) {
message = formatMessage(`is missing or empty`);
} else {
try {
let returnType: ReturnType;
if (typeof exp === 'boolean') {
returnType = ReturnType.Boolean;
} else if (typeof exp === 'number') {
returnType = ReturnType.Number;
} else {
returnType = ExpressionParser.parse(exp).returnType;
}
message = checkReturnType(returnType, types);
} catch (error) {
message = `${formatMessage('must be an expression:')} ${error})`;
}
}

return message;
};

export const validate = (
value: string | boolean | number,
required: boolean,
path: string,
types: string[]
): Diagnostic | null => {
//if there is no type do nothing
if (types.length === 0) return null;

if (!isExpression(value, types)) return null;

//remove '='
if (typeof value === 'string' && value[0] === '=') {
value = value.substring(1);
}

const message = checkExpression(value, required, types);
if (!message) return null;

const diagnostic = new Diagnostic(message, '');
diagnostic.path = path;
return diagnostic;
};