Installation
Quick Take
Purpose
It’s a library to compare two arrays, where each consists of plain objects, where array order is random. Program will try its best to “untangle” that order and to find matches.
API — deepContains()
The main function deepContains()
is imported like this:
It is a function which takes five input arguments:
Input argument | Type | Obligatory | Description |
---|---|---|---|
tree1 Type: reference AST, can be superset of tree2 Obligatory: yes | |||
tree1 | reference AST, can be superset of tree2 | yes | Array, object or nested thereof |
tree2 Type: AST being checked, can be subset of tree1 Obligatory: yes | |||
tree2 | AST being checked, can be subset of tree1 | yes | Array, object or nested thereof |
cb Type: function Obligatory: yes | |||
cb | function | yes | This function will be called with pairs, of values from each path. Think t.is of AVA. See API below. |
errCb Type: function Obligatory: yes | |||
errCb | function | yes | If path does not exist on tree1 , this callback function will be called with a message string. Think t.fail of tap or ava. |
opts Type: Plain object Obligatory: no | |||
opts | Plain object | no | Optional Options Object containing settings, see API below. |
The optional options object has the following shape:
Key | Value’s type | Default | Description |
---|---|---|---|
skipContainers Default: true | |||
skipContainers | Boolean | true | During traversal, containers (arrays and objects) will be checked for existence and traversed further but callback won’t be pinged. Set to false to stop doing that. |
arrayStrictComparison Default: false | |||
arrayStrictComparison | Boolean | false | Objects in the array can be of random order, as long as each one is matched, order does not matter. For strict order, set to true . |
Here are all defaults in one place for copying:
The function returns undefined
— this program is used exclusively through callbacks. Instead of calling the function and grabbing its result, you pass your callback function as input argument, and that callback function gets called with all the goodies passed as input arguments. Same like Array.forEach
or Array.reduce
.
API — defaults
You can import defaults
:
It's a plain object:
The main function calculates the options to be used by merging the options you passed with these defaults.
API — version
You can import version
:
Reasoning
We had a situation in emlint
— error objects come in asynchronous fashion so their order is random. Yet, we want to assert, does the error array contain error object X.
Another consideration is that result error object might have extra keys we don’t care to match — for example, row and column numbers.
[
{
ruleId: "tag-is-present",
line: 1, // not present in matched object
column: 4, // not present in matched object
severity: 2, // not present in matched object
idxFrom: 0,
idxTo: 4,
message: "h1 is not allowed.",
fix: {
ranges: [[0, 4]],
},
},
];
vs
[
{
ruleId: "tag-is-present",
idxFrom: 43,
idxTo: 48,
message: "h1 is not allowed.",
fix: {
ranges: [[43, 48]],
},
},
];
Notice how above we don’t bother with line
and column
values, as well as severity
. Also, note that key structure is very similar, yet objects are in a wrong order (because rules were raised in such way).
Ava’s t.deepEqual
is exact match so 1) missing keys and 2) wrong object order in the array would be an issue.
Tap’s t.same
would match set/subset keys but would still not be able to detect that two objects are in a wrong order.
Solution is this package.
It will try to match which object is the most similar to the source’s, then will not raise errors if source has extra keys.
Matching is passed to your chosen assertion functions, most likely t.is
and t.fail
.
Example #1 — checking subset of keys only
Here is reduced example based on codsen-tokenizer
tests:
import t from "tap";
import ct from "codsen-tokenizer";
import { deepContains } from "ast-deep-contains";
test("01.01 - text-tag-text", (t) => {
const gathered = [];
ct(" <a>z", (obj) => {
gathered.push(obj);
});
deepContains(
gathered,
[
{
type: "text",
start: 0,
end: 2,
// <---- tokenizer reports way more keys than that
},
{
type: "html",
start: 2,
end: 5,
},
{
type: "text",
start: 5,
end: 6,
},
],
t.is, // each pair of keys is ran by this function
t.fail // major failures are pinged to this function
);
});
In example above, reported objects will have more keys than what’s compared. Throughout the time, when improving the tokenizer we will surely add even more new keys. All this should not affect the main keys. Using t.same
would be a nuisance — we’d have to update all unit tests each time after a new improvement to the tokenizer is done, new key is added.
Example #2 — matching array contents, order is random
Our linter emlint
is pluggable — each rule is a plugin and program’s architecture is based on the Observer patten — the main checking function in EMLint is extending the Node’s EventEmitter
class:
class Linter extends EventEmitter {
...
}
This means, the nature in which errors are raised is somewhat undetermined. In EMLint unit tests we want to check, were correct errors raised and would the proposed string fixing index ranges fix the input.
Same way with the yard’s dog and cat example, we don’t care about the order of the pets (linter error objects) — as long each one of the set is reported, happy days.
Behold — a program flags up two backslashes on a void HTML tag — the first backslash should be deleted, second one turned into normal slash — we don’t care about the order of the elements as long as all elements were matched, plus there might be extra keys in the source objects — source objects are superset of what we’re matching:
import t from "tap";
import { Linter } from "emlint";
import { deepContains } from "ast-deep-contains"; // <------------ this program
import { applyFixes } from "t-util/util";
const BACKSLASH = "\u005C";
t.test(
`06.01 - ${`\u001b[${36}m${`both sides`}\u001b[${39}m`} - extreme case`,
(t) => {
const str = `<${BACKSLASH}br${BACKSLASH}>`;
const linter = new Linter();
// call the linter and record the result's error messages:
const messages = linter.verify(str, {
rules: {
tag: 2,
},
});
// assertion:
deepContains(
messages,
[
{
ruleId: "tag-closing-backslash",
severity: 2,
idxFrom: 1,
idxTo: 2,
message: "Wrong slash - backslash.",
fix: {
ranges: [[1, 2]],
},
// <---- "messages" we're comparing against will have more keys but we don't care
},
{
ruleId: "tag-closing-backslash",
severity: 2,
idxFrom: 4,
idxTo: 5,
message: "Replace backslash with slash.",
fix: {
ranges: [[4, 5, "/"]],
},
},
],
t.is, // each pair of key values is ran by this function
t.fail // major failures are pinged to this function
);
}
);
The order in which backslashes will be reported does not matter, plus Linter might report more information — that’s welcomed but will be ignored, not a cause for error.
opts.skipContainers
Consider these two AST’s, for example:
Object 1:
{
"a": {
"b": "c"
}
}
Object 2:
{
"a": {
"b": "d"
}
}
During traversal, ast-monkey-traverse will check for existence of path “a” on Object 1 but won’t report the values {"b": "c"}
and {"b": "d"}
. This way, when using this program in unit test context, AVA’s t.is
can be passed and we would be matching the values only. Missing paths would get reported to your chosen unit test runner’s “fail” assert.