Skip to content

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.

License

Notifications You must be signed in to change notification settings

clausejs/clausejs

Repository files navigation

Clause

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.

Build Status npm version Dependencies Size Gzipped Open Source Love Discussion

Overview

Clause enables you to:

  • Build your data validation rules (clauses) with simple predicate functions and composable logical expressions such as and, any, shape, collOf, mapOf, maybe as well as regex-like clause-composing operators such as concatenation(cat), or(|), oneOrMore(+), zeroOrMore(*), zeroOrOne(?), etc
  • Validate and make assertions about your data with your clauses
  • Define complex clauses for your functions' arguments, return value and the relation between them
  • Conform (parse) your data/arguments for writing simpler code
  • Create types that are recursively defined

Clause is heavily inspired by clojure.spec, but will be evolving on its own to better suite the needs of JavaScript community.

Why Clause

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

  • Clearly defined specifications for your app's functions and data structures
  • Type and behavior checking at runtime for data and functions (some compile-time analysis may also be possible in the future)
  • Convenient data conformation (parsing) that simplifies your code for complex data and parameter parsing
  • Mathematically sound, plain-object style contracts that can be compared against previous and future versions to detect breaking changes (coming soon)
  • Automatic generation of API documentation for your app (via clausejs-docgen, WIP)
  • Automatic generation of property-based test coverage for your functions (via clausejs-gen, coming soon)

Also worth looking at are videos on rationale for clojure.spec (and, by extension, Clause).

Project status

Alpha.

  • Most core functions of Clause have gone through many iterations of bug fixing and are stablizing.
  • Some design aspects, especially API interfaces are still subject to change based on developer feedback.
  • Feedback and suggestions are welcome.

Quick Examples

Regex Ops & conformation

// 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 Registry

// 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)

Object Shapes

// 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").

Function Clauses

// 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.

More Examples

In addition, there are plenty of examples in test files under /test.

Try It Yourself

Usage

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).

Run tests

In Dev mode

npm run dev

In CI mode

npm run test

Documentation

Documentation website: http://clause.js.org

Performance & Reliability

  • Size Minified Size Gzipped
  • No external dependencies
  • Clause is mostly self-contained and has very few external dependencies.
  • Clause uses NFA algorithm for regex parsing, which means it's generally pretty fast in handling complex regex operations.
  • Clause's implementation is optimized in such a way that it avoid long chains of recursive function calls and makes as few redundant path visits as possible.

Subprojects

WIP:

  • clausejs-docgen: Automatic documentation generation based on function clauses
  • clausejs-react: More robust props validation for your React apps. A replacement for React.PropTypes

Coming soon:

  • clausejs-gen: Generative/Property-based Testing
  • clausejs-diff: (Coming soon) clause version diffing that detects breaking changes.

FAQ

Why not just use ClojureScript + cljs.spec?

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.

Why don't you call it "spec"?

"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.

Community

Discussion

Prior art & related work

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published