Write contract once. Get data & function validators & conformers, an accurate & readable project contract, auto-generated API documentation, generative test coverage, plus more. A tool that enables a more predictable workflow for developing your JavaScript projects.
MIT License
If a programming language is regarded as a tool to aid the programmer, it should give him the greatest assistance in the most difficult aspects of his art, namely program design, documentation, and debugging.
— C. A. R. Hoare, Hints on Programming Language Design
A powerful, expressive & practical JavaScript library for defining and verifying your JS app contract. Also facilitates with bug discovery, debugging & data parsing.
Clause enables you to:
and
, any
, shape
, collOf
, mapOf
, maybe
as well as regex-like clause-composing operators such as concatenation(cat
), or
(|
), oneOrMore
(+
), zeroOrMore
(*
), zeroOrOne
(?
), etcClause is heavily inspired by clojure.spec, but will be evolving on its own to better suite the needs of JavaScript community.
Clause's primary goal is to allow you to create the definitive contract for your JavaScript project.
By writing clauses for your data and functions only once, you can get a lot of leverage out your effort, including
Also worth looking at are videos on rationale for clojure.spec (and, by extension, Clause).
Alpha.
// In browser environment, Clause will by default expose "C" as a global variable
var C = require('clausejs');
var MyClause = C.cat( C.oneOrMore(C.isNum), C.zeroOrOne( C.isObj ) );
C.isValid(MyClause, [ 1, 2, 3, { a: 1 } ]); //=> true
C.isValid(MyClause, [ 1, 2, 3 ]); //=> true
C.isValid(MyClause, [ 1, 2, 3, null ]); //=> false: the trailing element does not satisfy our clause
C.isValid(MyClause, [ 1, 2, 3, { a: 1 }, { } ]); //=> false: extra trailing element
C.conform(MyClause, [ 1, 2, 3, null ]);
//=> a "Problem" object with detailed explanation why validation failed
// Next, we redefine the above concatenation clause, with a label for each part.
var MyLabelledClause = C.cat(
"myNumbers", C.oneOrMore(C.isNum),
"myObject", C.zeroOrOne( C.isObj )
);
MyLabelledClause.conform( [ 1, 2, 3, { a: 1 } ] );
//=> { myNumbers: [ 1, 2, 3 ], myObject: { a: 1 } }
(Notice how the returned results are grouped by the labels specified in the cat
clause.)
// Clause comes with an application-wide global clause registry.
C("myApp/myLabelledClause", MyLabelledClause); // defines a clause in the registry
C("myApp/myLabelledClause"); // returns the same clause above (MyLabelledClause)
// Before we continue: let's first define a predicate function
// (which is just a function that returns either true or false).
function startsWithBar( str ) {
return str.indexOf("bar") === 0;
}
// Now let's clauseify a "shape" for our objects.
var MyObjClause = C.shape({
// alternatively, you can simply provide an array of strings
// as required keys e.g. [ "propertyA", "propertyB",... ]
required: {
// define a single key with value clause
foo: C.isBool,
// ...or define a group of properties whose keys satisfy the first clause (e.g. startsWithBar),
// and whose value satisfies the second (e.g. C.any)
bars: [ startsWithBar, C.any ]
},
optional: {
// you can also arbitrarily compose new clauses from registered clauses
myObj: C("myApp/myLabelledClause")
}
});
// With the above clause defined, now let's try shape conformation.
C.conform( MyObjClause, { foo: true, bar1: 1, bar2: 2, bar3: 3 });
// { foo: true, bars: { bar1: 1, bar2: 2, bar3: 3 } }
// (Notice how all object keys that begin with "bar" are now grouped under a single value "bars").
// Now onward to function clauses.
var MyFnClause = C.fclause({
args: MyLabelledClause , // reusing MyClause from above
ret: C.isBool,
});
// Next we write our function.
function __myFunction(num1, num2, num3, myObj) {
// doesn't do much; just returns true for now.
return true;
};
// Then "instrument"(wrap/protect) this function with our function clause.
var myProtectedFn = MyFnClause.instrument(__myFunction);
// We can now try our new protected function.
myProtectedFn(1, 2, 3, { a: true }); // returns true
myProtectedFn(1, 2, 3, 'hello'); // Throws a "Problem" due to mismatched argument per our fclause definition.
// Finally, let's build a function that checks if the sum of all numbers are odd
// by taking advantage of Clause's function argument conformation.
// Step 1: we write a "barebone" function with our core logic,
// which consumes the conformed arguments as a single object.
// This will make sense in a second.
function __sumIsOdd( conformedArgs ) {
// (Here "conformedArgs" stores the value of the conformed object
// as we illustrated above.)
var myNumbers = conformedArgs.myNumbers; // e.g. [ 1, 2, 3 ]
var myObject = conformedArgs.myObject; // e.g. { a: 1 }
// (or simply { myNumbers, myObject } with ES6 destructring)
// Get the sum
var sum = myNumbers.reduce( function(c,s) { return s + c; }, 0 );
// Returns whether the sum is odd
return sum % 2 === 1;
}
// Step 2: wrap the barebone function with C.instrumentConformed()
var sumIsOdd = MyFnClause.instrmentConformed(__sumIsOdd);
// Let's try our new super function!
sumIsOdd( 1, 1, 1 ); //=> true: sum is odd
sumIsOdd( 2, 2, 2 ); //=> false: sum is even
sumIsOdd( 1, 1, 1, {} ); //=> true (remember the optional trailing isObj we defined above?)
sumIsOdd( 2, 2, 2, null ); //=> throws a "Problem" because arguments do not conform
sumIsOdd( 2, 2, 2, {}, {} ); //=> same as above
For more examples (with live demos), advanced features and concepts, refer to documentation site.
In addition, there are plenty of examples in test files under /test
.
For Node.js/browserify/webpack:
npm install clausejs
For browser:
Include Clause script tag in <head>
or <body>
tag of your HTML:
<script src="//unpkg.com/clausejs@latest/dist/clausejs.js"></script>
The variable C
will be exposed in the global environment (e.g. window
for browser and globals
for Node.js).
npm run dev
npm run test
Documentation website: http://clause.js.org
WIP:
clausejs-docgen
: Automatic documentation generation based on function clausesclausejs-react
: More robust props validation for your React apps. A replacement for React.PropTypes
Coming soon:
clausejs-gen
: Generative/Property-based Testingclausejs-diff
: (Coming soon) clause version diffing that detects breaking changes.Clojure IMO is a great and practical language. If you can use Clojure/ClojureScript, by all means go ahead and try cljs.spec.
The goal for Clause is to provide JavaScript developers as much the benefit derived from the spec system as possible.
Clause API for the most part is kept similar to clojure.spec, except for some differences related to usability and JavaScript-related conventions.
"Spec" already carries a different meaning in the JavaScript community, which is strongly associated with unit tests. While introducing this library to developers with the term "spec", I was often met with a confused look along with a commment such as "I already know how to write a spec, so what's the point?" I then quickly realized that a new term needs to be coined to refect some of the vastly different concepts introduced in Clause.
Both origin of the idea and API are heavily inspired by Rich Hickey et, al.'s clojure.spec.
Some aspects of the design are drawn by Scheme's contract system.
NFA clause matching inspired by Thompson NFA regex matching algorithm, and is based on afader's implementation.
js.spec, another attempt at bringing the power of clojure.spec to JavaScript