From 1209f23048c68b7bf95c7ad058ad8322af001190 Mon Sep 17 00:00:00 2001 From: mliddell Date: Fri, 30 Oct 2020 15:07:04 -0700 Subject: [PATCH] fix: improves validations API - Consolidate the validation function defintions to a single generic - rename SlotValidationFunction -> StateValidationFunction Miscellaneous - add some missed exports to the "package-exports" index.ts BREAKING CHANGE: validation function types altered. --- package-lock.json | 22 +++++++++++ src/commonControls/DateControl.ts | 28 +++++--------- src/commonControls/NumberControl.ts | 24 +++++------- src/commonControls/ValueControl.ts | 22 ++++------- .../dateRangeControl/DateRangeControl.ts | 38 +++++++++---------- src/commonControls/listControl/ListControl.ts | 20 +++------- src/controls/ValidationResult.ts | 36 ++++++++++++++---- src/index.ts | 25 ++++++++++-- 8 files changed, 123 insertions(+), 92 deletions(-) diff --git a/package-lock.json b/package-lock.json index e69bad6..e953990 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2094,8 +2094,30 @@ "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", "requires": { "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", "has-symbols": "^1.0.1", "object-keys": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } } } diff --git a/src/commonControls/DateControl.ts b/src/commonControls/DateControl.ts index 776415c..e53b9f3 100644 --- a/src/commonControls/DateControl.ts +++ b/src/commonControls/DateControl.ts @@ -18,7 +18,7 @@ import { Control, ControlInputHandlingProps, ControlProps, ControlState } from ' import { ControlInput } from '../controls/ControlInput'; import { ControlResultBuilder } from '../controls/ControlResult'; import { InteractionModelContributor } from '../controls/mixins/InteractionModelContributor'; -import { ValidationResult } from '../controls/ValidationResult'; +import { StateValidationFunction, ValidationFailure } from '../controls/ValidationResult'; import { AmazonBuiltInSlotType } from '../intents/AmazonBuiltInSlotType'; import { GeneralControlIntent, unpackGeneralControlIntent } from '../intents/GeneralControlIntent'; import { @@ -71,7 +71,7 @@ export interface DateControlProps extends ControlProps { * valid: DateControlValidations.FUTURE_DATE_ONLY, * ``` */ - validation?: DateValidationFunction | DateValidationFunction[]; + validation?: StateValidationFunction | Array>; /** * Determines if the Control must obtain a value. @@ -123,14 +123,6 @@ export interface DateControlProps extends ControlProps { valueRenderer?: (value: string, input: ControlInput) => string; } -/** - * ValueControl validation function - */ -export type DateValidationFunction = ( - state: DateControlState, - input: ControlInput, -) => true | ValidationResult; - /** * Mapping of action slot values to the behaviors that this control supports. * @@ -240,10 +232,10 @@ export namespace DateControlValidations { * @param state - Control state * @param input - Input */ - export const PAST_DATE_ONLY: DateValidationFunction = ( + export const PAST_DATE_ONLY: StateValidationFunction = ( state: DateControlState, input: ControlInput, - ): true | ValidationResult => { + ): true | ValidationFailure => { const startDate = getStartDateOfRange(state.value!); const startDateInUTC = getUTCDate(startDate); @@ -262,10 +254,10 @@ export namespace DateControlValidations { * @param state - Control state * @param input - Input */ - export const FUTURE_DATE_ONLY: DateValidationFunction = ( + export const FUTURE_DATE_ONLY: StateValidationFunction = ( state: DateControlState, input: ControlInput, - ): true | ValidationResult => { + ): true | ValidationFailure => { const endDate = getEndDateOfRange(state.value!); const endDateInUTC = getUTCDate(endDate); @@ -699,7 +691,7 @@ export class DateControl extends Control implements InteractionModelContributor elicitationAction: string, ): void { this.state.elicitationAction = elicitationAction; - const validationResult: true | ValidationResult = this.validate(input); + const validationResult: true | ValidationFailure = this.validate(input); if (validationResult === true) { if (elicitationAction === $.Action.Change) { // if elicitationAction == 'change', then the previousValue must be defined. @@ -774,11 +766,11 @@ export class DateControl extends Control implements InteractionModelContributor this.validateAndAddActs(input, resultBuilder, $.Action.Change); } - private validate(input: ControlInput): true | ValidationResult { - const listOfValidationFunc: DateValidationFunction[] = + private validate(input: ControlInput): true | ValidationFailure { + const listOfValidationFunc: Array> = typeof this.props.validation === 'function' ? [this.props.validation] : this.props.validation; for (const validationFunction of listOfValidationFunc) { - const validationResult: true | ValidationResult = validationFunction(this.state, input); + const validationResult: true | ValidationFailure = validationFunction(this.state, input); if (validationResult !== true) { log.debug( `DateControl.validate(): validation failed. Reason: ${JSON.stringify( diff --git a/src/commonControls/NumberControl.ts b/src/commonControls/NumberControl.ts index 6a80f2a..6433e8d 100644 --- a/src/commonControls/NumberControl.ts +++ b/src/commonControls/NumberControl.ts @@ -28,7 +28,7 @@ import { Control, ControlInputHandlingProps, ControlProps, ControlState } from ' import { ControlInput } from '../controls/ControlInput'; import { ControlResultBuilder } from '../controls/ControlResult'; import { InteractionModelContributor } from '../controls/mixins/InteractionModelContributor'; -import { ValidationResult } from '../controls/ValidationResult'; +import { StateValidationFunction, ValidationFailure } from '../controls/ValidationResult'; import { GeneralControlIntent, unpackGeneralControlIntent } from '../intents/GeneralControlIntent'; import { ControlInteractionModelGenerator } from '../interactionModelGeneration/ControlInteractionModelGenerator'; import { ModelData, SharedSlotType } from '../interactionModelGeneration/ModelTypes'; @@ -68,7 +68,9 @@ export interface NumberControlProps extends ControlProps { * - Validation functions return either `true` or a `ValidationResult` to * describe what validation failed. */ - validation?: NumberValidationFunction | NumberValidationFunction[]; + validation?: + | StateValidationFunction + | Array>; /** * Determines if the Control must obtain a value. @@ -128,14 +130,6 @@ export interface NumberControlProps extends ControlProps { valueRenderer?: (value: number, input: ControlInput) => string; } -/** - * NumberControl validation function - */ -export type NumberValidationFunction = ( - state: NumberControlState, - input: ControlInput, -) => true | ValidationResult; - /** * NumberControl isRequired function */ @@ -1111,7 +1105,7 @@ export class NumberControl extends Control implements InteractionModelContributo if (!this.evaluateBooleanProp(this.props.required, input) || this.state.value === undefined) { return false; } - const validationResult: true | ValidationResult = this.validateNumber(input); + const validationResult: true | ValidationFailure = this.validateNumber(input); if (validationResult === true) { return false; } @@ -1128,7 +1122,7 @@ export class NumberControl extends Control implements InteractionModelContributo renderedValue: this.state.value !== undefined ? this.props.valueRenderer(this.state.value, input) : '', reasonCode: 'ValueInvalid', - renderedReason: (validationResult as ValidationResult).renderedReason, + renderedReason: (validationResult as ValidationFailure).renderedReason, }), ); resultBuilder.addAct(new RequestValueAct(this)); @@ -1157,11 +1151,11 @@ export class NumberControl extends Control implements InteractionModelContributo ); } - private validateNumber(input: ControlInput): true | ValidationResult { - const listOfValidationFunc: NumberValidationFunction[] = + private validateNumber(input: ControlInput): true | ValidationFailure { + const listOfValidationFunc: Array> = typeof this.props.validation === 'function' ? [this.props.validation] : this.props.validation; for (const validationFunction of listOfValidationFunc) { - const validationResult: boolean | ValidationResult = validationFunction(this.state, input); + const validationResult: boolean | ValidationFailure = validationFunction(this.state, input); if (validationResult !== true) { log.debug( `NumberControl.validate(): validation failed. Reason: ${JSON.stringify( diff --git a/src/commonControls/ValueControl.ts b/src/commonControls/ValueControl.ts index 63f7d60..24fd7e9 100644 --- a/src/commonControls/ValueControl.ts +++ b/src/commonControls/ValueControl.ts @@ -20,7 +20,7 @@ import { ControlInput } from '../controls/ControlInput'; import { ControlResultBuilder } from '../controls/ControlResult'; import { ControlStateDiagramming } from '../controls/mixins/ControlStateDiagramming'; import { InteractionModelContributor } from '../controls/mixins/InteractionModelContributor'; -import { ValidationResult } from '../controls/ValidationResult'; +import { StateValidationFunction, ValidationFailure } from '../controls/ValidationResult'; import { AmazonBuiltInSlotType } from '../intents/AmazonBuiltInSlotType'; import { GeneralControlIntent, unpackGeneralControlIntent } from '../intents/GeneralControlIntent'; import { @@ -77,7 +77,9 @@ export interface ValueControlProps extends ControlProps { * - Validation functions return either `true` or a `ValidationResult` to * describe what validation failed. */ - validation?: SlotValidationFunction | SlotValidationFunction[]; + validation?: + | StateValidationFunction + | Array>; /** * Determines if the Control must obtain a value. @@ -129,14 +131,6 @@ export interface ValueControlProps extends ControlProps { valueRenderer?: (value: string, input: ControlInput) => string; } -/** - * ValueControl validation function - */ -export type SlotValidationFunction = ( - state: ValueControlState, - input: ControlInput, -) => true | ValidationResult; - /** * Mapping of action slot values to the behaviors that this control supports. * @@ -758,7 +752,7 @@ export class ValueControl extends Control implements InteractionModelContributor elicitationAction: string, ): void { this.state.elicitationAction = elicitationAction; - const validationResult: true | ValidationResult = this.validate(input); + const validationResult: true | ValidationFailure = this.validate(input); if (typeof validationResult === 'boolean') { if (elicitationAction === $.Action.Change) { // if elicitationAction == 'change', then the previousValue must be defined. @@ -804,11 +798,11 @@ export class ValueControl extends Control implements InteractionModelContributor * * @param input - Input. */ - private validate(input: ControlInput): true | ValidationResult { - const listOfValidationFunc: SlotValidationFunction[] = + private validate(input: ControlInput): true | ValidationFailure { + const listOfValidationFunc: Array> = typeof this.props.validation === 'function' ? [this.props.validation] : this.props.validation; for (const validationFunction of listOfValidationFunc) { - const validationResult: true | ValidationResult = validationFunction(this.state, input); + const validationResult: true | ValidationFailure = validationFunction(this.state, input); if (validationResult !== true) { log.debug( `ValueControl.validate(): validation failed. Reason: ${JSON.stringify( diff --git a/src/commonControls/dateRangeControl/DateRangeControl.ts b/src/commonControls/dateRangeControl/DateRangeControl.ts index ae41593..4d063c1 100644 --- a/src/commonControls/dateRangeControl/DateRangeControl.ts +++ b/src/commonControls/dateRangeControl/DateRangeControl.ts @@ -24,7 +24,7 @@ import { import { ControlInput } from '../../controls/ControlInput'; import { ControlResultBuilder } from '../../controls/ControlResult'; import { InteractionModelContributor } from '../../controls/mixins/InteractionModelContributor'; -import { ValidationResult } from '../../controls/ValidationResult'; +import { StateValidationFunction, ValidationFailure } from '../../controls/ValidationResult'; import { AmazonBuiltInSlotType } from '../../intents/AmazonBuiltInSlotType'; import { ActionAndTask, @@ -51,7 +51,7 @@ import { SystemAct } from '../../systemActs/SystemAct'; import { evaluateCustomHandleFuncs, logIfBothTrue } from '../../utils/ControlUtils'; import { DeepRequired } from '../../utils/DeepRequired'; import { falseIfGuardFailed, okIf } from '../../utils/Predicates'; -import { DateControl, DateControlPromptProps, DateValidationFunction } from '../DateControl'; +import { DateControl, DateControlPromptProps, DateControlState } from '../DateControl'; import { alexaDateFormatToDate, findEdgeDateOfDateRange } from './DateHelper'; import { DateRangeControlIntentInput, generateDatesInputGroups } from './DateRangeNLUHelper'; @@ -85,7 +85,9 @@ export interface DateRangeControlProps extends ContainerControlProps { * valid: DateControlValidations.FUTURE_DATE_ONLY, * ``` */ - startDateValid?: DateValidationFunction | DateValidationFunction[]; + startDateValid?: + | StateValidationFunction + | Array>; /** * Function(s) that determine if the end date (in isolation) is valid. @@ -102,7 +104,9 @@ export interface DateRangeControlProps extends ContainerControlProps { * valid: DateControlValidations.FUTURE_DATE_ONLY, * ``` */ - endDateValid?: DateValidationFunction | DateValidationFunction[]; + endDateValid?: + | StateValidationFunction + | Array>; /** * Function(s) that determine if the date-range is valid. @@ -119,7 +123,9 @@ export interface DateRangeControlProps extends ContainerControlProps { * valid: DateRangeControlValidations.START_BEFORE_END, * ``` */ - rangeValid?: DateRangeValidationFunction | DateRangeValidationFunction[]; + rangeValid?: + | StateValidationFunction + | Array>; }; /** @@ -177,14 +183,6 @@ export type DateRange = { endDate: string; }; -/** - * Date range validation function - */ -export type DateRangeValidationFunction = ( - state: DateRangeControlState, - input: ControlInput, -) => true | ValidationResult; - /** * Mapping of action slot values to the behaviors that this control supports. * @@ -362,10 +360,10 @@ export enum DateRangeValidationFailReasonCode { * Built-in validation functions for use with DateControl */ export namespace DateRangeControlValidations { - export const START_BEFORE_END: DateRangeValidationFunction = ( + export const START_BEFORE_END: StateValidationFunction = ( state: DateRangeControlState, input: ControlInput, - ): true | ValidationResult => { + ): true | ValidationFailure => { const startDate = alexaDateFormatToDate(state.startDate!); const endDate = alexaDateFormatToDate(state.endDate!); if (startDate > endDate) { @@ -1214,13 +1212,13 @@ export class DateRangeControl extends ContainerControl implements InteractionMod } } - private validateDateRange(input: ControlInput): true | ValidationResult { - const listOfValidationFunc: DateRangeValidationFunction[] = + private validateDateRange(input: ControlInput): true | ValidationFailure { + const listOfValidationFunc: Array> = typeof this.props.validation.rangeValid === 'function' ? [this.props.validation.rangeValid] : this.props.validation.rangeValid; for (const validationFunction of listOfValidationFunc) { - const validationResult: true | ValidationResult = validationFunction(this.state, input); + const validationResult: true | ValidationFailure = validationFunction(this.state, input); if (validationResult !== true) { log.debug( `DateRangeControl.validate(): validation failed. Reason: ${JSON.stringify( @@ -1326,7 +1324,7 @@ export class DateRangeControl extends ContainerControl implements InteractionMod try { // Only fix range when range is set and there's no open question okIf(!this.needsValue(input) && !this.state.isChangingRange); - const rangeValidationResult: true | ValidationResult = this.validateDateRange(input); + const rangeValidationResult: true | ValidationFailure = this.validateDateRange(input); okIf(rangeValidationResult !== true); this.takeInitiativeFunc = this.correctRange; return true; @@ -1336,7 +1334,7 @@ export class DateRangeControl extends ContainerControl implements InteractionMod } private correctRange(input: ControlInput, resultBuilder: ControlResultBuilder): void { - const rangeValidationResult: true | ValidationResult = this.validateDateRange(input); + const rangeValidationResult: true | ValidationFailure = this.validateDateRange(input); this.state.onFocus = true; if (rangeValidationResult !== true) { const dateRange = { diff --git a/src/commonControls/listControl/ListControl.ts b/src/commonControls/listControl/ListControl.ts index b994512..0fb38a3 100644 --- a/src/commonControls/listControl/ListControl.ts +++ b/src/commonControls/listControl/ListControl.ts @@ -21,7 +21,7 @@ import { ControlAPL } from '../../controls/ControlAPL'; import { ControlInput } from '../../controls/ControlInput'; import { ControlResultBuilder } from '../../controls/ControlResult'; import { InteractionModelContributor } from '../../controls/mixins/InteractionModelContributor'; -import { ValidationResult } from '../../controls/ValidationResult'; +import { StateValidationFunction, ValidationFailure } from '../../controls/ValidationResult'; import { AmazonBuiltInSlotType } from '../../intents/AmazonBuiltInSlotType'; import { GeneralControlIntent, unpackGeneralControlIntent } from '../../intents/GeneralControlIntent'; import { OrdinalControlIntent, unpackOrdinalControlIntent } from '../../intents/OrdinalControlIntent'; @@ -89,7 +89,7 @@ export interface ListControlProps extends ControlProps { * - Validation functions return either `true` or a `ValidationResult` to * describe what validation failed. */ - validation?: SlotValidationFunction | SlotValidationFunction[]; + validation?: StateValidationFunction | Array>; /** * List of slot-value IDs that will be presented to the user as a list. @@ -156,14 +156,6 @@ export interface ListControlProps extends ControlProps { apl?: ListControlAPLProps; } -/** - * ListControl validation function - */ -export type SlotValidationFunction = ( - state: ListControlState, - input: ControlInput, -) => true | ValidationResult; - /** * Mapping of action slot values to the behaviors that this control supports. * @@ -962,7 +954,7 @@ export class ListControl extends Control implements InteractionModelContributor resultBuilder: ControlResultBuilder, elicitationAction: string, ): void { - const validationResult: true | ValidationResult = this.validate(input); + const validationResult: true | ValidationFailure = this.validate(input); if (validationResult === true) { if (elicitationAction === $.Action.Change) { // if elicitationAction == 'change', then the previousValue must be defined. @@ -1003,11 +995,11 @@ export class ListControl extends Control implements InteractionModelContributor return; } - private validate(input: ControlInput): true | ValidationResult { - const listOfValidationFunc: SlotValidationFunction[] = + private validate(input: ControlInput): true | ValidationFailure { + const listOfValidationFunc: Array> = typeof this.props.validation === 'function' ? [this.props.validation] : this.props.validation; for (const validationFunction of listOfValidationFunc) { - const validationResult: true | ValidationResult = validationFunction(this.state, input); + const validationResult: true | ValidationFailure = validationFunction(this.state, input); if (validationResult !== true) { log.debug( `ListControl.validate(): validation failed. Reason: ${JSON.stringify( diff --git a/src/controls/ValidationResult.ts b/src/controls/ValidationResult.ts index cb4d5a6..5023918 100644 --- a/src/controls/ValidationResult.ts +++ b/src/controls/ValidationResult.ts @@ -11,27 +11,47 @@ * permissions and limitations under the License. */ -// The return type of all builtin controls' validation function when validation fails +import { ControlInput } from './ControlInput'; /** - * Describes what validation failure occurred. + * State validation function + * + * Takes a Control state object and returns `true` if the state passes validation. If + * validation fails, a `ValidationResult` object is returned instead. + */ +export type StateValidationFunction = ( + state: TState, + input: ControlInput, +) => true | ValidationFailure; + +/** + * Describes a validation failure. * * Usage: - * - A reason code should be provided in situations where the reason may need - * context-specific rendering. + * - A reason code should be provided that uniquely describes the kind of validation + * failure. A reason code is useful for context-specific rendering and other business + * function. It is not mandatory as for some simple cases it may be duplicative to + * provide both a reason code and a single rendering of that reason code. * - * - A rendered reason can be provided if there is no need for context-specific - * rendering. Rendering the reason directly during construction may simplify - * code in these situations. + * - A rendered reason for situations where the rendered form is not context-sensitive and + * can be conveniently provided when the ValidationFailure is instantiated. */ -export type ValidationResult = { +export type ValidationFailure = { /** * A code representing what validation failed. + * + * Usage: + * - use reasonCode for business logic and transform to a prompt during the rendering + * phase. */ reasonCode?: string; /** * A rendered prompt fragment that can be directly included in the `Response`. + * + * Usage: + * - If convenient, generate the prompt fragment at instantiation time. + * - A renderedReason should not be used in logic or further transformed. */ renderedReason?: string; }; diff --git a/src/index.ts b/src/index.ts index 89b88aa..e01010c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ export { DateControlProps, DateControlState, DateControlValidations, - DateValidationFunction, + DateValidationFailReasonCode, } from './commonControls/DateControl'; export { alexaDateFormatToDate, @@ -30,18 +30,28 @@ export { getEndDateOfRange, getMonth, getStartDateOfRange, + getUTCDate, getYear, } from './commonControls/dateRangeControl/DateHelper'; export { + DateControlTarget, + DateRange, DateRangeControl, + DateRangeControlActionProps, + DateRangeControlInteractionModelProps, + DateRangeControlPromptProps, DateRangeControlProps, DateRangeControlState, + DateRangeControlTargetProps, DateRangeControlValidations, + DateRangeValidationFailReasonCode, TargetCategory, } from './commonControls/dateRangeControl/DateRangeControl'; +export { defaultI18nResources } from './commonControls/LanguageStrings'; export { ListControl, ListControlActionProps, + ListControlAPLProps, ListControlInteractionModelProps, ListControlPromptProps, ListControlProps, @@ -49,7 +59,9 @@ export { } from './commonControls/listControl/ListControl'; export { ListControlAPLPropsBuiltIns } from './commonControls/listControl/ListControlAPL'; export { + NumberConfirmationRequireFunction, NumberControl, + NumberControlActionProps, NumberControlInteractionModelProps, NumberControlPromptsProps, NumberControlProps, @@ -57,7 +69,9 @@ export { } from './commonControls/NumberControl'; export { ValueControl, + ValueControlActionProps, ValueControlInteractionModelProps, + ValueControlPromptProps, ValueControlProps, ValueControlState, } from './commonControls/ValueControl'; @@ -68,7 +82,7 @@ export { ContainerControlProps, ContainerControlState, } from './controls/ContainerControl'; -export { Control, ControlProps, ControlInputHandlingProps } from './controls/Control'; +export { Control, ControlInputHandlingProps, ControlProps } from './controls/Control'; export { ControlInput } from './controls/ControlInput'; export { ControlManager, renderActsInSequence } from './controls/ControlManager'; export { ControlResult, ControlResultBuilder } from './controls/ControlResult'; @@ -90,6 +104,11 @@ export { GeneralControlIntentSlots, unpackGeneralControlIntent, } from './intents/GeneralControlIntent'; +export { + OrdinalControlIntent, + OrdinalControlIntentSlots, + unpackOrdinalControlIntent, +} from './intents/OrdinalControlIntent'; export { SingleValueControlIntent, SingleValueControlIntentSlots, @@ -125,6 +144,7 @@ export { ISystemAct, SystemAct } from './systemActs/SystemAct'; export { matchIfDefined, mismatch, moveArrayItem, randomlyPick } from './utils/ArrayUtils'; export { StringOrList, StringOrTrue } from './utils/BasicTypes'; export { generateControlTreeTextDiagram } from './utils/ControlTreeVisualization'; +export { evaluateCustomHandleFuncs, logIfBothTrue } from './utils/ControlUtils'; export { visitControls } from './utils/ControlVisitor'; export { DeepRequired } from './utils/DeepRequired'; export { throwIf, throwIfUndefined } from './utils/ErrorUtils'; @@ -152,4 +172,3 @@ export { testTurn, waitForDebugger, } from './utils/testSupport/TestingUtils'; -export { evaluateCustomHandleFuncs, logIfBothTrue } from './utils/ControlUtils';