Skip to content

Commit

Permalink
feat: add MapValidator
Browse files Browse the repository at this point in the history
  • Loading branch information
Khasms committed Jan 24, 2022
1 parent ce04dc0 commit e4d60dc
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ s.enum('Red', 'Green', 'Blue');
// s.union(s.literal('Red'), s.literal('Green'), s.literal('Blue'));
```

#### Maps // TODO
#### Maps

```typescript
const map = s.map(s.string, s.number);
Expand Down
7 changes: 6 additions & 1 deletion src/lib/Shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
PassthroughValidator,
SetValidator,
StringValidator,
UnionValidator
UnionValidator,
MapValidator
} from '../validators/imports';
import type { Constructor, MappedObjectValidator } from './util-types';

Expand Down Expand Up @@ -90,4 +91,8 @@ export class Shapes {
public set<T>(validator: BaseValidator<T>) {
return new SetValidator(validator);
}

public map<T, U>(keyValidator: BaseValidator<T>, valueValidator: BaseValidator<U>) {
return new MapValidator(keyValidator, valueValidator);
}
}
1 change: 1 addition & 0 deletions src/type-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ export type { PassthroughValidator } from './validators/PassthroughValidator';
export type { SetValidator } from './validators/SetValidator';
export type { StringValidator } from './validators/StringValidator';
export type { UnionValidator } from './validators/UnionValidator';
export type { MapValidator } from './validators/MapValidator';
40 changes: 40 additions & 0 deletions src/validators/MapValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { IConstraint } from '../constraints/base/IConstraint';
import { ValidationError } from '../lib/errors/ValidationError';
import { Result } from '../lib/Result';
import { BaseValidator } from './imports';

export class MapValidator<K, V> extends BaseValidator<Map<K, V>> {
private readonly keyValidator: BaseValidator<K>;
private readonly valueValidator: BaseValidator<V>;

public constructor(keyValidator: BaseValidator<K>, valueValidator: BaseValidator<V>, constraints: readonly IConstraint<Map<K, V>>[] = []) {
super(constraints);
this.keyValidator = keyValidator;
this.valueValidator = valueValidator;
}

protected override clone(): this {
return Reflect.construct(this.constructor, [this.keyValidator, this.valueValidator, this.constraints]);
}

protected handle(values: unknown): Result<Map<K, V>, ValidationError | AggregateError> {
if (!(values instanceof Map)) {
return Result.err(new ValidationError('MapValidator', 'Expected a map', values));
}

const errors: Error[] = [];
const transformed = new Map<K, V>();

for (const [key, value] of values.entries()) {
const keyResult = this.keyValidator.run(key);
const valueResult = this.valueValidator.run(value);
const results = [keyResult, valueResult].filter((result) => result.isErr());
if (results.length === 0) transformed.set(keyResult.value!, valueResult.value!);
else errors.push(...results.map((result) => result.error!));
}

return errors.length === 0 //
? Result.ok(transformed)
: Result.err(new AggregateError(errors, 'Failed to validate at least one entry'));
}
}
1 change: 1 addition & 0 deletions src/validators/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './PassthroughValidator';
export * from './SetValidator';
export * from './StringValidator';
export * from './UnionValidator';
export * from './MapValidator';
35 changes: 35 additions & 0 deletions tests/validators/map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { s, ValidationError } from '../../src';

describe('MapValidator', () => {
const value = new Map();
value.set('a', 1);
value.set('b', 2);
const predicate = s.map(s.string, s.number);

test('GIVEN a non-map THEN throws ValidationError', () => {
expect(() => predicate.parse(false)).toThrow(new ValidationError('MapValidator', 'Expected a map', false));
});

test('GIVEN a matching map THEN returns a map', () => {
expect(predicate.parse(value)).toStrictEqual(value);
});

test('GIVEN a non-matching map THEN throws AggregateError', () => {
const map = new Map();
map.set('a', 1);
map.set('foo', 'bar');
map.set(2, 'fizz');
map.set(3, 'buzz');
expect(() => predicate.parse(map)).toThrow(
new AggregateError(
[
new ValidationError('NumberValidator', 'Expected a number primitive', 'bar'),
new ValidationError('StringValidator', 'Expected a string primitive', 1),
new ValidationError('StringValidator', 'Expected a string primitive', 3),
new ValidationError('NumberValidator', 'Expected a number primitive', 'buzz')
],
'Failed to validate at least one entry'
)
);
});
});

0 comments on commit e4d60dc

Please sign in to comment.