Skip to content

Commit

Permalink
fix(cli): improve validation help message (#9725)
Browse files Browse the repository at this point in the history
* fix: rename list capabilities to templates

* fix: question

* fix: validation

* refactor: move validation code from api into fx-core

* fix: launch.json

* fix: error

* test: ut

* test: ut
  • Loading branch information
jayzhang authored Aug 25, 2023
1 parent e1ef8a0 commit 28597af
Show file tree
Hide file tree
Showing 26 changed files with 771 additions and 1,074 deletions.
2 changes: 1 addition & 1 deletion packages/api/.nycrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"include": ["src/**/*.ts", "src/**/*.js"],
"reporter": ["text", "html", "cobertura", "lcov"],
"check-coverage": true,
"lines": 90
"lines": 75
}
26 changes: 1 addition & 25 deletions packages/api/review/teamsfx-api.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,6 @@ export interface FxError extends Error {
userData?: any;
}

// @public
export function getValidationFunction<T extends string | string[] | undefined>(validation: ValidationSchema, inputs: Inputs): (input: T) => string | undefined | Promise<string | undefined>;

// @public
export interface Group {
// (undocumented)
Expand Down Expand Up @@ -404,7 +401,7 @@ export interface IProgressHandler {
start: (detail?: string) => Promise<void>;
}

// @public (undocumented)
// @public
export interface IQTreeNode {
// (undocumented)
children?: IQTreeNode[];
Expand Down Expand Up @@ -615,24 +612,6 @@ export enum Platform {
// @public (undocumented)
export const ProductName = "teamsfx";

// @public
export class QTreeNode implements IQTreeNode {
constructor(data: Question | Group);
// (undocumented)
addChild(node: QTreeNode): QTreeNode;
// (undocumented)
children?: QTreeNode[];
cliOptionDisabled?: "self" | "children" | "all";
// (undocumented)
condition?: StringValidation | StringArrayValidation | ConditionFunc;
// (undocumented)
data: Question | Group;
inputsDisabled?: "self" | "children" | "all";
trim(): QTreeNode | undefined;
// (undocumented)
validate(): boolean;
}

// @public (undocumented)
export type Question = SingleSelectQuestion | MultiSelectQuestion | TextInputQuestion | SingleFileQuestion | MultiFileQuestion | FolderQuestion | SingleFileQuestion | SingleFileOrInputQuestion;

Expand Down Expand Up @@ -1103,9 +1082,6 @@ export interface UserInteraction {
}>, modal: boolean, ...items: string[]): Promise<Result<string | undefined, FxError>>;
}

// @public
export function validate<T extends string | string[] | OptionItem | OptionItem[] | undefined>(validSchema: ValidationSchema | ConditionFunc, value: T, inputs?: Inputs): Promise<string | undefined>;

// @public (undocumented)
export type ValidateFunc<T> = (input: T, inputs?: Inputs) => string | undefined | Promise<string | undefined>;

Expand Down
64 changes: 1 addition & 63 deletions packages/api/src/qm/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,73 +451,11 @@ export type Question =
| SingleFileOrInputQuestion;

/**
* QTreeNode is the tree node data structure, which have three main properties:
* IQTreeNode is the tree node data structure, which have three main properties:
* - data: data is either a group or question. Questions can be organized into a group, which has the same trigger condition.
* - condition: trigger condition for this node to be activated;
* - children: child questions that will be activated according their trigger condition.
*/
export class QTreeNode implements IQTreeNode {
data: Question | Group;
condition?: StringValidation | StringArrayValidation | ConditionFunc;
children?: QTreeNode[];
/**
* @description the question node will be ignored as CLI option in non-interactive mode
* "self" - only ignore the question itself
* "children" - ignore all nodes in sub-tree
* "all" - ignore self and all nodes in sub-tree
*/
cliOptionDisabled?: "self" | "children" | "all";
/**
* @description the question node will be ignored as an Inputs property
* "self" - only ignore the question itself
* "children" - ignore all nodes in sub-tree
* "all" - ignore self and all nodes in sub-tree
*/
inputsDisabled?: "self" | "children" | "all";
addChild(node: QTreeNode): QTreeNode {
if (!this.children) {
this.children = [];
}
this.children.push(node);
return this;
}
validate(): boolean {
//1. validate the cycle dependency
//2. validate the name uniqueness
//3. validate the params of RPC
// if (this.data.type === NodeType.group && (!this.children || this.children.length === 0)) return false;
return true;
}

/**
* trim the tree
*/
trim(): QTreeNode | undefined {
if (this.children) {
const newChildren: QTreeNode[] = [];
for (const node of this.children) {
const trimmed = node.trim();
if (trimmed) newChildren.push(trimmed);
}
this.children = newChildren;
}
if (this.data.type === "group") {
if (!this.children || this.children.length === 0) return undefined;
if (this.children.length === 1) {
const child = this.children[0];
if (!(this.condition && child.condition)) {
child.condition ||= this.condition;
return child;
}
}
}
return this;
}
constructor(data: Question | Group) {
this.data = data;
}
}

export interface IQTreeNode {
data: Question | Group;
condition?: StringValidation | StringArrayValidation | ConditionFunc;
Expand Down
211 changes: 0 additions & 211 deletions packages/api/src/qm/validation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as jsonschema from "jsonschema";
import { Inputs, OptionItem } from "../types";

export type ValidateFunc<T> = (
Expand Down Expand Up @@ -138,213 +137,3 @@ export type ConditionFunc = (inputs: Inputs) => boolean | Promise<boolean>;
* Definition of validation schema, which is a union of `StringValidation`, `StringArrayValidation` and `FuncValidation<any>`
*/
export type ValidationSchema = StringValidation | StringArrayValidation | FuncValidation<any>;

/**
* A function to return a validation function according the validation schema
* @param validation validation schema
* @param inputs object to carry all user inputs
* @returns a validation function
*/
export function getValidationFunction<T extends string | string[] | undefined>(
validation: ValidationSchema,
inputs: Inputs
): (input: T) => string | undefined | Promise<string | undefined> {
return function (input: T): string | undefined | Promise<string | undefined> {
return validate(validation, input, inputs);
};
}

/**
* Implementation of validation function
* @param validSchema validation schema
* @param value value to validate
* @param inputs user inputs object, which works as the context of the validation
* @returns A human-readable string which is presented as diagnostic message.
* Return `undefined` when 'value' is valid.
*/
export async function validate<T extends string | string[] | OptionItem | OptionItem[] | undefined>(
validSchema: ValidationSchema | ConditionFunc,
value: T,
inputs?: Inputs
): Promise<string | undefined> {
{
//FuncValidation
const funcValidation: FuncValidation<T> = validSchema as FuncValidation<T>;
if (funcValidation.validFunc) {
const res = await funcValidation.validFunc(value, inputs);
return res as string;
} else if (typeof funcValidation === "function") {
const res = await (funcValidation as ConditionFunc)(inputs!);
if (res) return undefined;
return "condition function is not met.";
}
}

if (!value) {
if ((validSchema as StaticValidation).required === true) return `input value is required.`;
}

const noneEmptyKeyNum = Object.keys(validSchema).filter(
(key) => (validSchema as any)[key] !== undefined
).length;

if (noneEmptyKeyNum === 0) {
return undefined;
}

if (
value === undefined &&
((validSchema as any).required ||
(validSchema as any).equals ||
(validSchema as any).maxLength ||
(validSchema as any).minLength ||
(validSchema as any).pattern ||
(validSchema as any).enum ||
(validSchema as any).startsWith ||
(validSchema as any).endsWith ||
(validSchema as any).includes ||
(validSchema as any).maxItems ||
(validSchema as any).minItems ||
(validSchema as any).uniqueItems ||
(validSchema as any).contains ||
(validSchema as any).containsAll ||
(validSchema as any).containsAny)
) {
return `'undefined' does not meet condition:'${JSON.stringify(validSchema)}'`;
}

{
// StringValidation
const stringValidation: StringValidation = validSchema as StringValidation;
const strToValidate = value as string;
if (strToValidate === undefined || typeof strToValidate === "string") {
const schema: any = {};
if (stringValidation.equals && typeof stringValidation.equals === "string") {
if (strToValidate === undefined) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${strToValidate}' does not meet equals:'${stringValidation.equals}'`;
}
schema.const = stringValidation.equals;
}
if (
stringValidation.enum &&
stringValidation.enum.length > 0 &&
typeof stringValidation.enum[0] === "string"
)
schema.enum = stringValidation.enum;
if (stringValidation.minLength) schema.minLength = stringValidation.minLength;
if (stringValidation.maxLength) schema.maxLength = stringValidation.maxLength;
if (stringValidation.pattern) schema.pattern = stringValidation.pattern;
if (Object.keys(schema).length > 0) {
const validateResult = jsonschema.validate(strToValidate, schema);
if (validateResult.errors && validateResult.errors.length > 0) {
return `'${strToValidate}' ${validateResult.errors[0].message}`;
}
}

if (stringValidation.startsWith) {
if (!strToValidate.startsWith(stringValidation.startsWith)) {
return `'${strToValidate}' does not meet startsWith:'${stringValidation.startsWith}'`;
}
}
if (stringValidation.endsWith) {
if (!strToValidate.endsWith(stringValidation.endsWith)) {
return `'${strToValidate}' does not meet endsWith:'${stringValidation.endsWith}'`;
}
}
if (stringValidation.includes) {
if (!strToValidate.includes(stringValidation.includes)) {
return `'${strToValidate}' does not meet includes:'${stringValidation.includes}'`;
}
}
if (stringValidation.notEquals) {
if (strToValidate === stringValidation.notEquals) {
return `'${strToValidate}' does not meet notEquals:'${stringValidation.notEquals}'`;
}
}
if (stringValidation.excludesEnum) {
if (stringValidation.excludesEnum.includes(strToValidate)) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${strToValidate}' does not meet excludesEnum:'${stringValidation.excludesEnum}'`;
}
}
}
}

//StringArrayValidation
{
const stringArrayValidation: StringArrayValidation = validSchema as StringArrayValidation;
const arrayToValidate = value as string[];
if (arrayToValidate === undefined || arrayToValidate instanceof Array) {
const schema: any = {};
if (stringArrayValidation.maxItems) schema.maxItems = stringArrayValidation.maxItems;
if (stringArrayValidation.minItems) schema.minItems = stringArrayValidation.minItems;
if (stringArrayValidation.uniqueItems) schema.uniqueItems = stringArrayValidation.uniqueItems;
if (Object.keys(schema).length > 0) {
const validateResult = jsonschema.validate(arrayToValidate, schema);
if (validateResult.errors && validateResult.errors.length > 0) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${arrayToValidate}' ${validateResult.errors[0].message}`;
}
}
if (stringArrayValidation.equals) {
if (stringArrayValidation.equals instanceof Array) {
stringArrayValidation.enum = stringArrayValidation.equals;
stringArrayValidation.containsAll = stringArrayValidation.equals;
} else {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${arrayToValidate}' does not equals to:'${stringArrayValidation.equals}'`;
}
}
if (stringArrayValidation.enum && arrayToValidate) {
for (const item of arrayToValidate) {
if (!stringArrayValidation.enum.includes(item)) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${arrayToValidate}' does not meet with enum:'${stringArrayValidation.enum}'`;
}
}
}
if (stringArrayValidation.excludes) {
if (arrayToValidate && arrayToValidate.includes(stringArrayValidation.excludes)) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${arrayToValidate}' does not meet with excludes:'${stringArrayValidation.excludes}'`;
}
}
if (stringArrayValidation.contains) {
if (arrayToValidate && !arrayToValidate.includes(stringArrayValidation.contains)) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${arrayToValidate}' does not meet with contains:'${stringArrayValidation.contains}'`;
}
}
if (stringArrayValidation.containsAll) {
const containsAll: string[] = stringArrayValidation.containsAll;
if (containsAll.length > 0) {
for (const i of containsAll) {
if (arrayToValidate && !arrayToValidate.includes(i)) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${arrayToValidate}' does not meet with containsAll:'${containsAll}'`;
}
}
}
}
if (stringArrayValidation.containsAny) {
const containsAny: string[] = stringArrayValidation.containsAny;
if (containsAny.length > 0) {
// let array = valueToValidate as string[];
let found = false;
for (const i of containsAny) {
if (arrayToValidate && arrayToValidate.includes(i)) {
found = true;
break;
}
}
if (!found) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `'${arrayToValidate}' does not meet containsAny:'${containsAny}'`;
}
}
}
}
}
return undefined;
}
Loading

0 comments on commit 28597af

Please sign in to comment.