@toryt/contracts
Version:
Design-by-Contract and Test-by-Contract for JavaScript
164 lines (143 loc) • 7.47 kB
JavaScript
/*
Copyright 2016–2024 Jan Dockx
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const stack = require('./_private/stack')
const property = require('./_private/property')
const ConditionError = require('./ConditionError')
const AbstractContract = require('./AbstractContract')
const PreconditionViolation = require('./PreconditionViolation')
const PostconditionViolation = require('./PostconditionViolation')
const ExceptionConditionViolation = require('./ExceptionConditionViolation')
const assert = require('assert')
// MUDO rename to 'resultIsAPromise', because this name is used in error reporting
function resultIsAPromiseCondition() {
return arguments[arguments.length - 2] instanceof Promise // MUDO too strict; use duck typing, to also allow objects from Q, e.g.
}
const resultIsAPromise = [resultIsAPromiseCondition]
/**
* Contract for functions that return a `Promise`. Postconditions are applied to the successful `Promise` resolution.
* Exception conditions are applied to the rejection of the returned `Promise`. `fastException` conditions are applied
* to exceptions that the function throws while creating the `Promise`. Note that in `async` functions, the distinction
* between `exception` and `fastException` conditions is lost. The fast postcondition of a `Promise` function is always
* that the function must return a `Promise` instance. A `Promise` function that returns something else, or `null` or
* `undefined` occasionally, is not supported.
*
* @constructor
*/
function PromiseContract(kwargs) {
assert.ok(kwargs)
assert(!kwargs.pre || Array.isArray(kwargs.pre), 'optional kwargs.pre is an array')
assert(!kwargs.post || Array.isArray(kwargs.post), 'optional kwargs.post is an array')
assert(!kwargs.exception || Array.isArray(kwargs.exception), 'optional kwargs.exception is an array')
assert(!kwargs.fastException || Array.isArray(kwargs.fastException), 'optional kwargs.fastException is an array')
AbstractContract.call(this, kwargs, stack.location(1))
property.setAndFreeze(
this,
'_fastException',
Object.freeze(kwargs.fastException ? kwargs.fastException.slice() : AbstractContract.mustNotHappen)
)
}
PromiseContract.prototype = new AbstractContract(
{ pre: AbstractContract.mustNotHappen },
AbstractContract.internalLocation
)
PromiseContract.prototype.constructor = PromiseContract
property.frozenReadOnlyArray(PromiseContract.prototype, 'fastException', '_fastException')
PromiseContract.prototype.implementation = function (implFunction) {
assert.strictEqual(typeof implFunction, 'function')
const contract = this
const location = stack.location(1)
function contractFunction() {
// cfThis: the `this` of the contract function call
const cfThis = this
if (!contractFunction.contract.verify) {
/* Shortcut if there is no verification.
This is not contract.verify: contractFunction.contract is a separate object, that has contract as a
prototype. */
return implFunction.apply(cfThis, arguments)
}
// NOTE: This assertion is added to guard against a problem with Safari on iOS in . See AbstractContract.bless.
// noinspection JSPotentiallyInvalidConstructorUsage
assert(
!contractFunction.implementation.prototype ||
contractFunction.prototype === contractFunction.implementation.prototype ||
contractFunction.prototype instanceof contractFunction.implementation,
'contractFunction prototype must be instanceof implementation function prototype'
)
// noinspection JSUnresolvedReference
PreconditionViolation.prototype.verifyAll(contractFunction, contract.pre, cfThis, arguments)
if (!contractFunction.contract.verifyPostconditions) {
/* Shortcut if there is no verification.
This is not contract.verifyPostconditions: contractFunction.contract is a separate object, that has contract as
a prototype. */
return implFunction.apply(cfThis, arguments)
}
const extendedArgs = Array.prototype.slice.call(arguments)
try {
const promise = implFunction.apply(cfThis, arguments)
/* Although we can test for the promise being a Promise here directly, the current implementation chooses
to go through verifyAll. This is to keep the stack determination in 1 place. If we throw the
PostconditionViolation here, we need to define the stack trace here for all violations, and pass it through
verifyAll and verify. For this to work, we need to create an applicable extendedArgs (without tainting the
original for uses below). This is at the cost of going through verifyAll and verify, and the
resultIsAPromiseCondition to do a simple test. */
const promiseArgs = extendedArgs.concat([promise, null]) //
// noinspection JSUnresolvedReference
PostconditionViolation.prototype.verifyAll(contractFunction, resultIsAPromise, cfThis, promiseArgs)
return promise
.catch(rejection => {
if (rejection instanceof ConditionError) {
// necessary to report only the deepest failure clearly
throw rejection
}
extendedArgs.push(rejection)
extendedArgs.push(contractFunction.bind(cfThis))
// noinspection JSUnresolvedReference
return ExceptionConditionViolation.prototype
.verifyAllPromise(contractFunction, contract.exception, cfThis, extendedArgs)
.then(() => {
throw rejection
})
})
.then(resolution => {
extendedArgs.push(resolution)
extendedArgs.push(contractFunction.bind(cfThis))
// noinspection JSUnresolvedReference
return PostconditionViolation.prototype
.verifyAllPromise(contractFunction, contract.post, cfThis, extendedArgs)
.then(() => resolution)
})
} catch (fastException) {
if (fastException instanceof ConditionError) {
// necessary to report pcv and only the deepest failure clearly
throw fastException
}
extendedArgs.push(fastException)
extendedArgs.push(contractFunction.bind(cfThis))
// noinspection JSUnresolvedReference
ExceptionConditionViolation.prototype.verifyAll(contractFunction, contract.fastException, cfThis, extendedArgs)
throw fastException
}
}
AbstractContract.bless(contractFunction, contract, implFunction, location)
return contractFunction
}
PromiseContract.resultIsAPromiseCondition = resultIsAPromiseCondition
PromiseContract.falseCondition = AbstractContract.falseCondition
PromiseContract.mustNotHappen = AbstractContract.mustNotHappen
PromiseContract.root = AbstractContract.root
PromiseContract.isAContractFunction = AbstractContract.isAContractFunction
PromiseContract.outcome = AbstractContract.outcome
// noinspection JSAnnotator
PromiseContract.callee = AbstractContract.callee
module.exports = PromiseContract