Skip to content

Commit

Permalink
Added object validator
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei15193 committed Jul 7, 2024
1 parent 181e8a9 commit c113cc5
Show file tree
Hide file tree
Showing 19 changed files with 623 additions and 14 deletions.
4 changes: 2 additions & 2 deletions src/collections/observableMap/IMapChangedEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IEventHandler } from "../../events";
import type { IMapChange } from "./IMapChange";
import type { IEventHandler } from '../../events';
import type { IMapChange } from './IMapChange';

/**
* A specialized interface for handling map changed events.
Expand Down
2 changes: 1 addition & 1 deletion src/collections/observableMap/IObservableMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IReadOnlyObservableMap } from "./IReadOnlyObservableMap";
import type { IReadOnlyObservableMap } from './IReadOnlyObservableMap';

/**
* Represents an observable map based on the [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) interface.
Expand Down
4 changes: 2 additions & 2 deletions src/collections/observableMap/IReadOnlyObservableMap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { INotifyPropertiesChanged } from "../../viewModels";
import type { INotifyMapChanged } from "./INotifyMapChanged";
import type { INotifyPropertiesChanged } from '../../viewModels';
import type { INotifyMapChanged } from './INotifyMapChanged';

/**
* Represents a read-only observable map based on the [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) interface.
Expand Down
4 changes: 2 additions & 2 deletions src/collections/observableMap/ObservableMap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IObservableMap } from "./IObservableMap";
import { ReadOnlyObservableMap } from "./ReadOnlyObservableMap";
import type { IObservableMap } from './IObservableMap';
import { ReadOnlyObservableMap } from './ReadOnlyObservableMap';

export class ObservableMap<TKey, TItem> extends ReadOnlyObservableMap<TKey, TItem> implements IObservableMap<TKey, TItem> {
/**
Expand Down
10 changes: 5 additions & 5 deletions src/collections/observableMap/ReadOnlyObservableMap.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { IMapChange } from "./IMapChange";
import type { IMapChangedEvent } from "./IMapChangedEvent";
import type { IReadOnlyObservableMap } from "./IReadOnlyObservableMap";
import { EventDispatcher } from "../../events";
import { ViewModel } from "../../viewModels";
import type { IMapChange } from './IMapChange';
import type { IMapChangedEvent } from './IMapChangedEvent';
import type { IReadOnlyObservableMap } from './IReadOnlyObservableMap';
import { EventDispatcher } from '../../events';
import { ViewModel } from '../../viewModels';

/**
* Represents a read-only observable map based on the [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) interface.
Expand Down
4 changes: 2 additions & 2 deletions src/collections/observableMap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export type { IMapChangedEventHandler } from './IMapChangedEventHandler';
export type { IReadOnlyObservableMap } from './IReadOnlyObservableMap';
export type { IObservableMap } from './IObservableMap';

export type { ReadOnlyObservableMap } from './ReadOnlyObservableMap';
export type { ObservableMap } from './ObservableMap';
export { ReadOnlyObservableMap } from './ReadOnlyObservableMap';
export { ObservableMap } from './ObservableMap';
7 changes: 7 additions & 0 deletions src/validation/IReadOnlyValidatable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface IReadOnlyValidatable<TValidationError = string> {
readonly isValid: boolean;

readonly isInvalid: boolean;

readonly error: TValidationError | null;
}
5 changes: 5 additions & 0 deletions src/validation/IValidatable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IReadOnlyValidatable } from './IReadOnlyValidatable';

export interface IValidatable<TValidationError = string> extends IReadOnlyValidatable<TValidationError> {
error: TValidationError | null;
}
56 changes: 56 additions & 0 deletions src/validation/IValidationTrigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { INotifyPropertiesChanged, IPropertiesChangedEventHandler } from '../viewModels';
import type { INotifyCollectionChanged, INotifyCollectionReordered, INotifySetChanged, INotifyMapChanged, ICollectionChangedEventHandler, ICollectionReorderedEventHandler, ISetChangedEventHandler, IMapChangedEventHandler } from '../collections';

export type IValidationTrigger = INotifyPropertiesChanged | INotifyCollectionChanged<unknown> | INotifyCollectionReordered<unknown> | INotifySetChanged<unknown> | INotifyMapChanged<unknown, unknown>;

export interface IValidationTriggerEventHandlers<TKey = unknown, TItem = unknown> {
readonly propertiesChanged?: IPropertiesChangedEventHandler<INotifyPropertiesChanged>;

readonly collectionChanged?: ICollectionChangedEventHandler<INotifyCollectionChanged<TItem>, TItem>;
readonly collectionReordered?: ICollectionReorderedEventHandler<INotifyCollectionReordered<TItem>, TItem>;

readonly setChanged?: ISetChangedEventHandler<INotifySetChanged<TItem>, TItem>;
readonly mapChanged?: IMapChangedEventHandler<INotifyMapChanged<TItem, TKey>, TKey, TItem>;
}

export function subscribeToValidationTrigger<TKey = unknown, TItem = unknown>(validationTrigger: IValidationTrigger, eventHandlers: IValidationTriggerEventHandlers<TKey, TItem>): void {
let isSpecialized = false;

if (!!eventHandlers.collectionChanged && 'collectionChanged' in validationTrigger) {
isSpecialized = true;
validationTrigger.collectionChanged.subscribe(eventHandlers.collectionChanged);
}
if (!!eventHandlers.collectionReordered && 'collectionReordered' in validationTrigger) {
isSpecialized = true;
validationTrigger.collectionReordered.subscribe(eventHandlers.collectionReordered);
}

if (!!eventHandlers.setChanged && 'setChanged' in validationTrigger) {
isSpecialized = true;
validationTrigger.setChanged.subscribe(eventHandlers.setChanged);
}

if (!!eventHandlers.mapChanged && 'mapChanged' in validationTrigger) {
isSpecialized = true;
validationTrigger.mapChanged.subscribe(eventHandlers.mapChanged);
}

if (!isSpecialized && !!eventHandlers.propertiesChanged && 'propertiesChanged' in validationTrigger)
validationTrigger.propertiesChanged.subscribe(eventHandlers.propertiesChanged)
}

export function unsubscribeFromValidationTrigger<TKey = unknown, TItem = unknown>(validationTrigger: IValidationTrigger, eventHandlers: IValidationTriggerEventHandlers<TKey, TItem>): void {
if ('propertiesChanged' in validationTrigger && !!eventHandlers.propertiesChanged)
validationTrigger.propertiesChanged.unsubscribe(eventHandlers.propertiesChanged)

if ('collectionChanged' in validationTrigger && !!eventHandlers.collectionChanged)
validationTrigger.collectionChanged.unsubscribe(eventHandlers.collectionChanged);
if ('collectionReordered' in validationTrigger && !!eventHandlers.collectionReordered)
validationTrigger.collectionReordered.unsubscribe(eventHandlers.collectionReordered);

if ('setChanged' in validationTrigger && !!eventHandlers.setChanged)
validationTrigger.setChanged.unsubscribe(eventHandlers.setChanged);

if ('mapChanged' in validationTrigger && !!eventHandlers.mapChanged)
validationTrigger.mapChanged.unsubscribe(eventHandlers.mapChanged);
}
10 changes: 10 additions & 0 deletions src/validation/IValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { IReadOnlyValidatable } from './IReadOnlyValidatable';

export interface IValidator<TValidatable extends IReadOnlyValidatable<TValidationError>, TValidationError = string> {
onAdd?(validatable: TValidatable): void;
onRemove?(validatable: TValidatable): void;

readonly validate: ValidatorCallback<TValidatable, TValidationError>;
}

export type ValidatorCallback<TValidatable extends IReadOnlyValidatable<TValidationError>, TValidationError = string> = (object: TValidatable) => TValidationError | null | undefined;
12 changes: 12 additions & 0 deletions src/validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type { IReadOnlyValidatable } from './IReadOnlyValidatable';
export type { IValidatable } from './IValidatable';

export type { IValidationTrigger } from './IValidationTrigger';

export type { IValidator, ValidatorCallback } from './IValidator';

export {
type IReadOnlyObjectValidator,
type IObjectValidator,
ObjectValidator
} from './objectValidator';
20 changes: 20 additions & 0 deletions src/validation/objectValidator/IObjectValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { IObservableCollection, IObservableSet } from '../../collections';
import type { IValidator, ValidatorCallback } from '../IValidator';
import type { IValidationTrigger } from '../IValidationTrigger';
import type { IReadOnlyObjectValidator } from './IReadOnlyObjectValidator';
import type { IValidatable } from '../IValidatable';

export interface IObjectValidator<TValidatable extends IValidatable<TValidationError> & IValidationTrigger, TValidationError = string> extends IReadOnlyObjectValidator<TValidatable, TValidationError> {
readonly validators: IObservableCollection<IValidator<TValidatable, TValidationError>>;

readonly triggers: IObservableSet<IValidationTrigger>;

add(validator: IValidator<TValidatable, TValidationError>): this;
add(validator: IValidator<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this;
add(validator: ValidatorCallback<TValidatable, TValidationError>): this;
add(validator: ValidatorCallback<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this;

validate(): TValidationError | null;

reset(): this;
}
14 changes: 14 additions & 0 deletions src/validation/objectValidator/IReadOnlyObjectValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { IReadOnlyValidatable } from '../IReadOnlyValidatable';
import type { IReadOnlyObservableCollection, IReadOnlyObservableSet } from '../../collections';
import type { IValidator } from '../IValidator';
import type { IValidationTrigger } from '../IValidationTrigger';

export interface IReadOnlyObjectValidator<TValidatable extends IReadOnlyValidatable<TValidationError> & IValidationTrigger, TValidationError = string> {
readonly target: TValidatable;

readonly validators: IReadOnlyObservableCollection<IValidator<TValidatable, TValidationError>>;

readonly triggers: IReadOnlyObservableSet<IValidationTrigger>;

validate(): TValidationError | null;
}
159 changes: 159 additions & 0 deletions src/validation/objectValidator/ObjectValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type { IValidatable } from '../IValidatable';
import type { IValidator, ValidatorCallback } from '../IValidator';
import type { IObjectValidator } from './IObjectValidator';
import { type INotifyPropertiesChanged } from '../../viewModels';
import { type IObservableCollection, type INotifyCollectionChanged, type ICollectionChange, type INotifyCollectionReordered, type ICollectionReorder, type INotifySetChanged, type IObservableSet, type ISetChange, type INotifyMapChanged, type IMapChange, ObservableCollection, ObservableSet } from '../../collections';
import { type IValidationTrigger, type IValidationTriggerEventHandlers, subscribeToValidationTrigger, unsubscribeFromValidationTrigger } from '../IValidationTrigger';

export class ObjectValidator<TValidatable extends IValidatable<TValidationError> & IValidationTrigger, TValidationError = string> implements IObjectValidator<TValidatable, TValidationError> {
private _isValidating: boolean;
private readonly _triggerChangedEventHandlers: IValidationTriggerEventHandlers;

public constructor(target: TValidatable) {
this.target = target;
this._triggerChangedEventHandlers = {
propertiesChanged: {
handle: (validationTrigger, changedProperties) => {
if (this.shouldValidateViewModelTrigger(validationTrigger, changedProperties))
this.validate();
}
},

collectionChanged: {
handle: (validationTrigger, collectionChange) => {
if (this.shouldValidateCollectionChangedTrigger(validationTrigger, collectionChange))
this.validate();
}
},

collectionReordered: {
handle: (validationTrigger, collectionReorder) => {
if (this.shouldValidateCollectionReorderedTrigger(validationTrigger, collectionReorder))
this.validate();
}
},

setChanged: {
handle: (validationTrigger, setChanged) => {
if (this.shouldValidateSetChangedTrigger(validationTrigger, setChanged))
this.validate();
}
},

mapChanged: {
handle: (validationTrigger, mapChanged) => {
if (this.shouldValidateMapChangedTrigger(validationTrigger, mapChanged))
this.validate();
}
}
};

subscribeToValidationTrigger(this.target, this._triggerChangedEventHandlers);

this.validators = new ObservableCollection<IValidator<TValidatable, TValidationError>>();
this.validators.collectionChanged.subscribe({
handle: (_, { addedItems: addedValidators, removedItems: removedValidators }) => {
removedValidators.forEach(removedValidator => removedValidator.onRemove && removedValidator.onRemove(this.target));
addedValidators.forEach(addedValidator => addedValidator.onAdd && addedValidator.onAdd(this.target));

this.validate();
}
});

this.triggers = new ObservableSet<INotifyPropertiesChanged>();
this.triggers.setChanged.subscribe({
handle: (_, { addedItems: addedTriggers, removedItems: removedTriggers }) => {
removedTriggers.forEach(removedTrigger => {
unsubscribeFromValidationTrigger(removedTrigger, this._triggerChangedEventHandlers);
});

addedTriggers.forEach(addedTrigger => {
subscribeToValidationTrigger(addedTrigger, this._triggerChangedEventHandlers);
});
}
});

try {
this._isValidating = true;

this.target.error = null;
}
finally {
this._isValidating = false;
}
}

public readonly target: TValidatable;

public readonly validators: IObservableCollection<IValidator<TValidatable, TValidationError>>;

public readonly triggers: IObservableSet<IValidationTrigger>;

public add(validator: IValidator<TValidatable, TValidationError>): this;
public add(validator: IValidator<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this;
public add(validator: ValidatorCallback<TValidatable, TValidationError>): this;
public add(validator: ValidatorCallback<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this;

public add(validator: IValidator<TValidatable, TValidationError> | ValidatorCallback<TValidatable, TValidationError>, triggers?: readonly IValidationTrigger[]): this {
if (triggers !== null && triggers !== undefined)
triggers.forEach(trigger => this.triggers.add(trigger));

if (typeof validator === 'function')
this.validators.push({ validate: validator });
else
this.validators.push(validator);

return this;
}

public validate(): TValidationError | null {
let error: TValidationError | null = null;
let index: number = 0;

while (index < this.validators.length && error === null) {
const validationResult = this.validators[index].validate(this.target);
if (validationResult !== null && validationResult !== undefined)
error = validationResult;
else
index++;
}

try {
this._isValidating = true;

this.target.error = error;
return error;
}
finally {
this._isValidating = false;
}

}

public reset(): this {
this.triggers.clear();
this.validators.splice(0);

return this;
}

protected shouldValidateViewModelTrigger(changedViewModel: INotifyPropertiesChanged, changedProperties: readonly PropertyKey[]): boolean {
return (!this._isValidating || this.target !== changedViewModel);
}

protected shouldValidateCollectionChangedTrigger(changedCollection: INotifyCollectionChanged<unknown>, collectionChange: ICollectionChange<unknown>): boolean {
return true;
}

protected shouldValidateCollectionReorderedTrigger(changedCollection: INotifyCollectionReordered<unknown>, collectionReorder: ICollectionReorder<unknown>): boolean {
return true;
}

protected shouldValidateSetChangedTrigger(changedSet: INotifySetChanged<unknown>, setChange: ISetChange<unknown>): boolean {
return true;
}

protected shouldValidateMapChangedTrigger(changedMap: INotifyMapChanged<unknown, unknown>, mapChange: IMapChange<unknown, unknown>): boolean {
return true;
}
}
3 changes: 3 additions & 0 deletions src/validation/objectValidator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { IReadOnlyObjectValidator } from './IReadOnlyObjectValidator';
export type { IObjectValidator } from './IObjectValidator';
export { ObjectValidator } from './ObjectValidator';
Loading

0 comments on commit c113cc5

Please sign in to comment.