From 770df05d4aaeed27ae3a0646163a3d3010a83dcc Mon Sep 17 00:00:00 2001 From: Bart Tadych Date: Wed, 19 Jul 2023 22:38:37 +0200 Subject: [PATCH] 0.7.2. (#17) --- CHANGELOG.md | 22 +++++++ README.md | 1 + demos/webpack-app/package.json | 4 +- .../public/assets/placement-restrictions.css | 26 ++++++++ demos/webpack-app/public/editors.html | 2 +- .../public/placement-restrictions.html | 14 +++++ demos/webpack-app/public/playground.html | 2 +- .../src/placement-restrictions/app.ts | 50 ++++++++++++++++ .../definition-model.ts | 59 +++++++++++++++++++ .../src/playground/model/root-model.ts | 2 +- demos/webpack-app/webpack.config.js | 3 +- editor/css/editor.css | 3 + editor/package.json | 6 +- .../property-validation-error-component.ts | 6 +- editor/src/editor-provider.ts | 22 +++++-- editor/src/editor.ts | 27 ++++++++- editor/src/property-editor/property-editor.ts | 8 +-- model/package.json | 2 +- model/src/builders/property-model-builder.ts | 19 +++--- model/src/builders/step-model-builder.spec.ts | 40 ++++++++++++- model/src/builders/step-model-builder.ts | 23 ++++++-- model/src/context/definition-context.ts | 12 ++-- model/src/context/value-context.ts | 2 +- model/src/context/variables-provider.ts | 18 ++++-- model/src/core/simple-event.spec.ts | 24 ++++++++ model/src/model.ts | 15 +++-- .../validator/definition-validator.spec.ts | 4 +- model/src/validator/definition-validator.ts | 26 +++++--- model/src/validator/index.ts | 3 +- ...ntext.ts => property-validator-context.ts} | 16 +++-- model/src/validator/step-validator-context.ts | 16 +++++ 31 files changed, 409 insertions(+), 68 deletions(-) create mode 100644 demos/webpack-app/public/assets/placement-restrictions.css create mode 100644 demos/webpack-app/public/placement-restrictions.html create mode 100644 demos/webpack-app/src/placement-restrictions/app.ts create mode 100644 demos/webpack-app/src/placement-restrictions/definition-model.ts create mode 100644 model/src/core/simple-event.spec.ts rename model/src/validator/{custom-validator-context.ts => property-validator-context.ts} (50%) create mode 100644 model/src/validator/step-validator-context.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ed35d..21213f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 0.7.2 + +We added a new type of a validator: step validator. It allows to restrict a placement of a step in a definition. For example, you can enforce that a step can be placed only inside a specific step. + +```ts +createStepModel('writeSocket', 'task', step => { + step.validator({ + validate(context: StepValidatorContext) { + const parentTypes = context.getParentStepTypes() + return parentTypes.includes('socket'); + ? null // No errors + : 'The write socket step must be inside a socket.'; + } + }); +}); +``` + +Additionally we've renamed: + +* the `CustomValidatorContext` class to `PropertyValidatorContext`, +* the `customValidator` method of the `PropertyModelBuilder` class to `validator`. + ## 0.7.1 This version renames all `*ValueModel` functions to `create*ValueModel`, adding the `create` prefix. diff --git a/README.md b/README.md index 514a553..b27544a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Powerful workflow editor builder for sequential workflows. Written in TypeScript * [🛠 Playground](https://nocode-js.github.io/sequential-workflow-editor/webpack-app/public/playground.html) * [📖 Editors](https://nocode-js.github.io/sequential-workflow-editor/webpack-app/public/editors.html) +* [🎯 Placement Restrictions](https://nocode-js.github.io/sequential-workflow-editor/webpack-app/public/placement-restrictions.html) ## 🚀 Installation diff --git a/demos/webpack-app/package.json b/demos/webpack-app/package.json index 72ca596..244c7ce 100644 --- a/demos/webpack-app/package.json +++ b/demos/webpack-app/package.json @@ -18,8 +18,8 @@ "sequential-workflow-model": "^0.1.4", "sequential-workflow-designer": "^0.13.5", "sequential-workflow-machine": "^0.3.0", - "sequential-workflow-editor-model": "^0.7.1", - "sequential-workflow-editor": "^0.7.1" + "sequential-workflow-editor-model": "^0.7.2", + "sequential-workflow-editor": "^0.7.2" }, "devDependencies": { "ts-loader": "^9.4.2", diff --git a/demos/webpack-app/public/assets/placement-restrictions.css b/demos/webpack-app/public/assets/placement-restrictions.css new file mode 100644 index 0000000..85b2186 --- /dev/null +++ b/demos/webpack-app/public/assets/placement-restrictions.css @@ -0,0 +1,26 @@ +html, +body, +#designer { + margin: 0; + padding: 0; + width: 100vw; + height: 100vh; + overflow: hidden; +} +body, +input, +textarea { + font: 14px/1.3em Arial, Verdana, sans-serif; +} +.sqd-global-editor { + padding: 10px; + line-height: 1.3em; + box-sizing: border-box; +} +a { + color: #000; + text-decoration: underline; +} +a:hover { + text-decoration: none; +} diff --git a/demos/webpack-app/public/editors.html b/demos/webpack-app/public/editors.html index 495b8d6..896694d 100644 --- a/demos/webpack-app/public/editors.html +++ b/demos/webpack-app/public/editors.html @@ -1,5 +1,5 @@ - + 📖 Editors - Sequential Workflow Editor diff --git a/demos/webpack-app/public/placement-restrictions.html b/demos/webpack-app/public/placement-restrictions.html new file mode 100644 index 0000000..8162d04 --- /dev/null +++ b/demos/webpack-app/public/placement-restrictions.html @@ -0,0 +1,14 @@ + + + + + 🎯 Placement Restrictions - Sequential Workflow Editor + + + + + + +
+ + diff --git a/demos/webpack-app/public/playground.html b/demos/webpack-app/public/playground.html index 9bbfb7c..15293e1 100644 --- a/demos/webpack-app/public/playground.html +++ b/demos/webpack-app/public/playground.html @@ -1,5 +1,5 @@ - + 🛠 Playground - Sequential Workflow Editor diff --git a/demos/webpack-app/src/placement-restrictions/app.ts b/demos/webpack-app/src/placement-restrictions/app.ts new file mode 100644 index 0000000..28356ce --- /dev/null +++ b/demos/webpack-app/src/placement-restrictions/app.ts @@ -0,0 +1,50 @@ +import { EditorProvider } from 'sequential-workflow-editor'; +import { SocketStep, definitionModel } from './definition-model'; +import { Designer, Uid } from 'sequential-workflow-designer'; + +import 'sequential-workflow-designer/css/designer.css'; +import 'sequential-workflow-designer/css/designer-light.css'; +import 'sequential-workflow-editor/css/editor.css'; + +export class App { + public static create() { + const placeholder = document.getElementById('designer') as HTMLElement; + + const editorProvider = EditorProvider.create(definitionModel, { + uidGenerator: Uid.next + }); + + const definition = editorProvider.activateDefinition(); + const loop = editorProvider.activateStep('socket') as SocketStep; + loop.sequence.push(editorProvider.activateStep('writeSocket')); + const break_ = editorProvider.activateStep('writeSocket'); + definition.sequence.push(loop); + definition.sequence.push(break_); + + Designer.create(placeholder, definition, { + controlBar: true, + editors: { + globalEditorProvider: () => { + const editor = document.createElement('div'); + editor.innerHTML = + 'This example shows how to restrict the placement of steps. The write socket step can only be placed inside a socket step. GitHub'; + return editor; + }, + stepEditorProvider: editorProvider.createStepEditorProvider() + }, + validator: { + step: editorProvider.createStepValidator(), + root: editorProvider.createRootValidator() + }, + steps: { + iconUrlProvider: () => './assets/icon-task.svg' + }, + toolbox: { + groups: editorProvider.getToolboxGroups(), + labelProvider: editorProvider.createStepLabelProvider() + } + }); + } +} + +document.addEventListener('DOMContentLoaded', App.create, false); diff --git a/demos/webpack-app/src/placement-restrictions/definition-model.ts b/demos/webpack-app/src/placement-restrictions/definition-model.ts new file mode 100644 index 0000000..3a8e694 --- /dev/null +++ b/demos/webpack-app/src/placement-restrictions/definition-model.ts @@ -0,0 +1,59 @@ +import { + createDefinitionModel, + createNumberValueModel, + createSequentialStepModel, + createStepModel, + createStringValueModel +} from 'sequential-workflow-editor-model'; +import { SequentialStep } from 'sequential-workflow-model'; + +export interface SocketStep extends SequentialStep { + type: 'socket'; + componentType: 'container'; + properties: { + ip: string; + port: number; + }; +} + +export interface WriteSocketStep extends SequentialStep { + type: 'writeSocket'; + componentType: 'task'; + properties: { + data: string; + }; +} + +export const definitionModel = createDefinitionModel(model => { + model.root(() => { + // + }); + model.steps([ + createSequentialStepModel('socket', 'container', step => { + step.property('ip').value( + createStringValueModel({ + defaultValue: '127.0.0.1' + }) + ); + step.property('port').value( + createNumberValueModel({ + defaultValue: 5000 + }) + ); + }), + createStepModel('writeSocket', 'task', step => { + step.property('data').value( + createStringValueModel({ + defaultValue: 'Hello World!' + }) + ); + + step.validator({ + validate(context) { + const parentTypes = context.getParentStepTypes(); + return parentTypes.includes('socket') ? null : 'The write socket step must be inside a socket.'; + } + }); + }) + ]); +}); diff --git a/demos/webpack-app/src/playground/model/root-model.ts b/demos/webpack-app/src/playground/model/root-model.ts index b361d33..41fe1c2 100644 --- a/demos/webpack-app/src/playground/model/root-model.ts +++ b/demos/webpack-app/src/playground/model/root-model.ts @@ -6,7 +6,7 @@ export const rootModel = createRootModel(root => { .hint('Variables passed to the workflow from the outside.') .value(createVariableDefinitionsValueModel({})) .dependentProperty('outputs') - .customValidator({ + .validator({ validate(context) { const inputs = context.getPropertyValue('outputs'); return inputs.variables.length > 0 ? null : 'At least one input is required'; diff --git a/demos/webpack-app/webpack.config.js b/demos/webpack-app/webpack.config.js index d1f4c2f..174c1ee 100644 --- a/demos/webpack-app/webpack.config.js +++ b/demos/webpack-app/webpack.config.js @@ -29,5 +29,6 @@ function bundle(name) { module.exports = [ bundle('playground'), - bundle('editors') + bundle('editors'), + bundle('placement-restrictions') ]; diff --git a/editor/css/editor.css b/editor/css/editor.css index efe7701..a4578c4 100644 --- a/editor/css/editor.css +++ b/editor/css/editor.css @@ -26,6 +26,9 @@ margin: 0 0 10px; color: #666; } +.swe-editor > .swe-validation-error { + margin: 0 10px; +} /* properties */ diff --git a/editor/package.json b/editor/package.json index aa23370..3b5727b 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor", - "version": "0.7.1", + "version": "0.7.2", "type": "module", "main": "./lib/esm/index.js", "types": "./lib/index.d.ts", @@ -46,11 +46,11 @@ "prettier:fix": "prettier --write ./src ./css" }, "dependencies": { - "sequential-workflow-editor-model": "^0.7.1", + "sequential-workflow-editor-model": "^0.7.2", "sequential-workflow-model": "^0.1.4" }, "peerDependencies": { - "sequential-workflow-editor-model": "^0.7.1", + "sequential-workflow-editor-model": "^0.7.2", "sequential-workflow-model": "^0.1.4" }, "devDependencies": { diff --git a/editor/src/components/property-validation-error-component.ts b/editor/src/components/property-validation-error-component.ts index 67ab928..ba58019 100644 --- a/editor/src/components/property-validation-error-component.ts +++ b/editor/src/components/property-validation-error-component.ts @@ -1,4 +1,4 @@ -import { CustomValidator, CustomValidatorContext } from 'sequential-workflow-editor-model'; +import { PropertyValidator, PropertyValidatorContext } from 'sequential-workflow-editor-model'; import { Component } from './component'; import { validationErrorComponent } from './validation-error-component'; @@ -7,8 +7,8 @@ export interface PropertyValidationErrorComponent extends Component { } export function propertyValidationErrorComponent( - validator: CustomValidator, - context: CustomValidatorContext + validator: PropertyValidator, + context: PropertyValidatorContext ): PropertyValidationErrorComponent { const validation = validationErrorComponent(); diff --git a/editor/src/editor-provider.ts b/editor/src/editor-provider.ts index a2dab50..4b2a17f 100644 --- a/editor/src/editor-provider.ts +++ b/editor/src/editor-provider.ts @@ -1,6 +1,13 @@ import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model'; -import { Editor } from './editor'; -import { DefinitionContext, DefinitionModel, ModelActivator, DefinitionValidator, Path } from 'sequential-workflow-editor-model'; +import { Editor, EditorValidator } from './editor'; +import { + DefinitionContext, + DefinitionModel, + ModelActivator, + DefinitionValidator, + Path, + StepValidatorContext +} from 'sequential-workflow-editor-model'; import { EditorServices, ValueEditorEditorFactoryResolver } from './value-editors'; import { GlobalEditorContext, @@ -43,7 +50,7 @@ export class EditorProvider { return (definition: Definition, context: GlobalEditorContext): HTMLElement => { const rootContext = DefinitionContext.createForRoot(definition, this.definitionModel, this.definitionWalker); const typeClassName = 'root'; - const editor = Editor.create(null, this.definitionModel.root.properties, rootContext, this.services, typeClassName); + const editor = Editor.create(null, null, this.definitionModel.root.properties, rootContext, this.services, typeClassName); editor.onValuesChanged.subscribe(() => { context.notifyPropertiesChanged(); }); @@ -65,7 +72,14 @@ export class EditorProvider { description: stepModel.description }; - const editor = Editor.create(headerData, propertyModels, definitionContext, this.services, typeClassName); + let validator: EditorValidator | null = null; + if (stepModel.validator) { + const stepValidator = stepModel.validator; + const stepValidatorContext = StepValidatorContext.create(definitionContext); + validator = () => stepValidator.validate(stepValidatorContext); + } + + const editor = Editor.create(headerData, validator, propertyModels, definitionContext, this.services, typeClassName); editor.onValuesChanged.subscribe((paths: Path[]) => { if (paths.some(path => path.equals(stepModel.name.value.path))) { diff --git a/editor/src/editor.ts b/editor/src/editor.ts index cde1449..72e8c15 100644 --- a/editor/src/editor.ts +++ b/editor/src/editor.ts @@ -3,10 +3,14 @@ import { PropertyEditor } from './property-editor/property-editor'; import { EditorServices, ValueEditorEditorFactoryResolver } from './value-editors'; import { EditorHeader, EditorHeaderData } from './editor-header'; import { StackedSimpleEvent } from './core/stacked-simple-event'; +import { ValidationErrorComponent, validationErrorComponent } from './components/validation-error-component'; + +export type EditorValidator = () => string | null; export class Editor { public static create( headerData: EditorHeaderData | null, + validator: EditorValidator | null, propertyModels: PropertyModels, definitionContext: DefinitionContext, editorServices: EditorServices, @@ -20,6 +24,12 @@ export class Editor { root.appendChild(header.view); } + let validationComponent: ValidationErrorComponent | null = null; + if (validator) { + validationComponent = validationErrorComponent(); + root.appendChild(validationComponent.view); + } + const editors = new Map(); for (const propertyModel of propertyModels) { if (ValueEditorEditorFactoryResolver.isHidden(propertyModel.value.id)) { @@ -31,14 +41,27 @@ export class Editor { editors.set(propertyModel, propertyEditor); } - const editor = new Editor(root, editors); + const editor = new Editor(root, editors, validator, validationComponent); editors.forEach(e => e.onValueChanged.subscribe(editor.onValueChangedHandler)); + editor.validate(); return editor; } public readonly onValuesChanged = new StackedSimpleEvent(); - private constructor(public readonly root: HTMLElement, private readonly editors: Map) {} + private constructor( + public readonly root: HTMLElement, + private readonly editors: Map, + private readonly validator: EditorValidator | null, + private readonly validationErrorComponent: ValidationErrorComponent | null + ) {} + + private validate() { + if (this.validator && this.validationErrorComponent) { + const error = this.validator(); + this.validationErrorComponent.setError(error); + } + } private readonly onValueChangedHandler = (path: Path) => { this.onValuesChanged.push(path); diff --git a/editor/src/property-editor/property-editor.ts b/editor/src/property-editor/property-editor.ts index c4f0947..07aa3d6 100644 --- a/editor/src/property-editor/property-editor.ts +++ b/editor/src/property-editor/property-editor.ts @@ -1,5 +1,5 @@ import { - CustomValidatorContext, + PropertyValidatorContext, DefinitionContext, Path, PropertyModel, @@ -63,9 +63,9 @@ export class PropertyEditor implements Component { } let validationError: PropertyValidationErrorComponent | null = null; - if (propertyModel.customValidator) { - const customValidationContext = CustomValidatorContext.create(propertyModel, definitionContext); - validationError = propertyValidationErrorComponent(propertyModel.customValidator, customValidationContext); + if (propertyModel.validator) { + const validatorContext = PropertyValidatorContext.create(propertyModel, definitionContext); + validationError = propertyValidationErrorComponent(propertyModel.validator, validatorContext); view.appendChild(validationError.view); } diff --git a/model/package.json b/model/package.json index 3836dc5..728e04b 100644 --- a/model/package.json +++ b/model/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor-model", - "version": "0.7.1", + "version": "0.7.2", "homepage": "https://nocode-js.com/", "author": { "name": "NoCode JS", diff --git a/model/src/builders/property-model-builder.ts b/model/src/builders/property-model-builder.ts index f0947a5..ee272fa 100644 --- a/model/src/builders/property-model-builder.ts +++ b/model/src/builders/property-model-builder.ts @@ -1,6 +1,6 @@ import { Properties, PropertyValue } from 'sequential-workflow-model'; import { Path } from '../core/path'; -import { CustomValidator, PropertyModel, ValueModelFactory } from '../model'; +import { PropertyValidator, PropertyModel, ValueModelFactory } from '../model'; import { CircularDependencyDetector } from './circular-dependency-detector'; import { buildLabel } from '../core/label-builder'; @@ -9,7 +9,7 @@ export class PropertyModelBuilder): this { - if (this._customValidator) { + public validator(validator: PropertyValidator): this { + if (this._validator) { throw new Error('Custom validator is already set'); } - this._customValidator = customValidator; + this._validator = validator; return this; } + /** + * @deprecated Use `validator` instead. + */ + public readonly customValidator = this.validator.bind(this); + public build(): PropertyModel { if (!this._value) { throw new Error(`Model is not set for ${this.path.toString()}`); @@ -91,7 +96,7 @@ export class PropertyModelBuilder { }); }).toThrowError('It is not allowed to depend on dependency with dependency: properties/red <-> properties/pink'); }); + + it('sets default value mode for name if not specified', () => { + const model = createStepModel('blue', 'component', () => { + /* ... */ + }); + + expect(model.name.path.toString()).toBe('name'); + expect(model.name.value.id).toBe('string'); + }); + + it('creates model correctly', () => { + const validator: StepValidator = { + validate: () => null + }; + + const model = createStepModel('blue', 'component', builder => { + builder.category('Some category'); + builder.description('Some description'); + builder.label('Some label'); + builder.validator(validator); + builder.name().value(createStringValueModel({})); + builder.property('red').value(createStringValueModel({})); + builder.property('pink').value(createNumberValueModel({})); + }); + + expect(model.category).toBe('Some category'); + expect(model.description).toBe('Some description'); + expect(model.label).toBe('Some label'); + expect(model.validator).toBe(validator); + expect(model.name.path.toString()).toBe('name'); + expect(model.name.value.id).toBe('string'); + expect(model.properties[0].path.toString()).toBe('properties/red'); + expect(model.properties[0].value.id).toBe('string'); + expect(model.properties[1].path.toString()).toBe('properties/pink'); + expect(model.properties[1].value.id).toBe('number'); + }); }); diff --git a/model/src/builders/step-model-builder.ts b/model/src/builders/step-model-builder.ts index e0b429c..9c74cb7 100644 --- a/model/src/builders/step-model-builder.ts +++ b/model/src/builders/step-model-builder.ts @@ -1,6 +1,6 @@ import { ComponentType, Step } from 'sequential-workflow-model'; import { Path } from '../core/path'; -import { StepModel } from '../model'; +import { StepModel, StepValidator } from '../model'; import { createStringValueModel } from '../value-models'; import { PropertyModelBuilder } from './property-model-builder'; import { CircularDependencyDetector } from './circular-dependency-detector'; @@ -13,6 +13,7 @@ export class StepModelBuilder { private _label?: string; private _description?: string; private _category?: string; + private _validator?: StepValidator; private readonly nameBuilder = new PropertyModelBuilder(namePath, this.circularDependencyDetector); private readonly propertyBuilder: PropertyModelBuilder[] = []; @@ -33,6 +34,16 @@ export class StepModelBuilder { return this; } + /** + * Sets the description of the step. + * @param description The description of the step. + * @example `builder.description('This step does something useful.');` + */ + public description(description: string): this { + this._description = description; + return this; + } + /** * Sets the category of the step. This field is used in the toolbox to group steps. * @param category The category of the step. @@ -44,12 +55,11 @@ export class StepModelBuilder { } /** - * Sets the description of the step. - * @param description The description of the step. - * @example `builder.description('This step does something useful.');` + * Sets the validator of the step. + * @param validator The validator. */ - public description(description: string): this { - this._description = description; + public validator(validator: StepValidator): this { + this._validator = validator; return this; } @@ -90,6 +100,7 @@ export class StepModelBuilder { label: this._label ?? buildLabel(this.type), category: this._category, description: this._description, + validator: this._validator, name: this.nameBuilder.build(), properties: this.propertyBuilder.map(builder => builder.build()) }; diff --git a/model/src/context/definition-context.ts b/model/src/context/definition-context.ts index 64f4e9e..3e11249 100644 --- a/model/src/context/definition-context.ts +++ b/model/src/context/definition-context.ts @@ -1,6 +1,6 @@ import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model'; import { DefinitionModel } from '../model'; -import { VariablesProvider } from './variables-provider'; +import { ParentsProvider } from './variables-provider'; export class DefinitionContext { public static createForStep( @@ -9,8 +9,8 @@ export class DefinitionContext { definitionModel: DefinitionModel, definitionWalker: DefinitionWalker ): DefinitionContext { - const variablesProvider = VariablesProvider.createForStep(step, definition, definitionModel, definitionWalker); - return new DefinitionContext(step, definition, definitionModel, variablesProvider); + const parentsProvider = ParentsProvider.createForStep(step, definition, definitionModel, definitionWalker); + return new DefinitionContext(step, definition, definitionModel, parentsProvider); } public static createForRoot( @@ -18,14 +18,14 @@ export class DefinitionContext { definitionModel: DefinitionModel, definitionWalker: DefinitionWalker ): DefinitionContext { - const variablesProvider = VariablesProvider.createForRoot(definition, definitionModel, definitionWalker); - return new DefinitionContext(definition, definition, definitionModel, variablesProvider); + const parentsProvider = ParentsProvider.createForRoot(definition, definitionModel, definitionWalker); + return new DefinitionContext(definition, definition, definitionModel, parentsProvider); } private constructor( public readonly object: Step | Definition, public readonly definition: Definition, public readonly definitionModel: DefinitionModel, - public readonly variablesProvider: VariablesProvider + public readonly parentsProvider: ParentsProvider ) {} } diff --git a/model/src/context/value-context.ts b/model/src/context/value-context.ts index c4e1e83..efbb2ec 100644 --- a/model/src/context/value-context.ts +++ b/model/src/context/value-context.ts @@ -52,7 +52,7 @@ export class ValueContext>( diff --git a/model/src/context/variables-provider.ts b/model/src/context/variables-provider.ts index b516e8c..3eba3de 100644 --- a/model/src/context/variables-provider.ts +++ b/model/src/context/variables-provider.ts @@ -4,22 +4,22 @@ import { DefinitionContext } from './definition-context'; import { PropertyModels } from '../model'; import { ValueContext } from './value-context'; -export class VariablesProvider { +export class ParentsProvider { public static createForStep( step: Step, definition: Definition, definitionModel: DefinitionModel, definitionWalker: DefinitionWalker - ): VariablesProvider { - return new VariablesProvider(step, definition, definitionModel, definitionWalker); + ): ParentsProvider { + return new ParentsProvider(step, definition, definitionModel, definitionWalker); } public static createForRoot( definition: Definition, definitionModel: DefinitionModel, definitionWalker: DefinitionWalker - ): VariablesProvider { - return new VariablesProvider(null, definition, definitionModel, definitionWalker); + ): ParentsProvider { + return new ParentsProvider(null, definition, definitionModel, definitionWalker); } private constructor( @@ -80,4 +80,12 @@ export class VariablesProvider { } } } + + public readonly getStepTypes = (): string[] => { + if (this.step) { + const parents = this.definitionWalker.getParents(this.definition, this.step); + return (parents.filter(p => typeof p === 'object') as Step[]).map(p => p.type); + } + return []; + }; } diff --git a/model/src/core/simple-event.spec.ts b/model/src/core/simple-event.spec.ts new file mode 100644 index 0000000..32eb43a --- /dev/null +++ b/model/src/core/simple-event.spec.ts @@ -0,0 +1,24 @@ +import { SimpleEvent } from './simple-event'; + +describe('SimpleEvent', () => { + it('forward() works as expected', () => { + const e = new SimpleEvent(); + + let counter = 0; + function listener() { + counter++; + } + + e.subscribe(listener); + e.forward(); + + expect(counter).toEqual(1); + expect(e.count()).toEqual(1); + + e.unsubscribe(listener); + e.forward(); + + expect(counter).toEqual(1); + expect(e.count()).toEqual(0); + }); +}); diff --git a/model/src/model.ts b/model/src/model.ts index 44e11a9..92aaf06 100644 --- a/model/src/model.ts +++ b/model/src/model.ts @@ -2,7 +2,7 @@ import { Definition, Properties, PropertyValue, Sequence } from 'sequential-work import { ValueModelId, ValueType, VariableDefinition } from './types'; import { Path } from './core/path'; import { ValueContext } from './context'; -import { CustomValidatorContext } from './validator'; +import { PropertyValidatorContext, StepValidatorContext } from './validator'; import { DefaultValueContext } from './context/default-value-context'; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -27,6 +27,7 @@ export interface StepModel { description?: string; name: PropertyModel; properties: PropertyModels; + validator?: StepValidator; } export type PropertyModels = PropertyModel[]; @@ -36,7 +37,7 @@ export interface PropertyModel; + validator?: PropertyValidator; value: ValueModel; } @@ -53,6 +54,10 @@ export type ValueModelFactoryFromModel; +export interface StepValidator { + validate(context: StepValidatorContext): string | null; +} + export interface ValueModel< TValue extends PropertyValue = PropertyValue, TConfiguration extends object = object, @@ -68,14 +73,14 @@ export interface ValueModel< validate(context: ValueContext>): ValidationResult; } -export interface CustomValidator { - validate(context: CustomValidatorContext): string | null; +export interface PropertyValidator { + validate(context: PropertyValidatorContext): string | null; } export type ValidationError = Record; export type ValidationResult = ValidationError | null; -export function createValidationSingleError(error: string): ValidationResult { +export function createValidationSingleError(error: string): ValidationError { return { $: error }; diff --git a/model/src/validator/definition-validator.spec.ts b/model/src/validator/definition-validator.spec.ts index 8db956c..1b0b3c9 100644 --- a/model/src/validator/definition-validator.spec.ts +++ b/model/src/validator/definition-validator.spec.ts @@ -27,7 +27,7 @@ describe('DefinitionValidator', () => { }) ); - builder.steps([ + builder.step( createStepModel('move', 'task', step => { step.property('delta').value( createNumberValueModel({ @@ -35,7 +35,7 @@ describe('DefinitionValidator', () => { }) ); }) - ]); + ); }); const walker = new DefinitionWalker(); const validator = DefinitionValidator.create(model, walker); diff --git a/model/src/validator/definition-validator.ts b/model/src/validator/definition-validator.ts index d4bd55e..8e9bfc1 100644 --- a/model/src/validator/definition-validator.ts +++ b/model/src/validator/definition-validator.ts @@ -1,8 +1,9 @@ import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model'; import { DefinitionModel, PropertyModel, PropertyModels, ValidationError, ValidationResult, createValidationSingleError } from '../model'; import { DefinitionContext, ValueContext } from '../context'; -import { CustomValidatorContext } from './custom-validator-context'; +import { PropertyValidatorContext } from './property-validator-context'; import { Path } from '../core'; +import { StepValidatorContext } from './step-validator-context'; export class DefinitionValidator { public static create(definitionModel: DefinitionModel, definitionWalker: DefinitionWalker): DefinitionValidator { @@ -33,7 +34,7 @@ export class DefinitionValidator { ...stepError, stepId: step.id }; - return false; // stop walking + return false; // stops walking } }); return result; @@ -47,6 +48,17 @@ export class DefinitionValidator { throw new Error(`Cannot find model for step type: ${step.type}`); } + if (stepModel.validator) { + const stepContext = StepValidatorContext.create(definitionContext); + const stepError = stepModel.validator.validate(stepContext); + if (stepError) { + return { + propertyPath: Path.root(), + error: createValidationSingleError(stepError) + }; + } + } + const nameError = this.validateProperty(stepModel.name, definitionContext); if (nameError) { return { @@ -82,11 +94,11 @@ export class DefinitionValidator { return valueError; } - if (propertyModel.customValidator) { - const customContext = CustomValidatorContext.create(propertyModel, definitionContext); - const customError = propertyModel.customValidator.validate(customContext); - if (customError) { - return createValidationSingleError(customError); + if (propertyModel.validator) { + const propertyContext = PropertyValidatorContext.create(propertyModel, definitionContext); + const propertyError = propertyModel.validator.validate(propertyContext); + if (propertyError) { + return createValidationSingleError(propertyError); } } return null; diff --git a/model/src/validator/index.ts b/model/src/validator/index.ts index 7f5f022..61bb31c 100644 --- a/model/src/validator/index.ts +++ b/model/src/validator/index.ts @@ -1,2 +1,3 @@ -export * from './custom-validator-context'; export * from './definition-validator'; +export * from './property-validator-context'; +export * from './step-validator-context'; diff --git a/model/src/validator/custom-validator-context.ts b/model/src/validator/property-validator-context.ts similarity index 50% rename from model/src/validator/custom-validator-context.ts rename to model/src/validator/property-validator-context.ts index 5e83f0f..73cc641 100644 --- a/model/src/validator/custom-validator-context.ts +++ b/model/src/validator/property-validator-context.ts @@ -3,15 +3,15 @@ import { PropertyModel } from '../model'; import { DefinitionContext } from '../context'; import { readPropertyValue } from '../context/read-property-value'; -export class CustomValidatorContext { +export class PropertyValidatorContext { public static create( propertyModel: PropertyModel, definitionContext: DefinitionContext - ): CustomValidatorContext { - return new CustomValidatorContext(propertyModel, definitionContext); + ): PropertyValidatorContext { + return new PropertyValidatorContext(propertyModel, definitionContext); } - private constructor(private readonly model: PropertyModel, private readonly definitionContext: DefinitionContext) {} + protected constructor(private readonly model: PropertyModel, private readonly definitionContext: DefinitionContext) {} public getValue(): TValue { return this.model.path.read(this.definitionContext.object) as TValue; @@ -21,3 +21,11 @@ export class CustomValidatorContext extends PropertyValidatorContext {} diff --git a/model/src/validator/step-validator-context.ts b/model/src/validator/step-validator-context.ts new file mode 100644 index 0000000..7e86e45 --- /dev/null +++ b/model/src/validator/step-validator-context.ts @@ -0,0 +1,16 @@ +import { DefinitionContext } from '../context'; +import { ParentsProvider } from '../context/variables-provider'; + +export class StepValidatorContext { + public static create(definitionContext: DefinitionContext): StepValidatorContext { + return new StepValidatorContext(definitionContext.parentsProvider); + } + + private constructor(private readonly parentsProvider: ParentsProvider) {} + + /** + * @returns The parent step types. + * @example `['loop', 'if']` + */ + public readonly getParentStepTypes = this.parentsProvider.getStepTypes; +}