Aro
Introduction
Aro adds metaprogramming helpers to modern JS code, chiefly to make it easy to (i) test and mock complex behavior, (ii) create type checks, and (iii) enforce code contracts. The code that is generated can be run Node.js 12+, and in all major web browsers without modification.
npm install aro -g
Once installed, in any JS file that will use the helpers, include the 'use aro'
directive at the top. Then use Aro to build the development and production versions from the src directory:
aro ./project-root --your-args
The project root directory must be structured around a src
directory containing an index.js
file, as follows:
.
├── package.json
├── node_modules
│ └── ...
└── src
├── index.js
├── index.test.js
├── foo.js
├── foo.test.js
└── ...
Aro-Style Code
At the top of each file that will use Aro tools, add the 'use aro'
directive before any other material, including comments (only a BOM string is permitted). The meta-programming helpers are provided via normal JS syntax:
'use aro' const foo =
fn
Functions
Functions defined with fn
are special in that they are tracked by Aro, so that their input and output types can be checked, and contracts enforced. This works for both synchronous and async
functions.
The fn
-internal tools are:
param
checks a parameter's type.returns
checks the parent function's return type.precon
enforces a precondition.postcon
enforces a postcondition.
These appear at the top of a function body as one contiguous block, mimicking the organization of JSDoc-style comments. In production mode, they are commented out of the code, eliminating any performance overhead, while leaving stacktrace line numbers in tact:
'use aro' const foo = /*fn*/ { // param (bar)(Number) // precon (() => Number.isInteger(bar)) // returns (Number) // postcon (rv => Number.isInteger(rv)) return **2}
main
Function
The The main
variable is used to define the main app function, and is implicitly executed by Aro once all tests have run. So in the case below, a set of tests would run (more on that later), and then an HTTP server would spin up to handle requests on port 3000:
'use aro' main =
If defining a module that will be included and run by other code, ignore main
and use the ESModules machinery as usual.
test
, mock
, & local
Testing with Tests are declared in sibling files using the *.test.js
naming convention. Each test file implicitly imports the material that it tests using the module
and local
variables from the source file that it is testing (i.e., values that are not exported can be accessed in tests via local
). Here is an example file saved as ./foo.js, for which tests will be specified in ./foo.test.js (shown below):
./foo.js:
'use aro' localinsertSpaces = const fromCamelCase =
./foo.test.js:
'use aro'
Mocking Functions
The mock
function is the most valuable tool provided by Aro. It renders the ordinarily harrowing task of setting up mocks as simple as one function call. Any function that has been defined with fn
can be mocked inline, as shown below.
First, consider this example source file, at ./bar.js:
'use aro' localrandomHex = const randomizeFname =
Notice that because the randomHex
produces non-predictable output, it will be useful to mock it in order to make the behavior of randomizeFname
predictable and therefore testable. Here is how that would be done within ./bar.test.js:
'use aro'
A mock persists for the duration of a single test; calling done()
wipes out the mock, setting the function back its real value.
Code Contracts
Contracts are enforced (development mode only) by the precon
and postcon
functions, which take functions that perform verification work before or after the business logic runs. For example:
'use aro' const read =
Type Checking
Aro relies on the Protocheck library. Type checks are implictly run on the inputs to the param
and returns
functions, as shown:
'use aro' const foo =
Simple Types
Protocheck implements simple types with semantics that keep to the type definitions in the ES6 spec, with two exceptions: arrays and functions are not considered Object
instances. The simple types are:
- Any
class
or constructor function (String
,Date
,YourClass
, etc.). Object
is any non-primitive except functions, arrays, and null-proto objects.Any
is anything (includingundefined
).Null
is the type ofnull
(per the ES6 spec).Undefined
is the type ofundefined
(per the ES6 spec).Void
is a value of typeNull
orUndefined
.Dictionary
is a null-prototype object (i.e.,Object.create(null)
).
Accessing Types
Aro's directly exposes the composable higher-order types implemented by Protocheck as global.types
. One can therefore access them by a simple destructuring assignment statement targeting types
:
'use aro' const Maybe Tuple Void U T ArrayT = types
Union Types
To declare a union type, pass a list of types to U
.
This could be used as a parameter type check, for example, as shown:
'use aro' const convertIdToInt =
Maybe Types
Maybe constructs a union type that implicitly includes Void
.
// The above is exactly the same as the below:
Tuple Types
To declare a tuple, pass a list of types to Tuple
.
Generic Types
To declare a generic type, use the T
function and pass it a value:
const fooFunc =
Array Types
To declare an array generic, use the ArrayT
function and pass it a type. Here's a number array:
Reusing Types
Any type can be saved and reused:
'use aro' const Coordinate = const distance =
Return Types in Async Functions
Within async
functions, Aro will respect the use of the return
keyword, so that returns (String)
would check the resolved value rather than the returned value (which would be a Promise
object).
'use aro' const asyncIdentity = // pass
ESLint Config
... // Let ESLint know about the globals and locally given variables. "globals": "main": true "fn": true "param": true "returns": true "precon": true "postcon": true "local": true // Treat .test.js files specially. "overrides": "files": "*.test.js" "globals": "test": true "mock": true "local": true