diff --git a/code-contract.test.ts b/code-contract.test.ts new file mode 100644 index 0000000..a2d6b45 --- /dev/null +++ b/code-contract.test.ts @@ -0,0 +1,122 @@ +/** + * @file Unit tests for code contract utility + */ +import { + assertEquals, + assertThrows, +} from "https://deno.land/std/testing/asserts.ts"; + +import { + codeContract, + disableContract, + enableContract, +} from "./code-contract.ts"; + +// +// Variables +// + +let sideEffectTarget = true; + +// +// Functions +// + +const func = (lhs: number, rhs: number) => (lhs + rhs); + +// +// Test cases +// + +Deno.test({ + name: "It should success the function call if the contract is kept", + fn: () => { + // arrange + enableContract(); + sideEffectTarget = false; + const contracted = codeContract(func, { + pre: (lhs: number, rhs: number) => 0 < lhs && 0 < rhs, + post: (result: number) => (result === 30), + }); + + // act & assert + assertEquals(contracted(10, 20), 30); + }, +}); +Deno.test({ + name: "It should fail the function call if the pre condition is invalid", + fn: () => { + assertThrows( + () => { + // arrange + enableContract(); + sideEffectTarget = false; + const contracted = codeContract(func, { + pre: (lhs: number, rhs: number) => 0 < lhs && 0 < rhs, + }); + + // act & assert + assertEquals(contracted(20, -10), 10); + }, + ); + }, +}); +Deno.test({ + name: "It should fail the function call if the post condition is invalid", + fn: () => { + assertThrows( + () => { + // arrange + enableContract(); + sideEffectTarget = false; + const contracted = codeContract(func, { + post: (result: number) => (result === 30), + }); + + // act & assert + assertEquals(contracted(20, 20), 40); + }, + ); + }, +}); +Deno.test({ + name: + "It should fail the function call if the invariant condition is invalid", + fn: () => { + assertThrows( + () => { + // arrange + enableContract(); + sideEffectTarget = false; + + const funcWithSideEffect = ( + lhs: number, + rhs: number, + ) => (sideEffectTarget = true, lhs + rhs); + const contracted = codeContract(funcWithSideEffect, { + invariant: () => (sideEffectTarget === false), + }); + + // act & assert + assertEquals(contracted(20, 20), 40); + }, + ); + }, +}); +Deno.test({ + name: "It should omit contract check if contract check is disabled", + fn: () => { + // arrange + disableContract(); + sideEffectTarget = false; + const contracted = codeContract(func, { + pre: (lhs: number, rhs: number) => 0 < lhs && 0 < rhs, + post: (result: number) => (result === 30), + }); + + // act & assert + assertEquals(contracted(-10, -20), -30); + + enableContract(); + }, +}); diff --git a/code-contract.ts b/code-contract.ts new file mode 100644 index 0000000..647665c --- /dev/null +++ b/code-contract.ts @@ -0,0 +1,102 @@ +/** + * @file Code contract utility + */ +// +// Types +// + +// deno-lint-ignore no-explicit-any +type FunctionType = ((...args: any[]) => T) & { name: string }; +type ContractType = { + // deno-lint-ignore no-explicit-any + pre?: (...args: any[]) => boolean; + post?: (result: TResult) => boolean; + invariant?: () => boolean; +}; + +// +// Variables +// + +let checkContract = true; + +// +// Functions +// + +/** + * Bind function with code contract + * @param fn Function to bind + * @param contract Code contract + * @returns Bound function + */ +export function codeContract< + T extends FunctionType>, +>( + fn: T, + contract: ContractType> = {}, +): T { + if (checkContract === false) { + return fn; + } + return ((...args) => { + const check = requireContractCheck(); + if ( + check !== false && contract.pre !== undefined && + contract.pre(...args) === false + ) { + const msg = [ + "Code Contract: Failed to assert the pre condition.", + `\tfunction: ${fn.name}`, + `\targs: ${JSON.stringify(args)}`, + ]; + throw new Error(msg.join("\n")); + } + const result = fn(...args); + if ( + check !== false && contract.post !== undefined && + contract.post(result) === false + ) { + const msg = [ + "Code Contract: Failed to assert the post condition.", + `\tfunction: ${fn.name}`, + `\tresult: ${JSON.stringify(args)}`, + ]; + throw new Error(msg.join("\n")); + } + if ( + check !== false && contract.invariant !== undefined && + contract.invariant() === false + ) { + const msg = [ + "Code Contract: Failed to assert the invariant.", + `\tfunction: ${fn.name}`, + ]; + throw new Error(msg.join("\n")); + } + + return result; + }) as T; +} + +/** + * Enable contract + */ +export function enableContract() { + checkContract = true; +} + +/** + * Disable contract check + */ +export function disableContract() { + checkContract = false; +} + +/** + * Require contract check + * @returns Require contract check flag + */ +function requireContractCheck() { + return checkContract; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0f11fb7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strict": true + } +}