From b45cda645b70abeebe2fcb2f2f4a2c39fced182a Mon Sep 17 00:00:00 2001 From: icey Date: Wed, 13 Oct 2021 21:26:48 +0800 Subject: [PATCH] Test and fix bugs --- README.md | 82 +----------------------------------------- package.json | 27 ++++++++++++++ test.cjs | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ test.js | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ typeAssert.cjs | 74 +++++++++++++++++++++----------------- typeAssert.js | 71 ++++++++++++++++++++---------------- 6 files changed, 304 insertions(+), 144 deletions(-) create mode 100644 package.json create mode 100644 test.cjs create mode 100644 test.js diff --git a/README.md b/README.md index 1b99c02..4a7ed19 100644 --- a/README.md +++ b/README.md @@ -2,84 +2,4 @@ Minimal JavaScript type assertions ## Basic usage -```javascript -// or you use import if use ESM -const { typeAssert } = require('./typeAssert.cjs') - -// simple types -typeAssert(1, 'number') -typeAssert('2', 'string') - -// object -typeAssert({ x: 1, y: 3 }, 'object') -typeAssert({ x: 1, y: 3 }, {}) - -// array -typeAssert([1, 2, 3], 'Array') -typeAssert([1, 2, 3], []) - -// object fields -typeAssert({ - x: 1, - y: '2' -}, { - x: 'number', - y: 'string' -}) - -// array elements -typeAssert([1, 2, 3], ['number']) - -// "sum" types -const assertion = 'string'.sumWith('number') -typeAssert('abc', assertion) -typeAssert(123, assertion) - -// nullable types -const assertion = { x: 'number', y: 'function' }.orNull() -typeAssert({ - x: 144, - y: () => {} -}, assertion) -typeAssert(null, assertion) - -// nullable shorthand for simple types -typeAssert(114, 'number?') -typeAssert(null, 'number?') - -// nested situation -const assertion = { - a: 'number', - b: 'string', - c: [ - { - x: 'function', - y: 'function' - }.sumWith({ - x: [], - y: {} - }).orNull() - ] -} - -typeAssert({ - a: 114, - b: '514', - c: [ - null, - { - x: [1, 2, 3], - y: { - z: '4' - } - }, - { - x: console.log, - y: Array.prototype.push - } - ] -}, assertion) - -// chained situation -typeAssert(5, 'number'.chainWith(x => x > 0 ? true : 'no negative numbers')) -``` +See `test.cjs` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a45624a --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "typeassert", + "version": "0.3.0", + "description": "A light weight type assertion \"framework\"?", + "type": "module", + "main": "", + "scripts": { + "test-esm": "node test.js", + "test-cjs": "node test.cjs" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/chuigda/typeAssert.git" + }, + "keywords": [ + "type", + "assertion", + "commonjs", + "esmodule" + ], + "author": "Chuigda", + "license": "MIT", + "bugs": { + "url": "https://github.com/chuigda/typeAssert/issues" + }, + "homepage": "https://github.com/chuigda/typeAssert#readme" +} diff --git a/test.cjs b/test.cjs new file mode 100644 index 0000000..c4b9362 --- /dev/null +++ b/test.cjs @@ -0,0 +1,97 @@ +const { typeAssert, enableChainAPI, preventErrTrace } = require('./typeAssert.cjs') + +preventErrTrace(true) +enableChainAPI() + +typeAssert(1, 'number') +typeAssert('2', 'string') + +const expectFailure = testedCode => { + try { + testedCode() + console.error('expected failure not caught') + process.exit(-1) + } catch (e) { + console.info('expected failure caught:', e) + } +} + +expectFailure(() => typeAssert(2, 'string')) + +typeAssert({ x: 1, y: '3' }, 'object') +typeAssert({ x: 1, y: '3' }, {}) +typeAssert({ x: 1, y: '3' }, { x: 'number', y: 'string' }) + +// By this time, arrays are also considered a kind of 'object'. +// this may change in further editions +typeAssert([], 'object') +typeAssert([], {}) + +expectFailure(() => typeAssert([], { x: 'number' })) +expectFailure(() => typeAssert({ x: 1, y: '2' }, { x: 'number', y: 'number' })) + +typeAssert([1, 2, 3], 'Array') +typeAssert([1, 2, 3], []) +typeAssert([1, 2, 3], ['number']) +typeAssert(['1', '2', '3'], ['string']) + +expectFailure(() => typeAssert({}, [])) +expectFailure(() => typeAssert({}, 'Array')) +expectFailure(() => typeAssert([1, 2, 3], ['string'])) + +const sumAssertion = 'string'.sumWith('number') +typeAssert('abc', sumAssertion) +typeAssert(123, sumAssertion) + +expectFailure(() => typeAssert(() => 114514, sumAssertion)) + +const nullableAssertion = { x: 'number', y: 'function' }.orNull() +typeAssert({ x: 114, y: () => 514 }, nullableAssertion) +typeAssert(null, nullableAssertion) + +// `undefined` is not considered a kind of `null` till now. +expectFailure(() => typeAssert(undefined, nullableAssertion)) + +// cannot nest nullable modification +expectFailure(() => 'number?'.orNull()) +expectFailure(() => { return { x: 'string' }.orNull().orNull()} ) + +const compoundAssertion = { + a: 'number', + b: 'string', + c: [ + { + x: 'function', + y: 'function' + }.sumWith({ + x: [], + y: {} + }).orNull() + ] +} + +typeAssert({ + a: 114, + b: '514', + c: [ + null, + { + x: [1, 2, 3], + y: { + z: '4' + } + }, + { + x: console.log, + y: Array.prototype.push + } + ] +}, compoundAssertion) + +typeAssert(5, 'number'.chainWith(x => x > 0 ? true : 'no negative numbers')) +expectFailure(() => typeAssert(-1, 'number'.chainWith(x => x > 0 ? true : 'no negative numbers'))) + +typeAssert(5, 'number'.assertValue(5)) +expectFailure(() => typeAssert(5, 'number'.assertValue(114514))) + +console.info('mission accomplished') diff --git a/test.js b/test.js new file mode 100644 index 0000000..88106fa --- /dev/null +++ b/test.js @@ -0,0 +1,97 @@ +import { typeAssert, enableChainAPI, preventErrTrace } from './typeAssert.js' + +preventErrTrace(true) +enableChainAPI() + +typeAssert(1, 'number') +typeAssert('2', 'string') + +const expectFailure = testedCode => { + try { + testedCode() + console.error('expected failure not caught') + process.exit(-1) + } catch (e) { + console.info('expected failure caught:', e) + } +} + +expectFailure(() => typeAssert(2, 'string')) + +typeAssert({ x: 1, y: '3' }, 'object') +typeAssert({ x: 1, y: '3' }, {}) +typeAssert({ x: 1, y: '3' }, { x: 'number', y: 'string' }) + +// By this time, arrays are also considered a kind of 'object'. +// this may change in further editions +typeAssert([], 'object') +typeAssert([], {}) + +expectFailure(() => typeAssert([], { x: 'number' })) +expectFailure(() => typeAssert({ x: 1, y: '2' }, { x: 'number', y: 'number' })) + +typeAssert([1, 2, 3], 'Array') +typeAssert([1, 2, 3], []) +typeAssert([1, 2, 3], ['number']) +typeAssert(['1', '2', '3'], ['string']) + +expectFailure(() => typeAssert({}, [])) +expectFailure(() => typeAssert({}, 'Array')) +expectFailure(() => typeAssert([1, 2, 3], ['string'])) + +const sumAssertion = 'string'.sumWith('number') +typeAssert('abc', sumAssertion) +typeAssert(123, sumAssertion) + +expectFailure(() => typeAssert(() => 114514, sumAssertion)) + +const nullableAssertion = { x: 'number', y: 'function' }.orNull() +typeAssert({ x: 114, y: () => 514 }, nullableAssertion) +typeAssert(null, nullableAssertion) + +// `undefined` is not considered a kind of `null` till now. +expectFailure(() => typeAssert(undefined, nullableAssertion)) + +// cannot nest nullable modification +expectFailure(() => 'number?'.orNull()) +expectFailure(() => { return { x: 'string' }.orNull().orNull()} ) + +const compoundAssertion = { + a: 'number', + b: 'string', + c: [ + { + x: 'function', + y: 'function' + }.sumWith({ + x: [], + y: {} + }).orNull() + ] +} + +typeAssert({ + a: 114, + b: '514', + c: [ + null, + { + x: [1, 2, 3], + y: { + z: '4' + } + }, + { + x: console.log, + y: Array.prototype.push + } + ] +}, compoundAssertion) + +typeAssert(5, 'number'.chainWith(x => x > 0 ? true : 'no negative numbers')) +expectFailure(() => typeAssert(-1, 'number'.chainWith(x => x > 0 ? true : 'no negative numbers'))) + +typeAssert(5, 'number'.assertValue(5)) +expectFailure(() => typeAssert(5, 'number'.assertValue(114514))) + +console.info('mission accomplished') diff --git a/typeAssert.cjs b/typeAssert.cjs index 6358dcc..c9975fa 100644 --- a/typeAssert.cjs +++ b/typeAssert.cjs @@ -23,29 +23,38 @@ const removeTail = (srcText) => { return [srcText, false] } -const typeAssertError = (path, message) => { +let globalPreventErr = false + +const preventErrTrace = prevent => { + globalPreventErr = prevent +} + +const typeAssertError = (path, message, preventErr) => { const errMsg = `Type assertion failed: "${path}": ${message}` - console.trace(errMsg) + if (!preventErr && !globalPreventErr) { + console.trace(errMsg) + } throw errMsg } -const assertEquals = (path, expected, got) => { +const assertEquals = (path, expected, got, preventErr) => { if (expected !== got) { - typeAssertError(path, `expected value "${expected}", got "${got}"`) + typeAssertError(path, `expected value "${expected}", got "${got}"`, preventErr) } } -const assertTypeEqImpl = (path, expected, got) => { +const assertTypeEqImpl = (path, expected, got, preventErr) => { if (expected !== got) { - typeAssertError(path, `expected type "${expected}", got "${got}"`) + typeAssertError(path, `expected type "${expected}", got "${got}"`, preventErr) } } -const assertTypeEqImpl2 = (path, expectedCtor, gotCtor, expected) => { +const assertTypeEqImpl2 = (path, expectedCtor, gotCtor, expected, preventErr) => { if (expectedCtor !== gotCtor) { typeAssertError( path, - `expected type "${expected}", checking using ctor "${expectedCtor.name}", got "${gotCtor.name}"` + `expected type "${expected}", checking using ctor "${expectedCtor.name}", got "${gotCtor.name}"`, + preventErr ) } } @@ -58,98 +67,98 @@ const formatSumTypeError = (errors) => { return ret } -const typeAssertImpl = (path, object, assertion) => { +const typeAssertImpl = (path, object, assertion, preventErr) => { const assertionType = typeof assertion if (assertion === null) { if (object !== null) { - typeAssertError(path, `expected "null" value, got "${typeof object}"`) + typeAssertError(path, `expected "null" value, got "${typeof object}"`, preventErr) } } else if (assertionType === TypeStrings.String) { const [type, nullable] = removeTail(assertion) if (nullable) { if (type === TypeStrings.Undefined) { - typeAssertError(path, '"undefined" type cannot be nullable') + typeAssertError(path, '"undefined" type cannot be nullable', preventErr) } if (object === null) { return } } if (object === null) { - typeAssertError(path, 'unexpected "null" value') + typeAssertError(path, 'unexpected "null" value', preventErr) } if (object !== undefined) { switch (type) { case TypeStrings.Array: - assertTypeEqImpl2(path, Array.prototype.constructor, object.constructor, type) + assertTypeEqImpl2(path, Array.prototype.constructor, object.constructor, type, preventErr) return case TypeStrings.Date: - assertTypeEqImpl2(path, Date.prototype.constructor, object.constructor, type) + assertTypeEqImpl2(path, Date.prototype.constructor, object.constructor, type, preventErr) return case TypeStrings.RegExp: - assertTypeEqImpl2(path, RegExp.prototype.constructor, object.constructor, type) + assertTypeEqImpl2(path, RegExp.prototype.constructor, object.constructor, type, preventErr) return default: // fall through } } - assertTypeEqImpl(path, type, typeof object) + assertTypeEqImpl(path, type, typeof object, preventErr) } else if (assertionType === TypeStrings.Function) { const assertResult = assertion(object) if (assertResult !== true) { - typeAssertError(path, assertResult) + typeAssertError(path, assertResult, preventErr) } } else if (assertion.constructor === NullableType.prototype.constructor) { if (object === null) { return } - typeAssertImpl(path, object, assertion.origin) + typeAssertImpl(path, object, assertion.origin, preventErr) } else if (assertion.constructor === SumType.prototype.constructor) { const failures = [] for (const type of assertion.types) { try { - typeAssertImpl(path, object, type) + typeAssertImpl(path, object, type, true) } catch (error) { failures.push(error) continue } return } - typeAssertError(path, formatSumTypeError(failures)) + typeAssertError(path, formatSumTypeError(failures), preventErr) } else if (assertion.constructor === ChainType.prototype.constructor) { // eslint-disable-next-line guard-for-in for (const partIdx in assertion.types) { - typeAssertImpl(`${path}:<${partIdx}>`, object, assertion.types[partIdx]) + typeAssertImpl(`${path}:<${partIdx}>`, object, assertion.types[partIdx], preventErr) } } else if (assertion.constructor === ValueAssertion.prototype.constructor) { - assertEquals(`${path}:value`, object, assertion.value) + assertEquals(`${path}:value`, object, assertion.value, preventErr) } else if (object === undefined) { - typeAssertError(path, 'unexpected "undefined" value') + typeAssertError(path, 'unexpected "undefined" value', preventErr) } else if (object === null) { - typeAssertError(path, 'unexpected "null" value') + typeAssertError(path, 'unexpected "null" value', preventErr) } else if (assertion.constructor === Array.prototype.constructor) { assertTypeEqImpl2(path, Array.prototype.constructor, object.constructor, TypeStrings.Array) if (assertion.length === 0) { // fallthrough } else if (assertion.length === 1) { for (const [idx, element] of Object.entries(object)) { - typeAssertImpl(`${path}[${idx}]`, element, assertion[0]) + typeAssertImpl(`${path}[${idx}]`, element, assertion[0], preventErr) } } else { - typeAssertError(path, '"array" type assertion should only have one element') + typeAssertError(path, '"array" type assertion should only have one element', preventErr) } } else if (assertionType === TypeStrings.Object - && assertion.constructor === Object.prototype.constructor) { + && assertion.constructor === Object.prototype.constructor) { for (const [field, fieldAssertion] of Object.entries(assertion)) { - typeAssertImpl(`${path}.${field}`, object[field], fieldAssertion) + typeAssertImpl(`${path}.${field}`, object[field], fieldAssertion, preventErr) } } else { - typeAssertError(path, 'invalid assertion') + typeAssertError(path, 'invalid assertion', preventErr) } } -const typeAssert = (object, assertion) => typeAssertImpl('object', object, assertion) +const typeAssert = (object, assertion) => typeAssertImpl('object', object, assertion, false) const NullableType = (function () { function NullableType(origin) { @@ -198,7 +207,7 @@ const enableChainAPI = methodNames => { } const checkChainNotEndedByValueAssertion = types => { - if (types[this.types.length - 1].constructor === ValueAssertion.prototype.constructor) { + if (types[types.length - 1].constructor === ValueAssertion.prototype.constructor) { typeAssertError(' ChainType.prototype.orNull', `should append any assertion after ${assertValueName}`) } } @@ -311,5 +320,6 @@ module.exports = { SumType, ChainType, ValueAssertion, - enableChainAPI + enableChainAPI, + preventErrTrace } diff --git a/typeAssert.js b/typeAssert.js index 70e77e8..721358a 100644 --- a/typeAssert.js +++ b/typeAssert.js @@ -23,29 +23,38 @@ const removeTail = (srcText) => { return [srcText, false] } -const typeAssertError = (path, message) => { +let globalPreventErr = false + +export const preventErrTrace = prevent => { + globalPreventErr = prevent +} + +const typeAssertError = (path, message, preventErr) => { const errMsg = `Type assertion failed: "${path}": ${message}` - console.trace(errMsg) + if (!preventErr && !globalPreventErr) { + console.trace(errMsg) + } throw errMsg } -const assertEquals = (path, expected, got) => { +const assertEquals = (path, expected, got, preventErr) => { if (expected !== got) { - typeAssertError(path, `expected value "${expected}", got "${got}"`) + typeAssertError(path, `expected value "${expected}", got "${got}"`, preventErr) } } -const assertTypeEqImpl = (path, expected, got) => { +const assertTypeEqImpl = (path, expected, got, preventErr) => { if (expected !== got) { - typeAssertError(path, `expected type "${expected}", got "${got}"`) + typeAssertError(path, `expected type "${expected}", got "${got}"`, preventErr) } } -const assertTypeEqImpl2 = (path, expectedCtor, gotCtor, expected) => { +const assertTypeEqImpl2 = (path, expectedCtor, gotCtor, expected, preventErr) => { if (expectedCtor !== gotCtor) { typeAssertError( path, - `expected type "${expected}", checking using ctor "${expectedCtor.name}", got "${gotCtor.name}"` + `expected type "${expected}", checking using ctor "${expectedCtor.name}", got "${gotCtor.name}"`, + preventErr ) } } @@ -58,98 +67,98 @@ const formatSumTypeError = (errors) => { return ret } -const typeAssertImpl = (path, object, assertion) => { +const typeAssertImpl = (path, object, assertion, preventErr) => { const assertionType = typeof assertion if (assertion === null) { if (object !== null) { - typeAssertError(path, `expected "null" value, got "${typeof object}"`) + typeAssertError(path, `expected "null" value, got "${typeof object}"`, preventErr) } } else if (assertionType === TypeStrings.String) { const [type, nullable] = removeTail(assertion) if (nullable) { if (type === TypeStrings.Undefined) { - typeAssertError(path, '"undefined" type cannot be nullable') + typeAssertError(path, '"undefined" type cannot be nullable', preventErr) } if (object === null) { return } } if (object === null) { - typeAssertError(path, 'unexpected "null" value') + typeAssertError(path, 'unexpected "null" value', preventErr) } if (object !== undefined) { switch (type) { case TypeStrings.Array: - assertTypeEqImpl2(path, Array.prototype.constructor, object.constructor, type) + assertTypeEqImpl2(path, Array.prototype.constructor, object.constructor, type, preventErr) return case TypeStrings.Date: - assertTypeEqImpl2(path, Date.prototype.constructor, object.constructor, type) + assertTypeEqImpl2(path, Date.prototype.constructor, object.constructor, type, preventErr) return case TypeStrings.RegExp: - assertTypeEqImpl2(path, RegExp.prototype.constructor, object.constructor, type) + assertTypeEqImpl2(path, RegExp.prototype.constructor, object.constructor, type, preventErr) return default: - // fall through + // fall through } } - assertTypeEqImpl(path, type, typeof object) + assertTypeEqImpl(path, type, typeof object, preventErr) } else if (assertionType === TypeStrings.Function) { const assertResult = assertion(object) if (assertResult !== true) { - typeAssertError(path, assertResult) + typeAssertError(path, assertResult, preventErr) } } else if (assertion.constructor === NullableType.prototype.constructor) { if (object === null) { return } - typeAssertImpl(path, object, assertion.origin) + typeAssertImpl(path, object, assertion.origin, preventErr) } else if (assertion.constructor === SumType.prototype.constructor) { const failures = [] for (const type of assertion.types) { try { - typeAssertImpl(path, object, type) + typeAssertImpl(path, object, type, true) } catch (error) { failures.push(error) continue } return } - typeAssertError(path, formatSumTypeError(failures)) + typeAssertError(path, formatSumTypeError(failures), preventErr) } else if (assertion.constructor === ChainType.prototype.constructor) { // eslint-disable-next-line guard-for-in for (const partIdx in assertion.types) { - typeAssertImpl(`${path}:<${partIdx}>`, object, assertion.types[partIdx]) + typeAssertImpl(`${path}:<${partIdx}>`, object, assertion.types[partIdx], preventErr) } } else if (assertion.constructor === ValueAssertion.prototype.constructor) { - assertEquals(`${path}:value`, object, assertion.value) + assertEquals(`${path}:value`, object, assertion.value, preventErr) } else if (object === undefined) { - typeAssertError(path, 'unexpected "undefined" value') + typeAssertError(path, 'unexpected "undefined" value', preventErr) } else if (object === null) { - typeAssertError(path, 'unexpected "null" value') + typeAssertError(path, 'unexpected "null" value', preventErr) } else if (assertion.constructor === Array.prototype.constructor) { assertTypeEqImpl2(path, Array.prototype.constructor, object.constructor, TypeStrings.Array) if (assertion.length === 0) { // fallthrough } else if (assertion.length === 1) { for (const [idx, element] of Object.entries(object)) { - typeAssertImpl(`${path}[${idx}]`, element, assertion[0]) + typeAssertImpl(`${path}[${idx}]`, element, assertion[0], preventErr) } } else { - typeAssertError(path, '"array" type assertion should only have one element') + typeAssertError(path, '"array" type assertion should only have one element', preventErr) } } else if (assertionType === TypeStrings.Object && assertion.constructor === Object.prototype.constructor) { for (const [field, fieldAssertion] of Object.entries(assertion)) { - typeAssertImpl(`${path}.${field}`, object[field], fieldAssertion) + typeAssertImpl(`${path}.${field}`, object[field], fieldAssertion, preventErr) } } else { - typeAssertError(path, 'invalid assertion') + typeAssertError(path, 'invalid assertion', preventErr) } } -export const typeAssert = (object, assertion) => typeAssertImpl('object', object, assertion) +export const typeAssert = (object, assertion) => typeAssertImpl('object', object, assertion, false) export const NullableType = (function () { function NullableType(origin) { @@ -198,7 +207,7 @@ export const enableChainAPI = methodNames => { } const checkChainNotEndedByValueAssertion = types => { - if (types[this.types.length - 1].constructor === ValueAssertion.prototype.constructor) { + if (types[types.length - 1].constructor === ValueAssertion.prototype.constructor) { typeAssertError(' ChainType.prototype.orNull', `should append any assertion after ${assertValueName}`) } }