Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Map predicate #36

Merged
merged 2 commits into from
Dec 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {BooleanPredicate} from './lib/predicates/boolean';
import {ArrayPredicate} from './lib/predicates/array';
import {DatePredicate} from './lib/predicates/date';
import {ErrorPredicate} from './lib/predicates/error';
import {MapPredicate} from './lib/predicates/map';

/**
* @hidden
Expand Down Expand Up @@ -50,6 +51,10 @@ export interface Ow {
* Test the value to be an Error.
*/
readonly error: ErrorPredicate;
/**
* Test the value to be a Map.
*/
readonly map: MapPredicate;
/**
* Test the value to be a Function.
*/
Expand Down Expand Up @@ -153,6 +158,9 @@ Object.defineProperties(main, {
error: {
get: () => new ErrorPredicate()
},
map: {
get: () => new MapPredicate()
},
function: {
get: () => new Predicate('function')
},
Expand Down
215 changes: 215 additions & 0 deletions source/lib/predicates/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import * as isEqual from 'lodash.isequal';
import ow from '../..';
import {Predicate, Context} from './predicate';

export class MapPredicate extends Predicate<Map<any, any>> {
constructor(context?: Context) {
super('map', context);
}

/**
* Test a Map to have a specific size.
*
* @param size The size of the Map.
*/
size(size: number) {
return this.addValidator({
message: map => `Expected Map to have size \`${size}\`, got \`${map.size}\``,
validator: map => map.size === size
});
}

/**
* Test an Map to have a minimum size.
*
* @param size The minimum size of the Map.
*/
minSize(size: number) {
return this.addValidator({
message: map => `Expected Map to have a minimum size of \`${size}\`, got \`${map.size}\``,
validator: map => map.size >= size
});
}

/**
* Test an Map to have a maximum size.
*
* @param size The maximum size of the Map.
*/
maxSize(size: number) {
return this.addValidator({
message: map => `Expected Map to have a maximum size of \`${size}\`, got \`${map.size}\``,
validator: map => map.size <= size
});
}

/**
* Test a Map to include all the provided keys. The keys are tested by identity, not structure.
*
* @param keys The keys that should be a key in the Map.
*/
hasKeys(...keys: any[]) {
const missingKeys: any[] = [];

return this.addValidator({
message: () => `Expected Map to have keys \`${JSON.stringify(missingKeys)}\``,
validator: map => {
for (const key of keys) {
if (map.has(key)) {
continue;
}

missingKeys.push(key);

if (missingKeys.length === 5) {
return false;
}
}

return missingKeys.length === 0;
}
});
}

/**
* Test a Map to include any of the provided keys. The keys are tested by identity, not structure.
*
* @param keys The keys that could be a key in the Map.
*/
hasAnyKeys(...keys: any[]) {
return this.addValidator({
message: () => `Expected Map to have any key of \`${JSON.stringify(keys)}\``,
validator: map => keys.some(key => map.has(key))
});
}

/**
* Test a Map to include all the provided values. The values are tested by identity, not structure.
*
* @param values The values that should be a value in the Map.
*/
hasValues(...values: any[]) {
const missingValues: any[] = [];

return this.addValidator({
message: () => `Expected Map to have values \`${JSON.stringify(missingValues)}\``,
validator: map => {
const valueSet = new Set(map.values());

for (const value of values) {
if (valueSet.has(value)) {
continue;
}

missingValues.push(value);

if (missingValues.length === 5) {
return false;
}
}

return missingValues.length === 0;
}
});
}

/**
* Test a Map to include any of the provided values. The values are tested by identity, not structure.
*
* @param values The values that could be a value in the Map.
*/
hasAnyValues(...values: any[]) {
return this.addValidator({
message: () => `Expected Map to have any value of \`${JSON.stringify(values)}\``,
validator: map => {
const valueSet = new Set(map.values());

return values.some(key => valueSet.has(key));
}
});
}

/**
* Test all the keys in the Map to match the provided predicate.
*
* @param predicate The predicate that should be applied against every key in the Map.
*/
keysOfType<T>(predicate: Predicate<T>) {
let error: string;

return this.addValidator({
message: () => error,
validator: map => {
try {
for (const item of map.keys()) {
ow(item, predicate);
}

return true;
} catch (err) {
error = err.message;

return false;
}
}
});
}

/**
* Test all the values in the Map to match the provided predicate.
*
* @param predicate The predicate that should be applied against every value in the Map.
*/
valuesOfType<T>(predicate: Predicate<T>) {
let error: string;

return this.addValidator({
message: () => error,
validator: map => {
try {
for (const item of map.values()) {
ow(item, predicate);
}

return true;
} catch (err) {
error = err.message;

return false;
}
}
});
}

/**
* Test a Map to be empty.
*/
get empty() {
return this.addValidator({
message: map => `Expected Map to be empty, got \`${JSON.stringify(Array.from(map))}\``,
validator: map => map.size === 0
});
}

/**
* Test a Map to be not empty.
*/
get nonEmpty() {
return this.addValidator({
message: () => 'Expected Map to not be empty',
validator: map => map.size > 0
});
}

/**
* Test a Map to be deeply equal to the provided Map.
*
* @param expected Expected Map to match.
*/
deepEqual(expected: Map<any, any>) {
return this.addValidator({
message: map => `Expected Map to be deeply equal to \`${JSON.stringify(Array.from(expected))}\`, got \`${JSON.stringify(Array.from(map))}\``,
validator: map => isEqual(map, expected)
});
}
}
87 changes: 87 additions & 0 deletions source/test/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import test from 'ava';
import m from '..';

test('map', t => {
t.notThrows(() => m(new Map(), m.map));
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map));
t.throws(() => m(12 as any, m.map), 'Expected argument to be of type `map` but received type `number`');
});

test('map.size', t => {
t.notThrows(() => m(new Map(), m.map.size(0)));
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.size(1)));
t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.size(0)), 'Expected Map to have size `0`, got `1`');
});

test('map.minSize', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.minSize(1)));
t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.minSize(1)));
t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.minSize(2)), 'Expected Map to have a minimum size of `2`, got `1`');
});

test('map.maxSize', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.maxSize(1)));
t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.maxSize(4)));
t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.maxSize(1)), 'Expected Map to have a maximum size of `1`, got `2`');
});

test('map.hasKeys', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasKeys('unicorn')));
t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasKeys('unicorn', 'rainbow')));
t.notThrows(() => m(new Map([[1, '🦄'], [2, '🌈']]), m.map.hasKeys(1, 2)));
t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasKeys('foo')), 'Expected Map to have keys `["foo"]`');
t.throws(() => m(new Map([['unicorn', '🦄'], ['foo', '🌈']]), m.map.hasKeys('foo', 'bar')), 'Expected Map to have keys `["bar"]`');
t.throws(() => m(new Map([[2, '🦄'], [4, '🌈']]), m.map.hasKeys(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), 'Expected Map to have keys `[1,3,5,6,7]`');
});

test('map.hasAnyKeys', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasAnyKeys('unicorn', 'rainbow')));
t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyKeys('unicorn')));
t.notThrows(() => m(new Map([[1, '🦄'], [2, '🌈']]), m.map.hasAnyKeys(1, 2, 3, 4)));
t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyKeys('foo')), 'Expected Map to have any key of `["foo"]`');
});

test('map.hasValues', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasValues('🦄')));
t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasValues('🦄', '🌈')));
t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasValues('🦄', '🌦️')), 'Expected Map to have values `["🌦️"]`');
t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasValues('🌈', '⚡', '👓', '🐬', '🎃', '🎶', '❤', '️🐳', '🍀', '👽')), 'Expected Map to have values `["⚡","👓","🐬","🎃","🎶"]`');
});

test('map.hasAnyValues', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasAnyValues('🦄', '🌈')));
t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyValues('🦄')));
t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyValues('🌦️')), 'Expected Map to have any value of `["🌦️"]`');
});

test('map.keysOfType', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.keysOfType(m.string)));
t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.keysOfType(m.string.minLength(3))));
t.notThrows(() => m(new Map([[1, '🦄']]), m.map.keysOfType(m.number)));
t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.keysOfType(m.number)), 'Expected argument to be of type `number` but received type `string`');
});

test('map.valuesOfType', t => {
t.notThrows(() => m(new Map([['unicorn', 1]]), m.map.valuesOfType(m.number)));
t.notThrows(() => m(new Map([['unicorn', 10], ['rainbow', 11]]), m.map.valuesOfType(m.number.greaterThanOrEqual(10))));
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.valuesOfType(m.string)));
t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.valuesOfType(m.number)), 'Expected argument to be of type `number` but received type `string`');
});

test('map.empty', t => {
t.notThrows(() => m(new Map(), m.map.empty));
t.notThrows(() => m(new Map([]), m.map.empty));
t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.empty), 'Expected Map to be empty, got `[["unicorn","🦄"]]`');
});

test('map.notEmpty', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.nonEmpty));
t.throws(() => m(new Map(), m.map.nonEmpty), 'Expected Map to not be empty');
});

test('map.deepEqual', t => {
t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.deepEqual(new Map([['unicorn', '🦄']]))));
t.notThrows(() => m(new Map([['foo', {foo: 'bar'}]]), m.map.deepEqual(new Map([['foo', {foo: 'bar'}]]))));
t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.deepEqual(new Map([['rainbow', '🌈']]))), 'Expected Map to be deeply equal to `[["rainbow","🌈"]]`, got `[["unicorn","🦄"]]`');
t.throws(() => m(new Map([['foo', {foo: 'bar'}]]), m.map.deepEqual(new Map([['foo', {foo: 'baz'}]]))), 'Expected Map to be deeply equal to `[["foo",{"foo":"baz"}]]`, got `[["foo",{"foo":"bar"}]]`');
});