Skip to content

Commit

Permalink
Merge 934d5c3 into 74e465e
Browse files Browse the repository at this point in the history
  • Loading branch information
artem-zakharchenko committed May 29, 2019
2 parents 74e465e + 934d5c3 commit 39513e7
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 63 deletions.
6 changes: 2 additions & 4 deletions lib/next/test/unit/units/normalize/normalizeHeaders.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ const {

describe('normalizeHeaders', () => {
describe('when given nothing', () => {
const headers = normalizeHeaders(undefined);

it('coerces to empty object', () => {
assert.deepEqual(headers, {});
it('throws upon invalid headers value', () => {
assert.throw(() => normalizeHeaders(undefined));
});
});

Expand Down
134 changes: 87 additions & 47 deletions lib/next/test/unit/utils/evolve.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,71 +11,111 @@ const unexpectedTypes = [
];

describe('evolve', () => {
describe('evolves a given object', () => {
const res = evolve({
a: multiply(2),
c: () => null
})({
a: 2,
b: 'foo'
});
describe('weak mode', () => {
describe('evolves given object', () => {
const result = evolve({
a: multiply(2),
c: () => null
})({
a: 2,
b: 'foo'
});

it('returns object', () => {
assert.isObject(res);
});
it('returns object', () => {
assert.isObject(result);
});

it('evolves matching properties', () => {
assert.propertyVal(res, 'a', 4);
});
it('evolves matching properties', () => {
assert.propertyVal(result, 'a', 4);
});

it('bypasses properties not in schema', () => {
assert.propertyVal(res, 'b', 'foo');
});
it('bypasses properties not in schema', () => {
assert.propertyVal(result, 'b', 'foo');
});

it('ignores properties not in data', () => {
assert.notProperty(res, 'c');
it('ignores properties not in data', () => {
assert.notProperty(result, 'c');
});
});
});

describe('evolves a given array', () => {
const res = evolve({
0: multiply(2),
1: multiply(3),
3: multiply(4)
})([1, 2, 3]);
describe('evolves given array', () => {
const result = evolve({
0: multiply(2),
1: multiply(3),
3: multiply(4)
})([1, 2, 3]);

it('returns array', () => {
assert.isArray(res);
});
it('returns array', () => {
assert.isArray(result);
});

it('evolves matching keys', () => {
assert.propertyVal(res, 0, 2);
assert.propertyVal(res, 1, 6);
});
it('evolves matching properties', () => {
assert.propertyVal(result, 0, 2);
assert.propertyVal(result, 1, 6);
});

it('bypasses keys not in schema', () => {
assert.propertyVal(res, 2, 3);
it('bypasses properties not in schema', () => {
assert.propertyVal(result, 2, 3);
});

it('ignores properties not in data', () => {
assert.notProperty(result, 3);
});
});

it('ignores properties not in data', () => {
assert.notProperty(res, 3);
describe('throws when given unexpected schema', () => {
unexpectedTypes
.concat([['array', [1, 2]]])
.forEach(([typeName, dataType]) => {
it(`when given ${typeName}`, () => {
assert.throw(() => evolve(dataType)({}));
});
});
});
});

describe('throws when given unexpected schema', () => {
unexpectedTypes
.concat([['array', [1, 2]]])
.forEach(([typeName, dataType]) => {
describe('throws when given unexpected data', () => {
unexpectedTypes.forEach(([typeName, dataType]) => {
it(`when given ${typeName}`, () => {
assert.throw(() => evolve(dataType)({}));
assert.throw(() => evolve({ a: () => null })(dataType));
});
});
});
});

describe('throws when given unexpected data', () => {
unexpectedTypes.forEach(([typeName, dataType]) => {
it(`when given ${typeName}`, () => {
assert.throw(() => evolve({ a: () => null })(dataType));
/**
* Strict mode
*/
describe('strict mode', () => {
describe('evolves given object', () => {
const result = evolve(
{
method: () => 'GET',
headers: () => 'foo',
body: multiply(2)
},
{
strict: true
}
)({
statusCode: 200,
body: 5
});

it('returns an object', () => {
assert.isObject(result);
});

it('evolves matching properties', () => {
assert.propertyVal(result, 'body', 10);
});

it('forces all properties from schema', () => {
assert.propertyVal(result, 'headers', 'foo');
assert.propertyVal(result, 'method', 'GET');
});

it('bypasses properties not present in schema', () => {
assert.propertyVal(result, 'statusCode', 200);
});
});
});
Expand Down
8 changes: 8 additions & 0 deletions lib/next/units/coerce/coerceHeaders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Coerces given headers to an empty Object in case not present.
// Conceptually, diff between missing headers and empty headers
// should be treated the same.
const coerceHeaders = (headers) => {
return !headers ? {} : headers;
};

module.exports = { coerceHeaders };
15 changes: 15 additions & 0 deletions lib/next/units/coerce/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const evolve = require('../../utils/evolve');
const { coerceHeaders } = require('./coerceHeaders');

const coercionMap = {
headers: coerceHeaders
};

// Coercion uses strict evolve to ensure the properties
// set in expected schema are set on the result object,
// even if not present in data object. This is what
// coercion is about, in the end.
const coerce = evolve(coercionMap, { strict: true });
const coerceWeak = evolve(coercionMap);

module.exports = { coerce, coerceWeak };
4 changes: 0 additions & 4 deletions lib/next/units/normalize/normalizeHeaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ const normalizeStringValue = (value) => {
* @returns {Object}
*/
const normalizeHeaders = (headers) => {
if (!headers) {
return {};
}

const headersType = typeof headers;
const isHeadersNull = headers == null;

Expand Down
27 changes: 21 additions & 6 deletions lib/next/utils/evolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
* Applies a given evolution schema to the given data Object.
* Properties not present in schema are bypassed.
* Properties not present in data are ignored.
* @param {Object} schema
* @param {Object|Array} data
* @returns {Object}
* @param {Object<string, any>} schema
* @param {any[]|Object<string, any>} data
* @returns {any[]|Object<string, any>}
*/
const evolve = (schema) => (data) => {
const evolve = (schema, { strict = false } = {}) => (data) => {
const dataType = typeof data;
const schemaType = typeof schema;
const isArray = Array.isArray(data);
Expand All @@ -24,10 +24,11 @@ const evolve = (schema) => (data) => {
);
}

return Object.keys(data).reduce((acc, key) => {
const reducer = (acc, key) => {
const value = data[key];
const transform = schema[key];
const transformType = typeof transform;

/* eslint-disable no-nested-ternary */
const nextValue =
transformType === 'function'
Expand All @@ -38,7 +39,21 @@ const evolve = (schema) => (data) => {
/* eslint-enable no-nested-ternary */

return isArray ? acc.concat(nextValue) : { ...acc, [key]: nextValue };
}, result);
};

const nextData = Object.keys(data).reduce(reducer, result);

if (strict) {
// Strict mode ensures all keys in expected schema are present
// in the returned payload.
return Object.keys(schema)
.filter((expectedKey) => {
return !Object.prototype.hasOwnProperty.call(data, expectedKey);
})
.reduce(reducer, nextData);
}

return nextData;
};

module.exports = evolve;
15 changes: 13 additions & 2 deletions lib/next/validateMessage.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
const isset = require('../utils/isset');
const { coerce, coerceWeak } = require('./units/coerce');
const { normalize } = require('./units/normalize');
const { isValid } = require('./units/isValid');
const { validateStatusCode } = require('./units/validateStatusCode');
const { validateHeaders } = require('./units/validateHeaders');
const { validateBody } = require('./units/validateBody');

function validateMessage(realMessage, expectedMessage) {
const real = normalize(realMessage);
const expected = normalize(expectedMessage);
const results = {};

// Uses strict coercion on real message.
// Strict coercion ensures real message has properties illegible
// for validation with the expected message.
const real = normalize(coerce(realMessage));

// Weak coercion applies transformation only to the properties
// present in the given message. We don't want to mutate user's assertion.
// However, we do want to use the same coercion logic we do
// for strict coercion. Thus normalization and coercion are separate.
const expected = normalize(coerceWeak(expectedMessage));

if (real.statusCode) {
results.statusCode = validateStatusCode(real, expected);
}
Expand All @@ -25,6 +35,7 @@ function validateMessage(realMessage, expectedMessage) {
results.body = validateBody(real, expected);
}

// Indicates the validity of a real message
results.isValid = isValid(results);

return results;
Expand Down

0 comments on commit 39513e7

Please sign in to comment.