Skip to content

Commit

Permalink
Example implementation of human-readable mode (#17)
Browse files Browse the repository at this point in the history
* Example implementation of human-readable mode

* Ignore case + good implementation of human readability mode

* Updated CLI usage for human readability mode

* Bumbped version number
  • Loading branch information
nikitawootten-nist committed Sep 3, 2020
1 parent a39ce46 commit dc307b1
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 17 deletions.
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@
"/catalog/back-matter/resources,id",
"--excludeContent"
]
},
{
"type": "node",
"request": "launch",
"name": "Run on NIST_SP-800-53 Human Readable Mode",
"program": "${workspaceFolder}\\src\\cli.ts",
"preLaunchTask": "npm: build",
"sourceMaps": true,
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outFiles": [
"${workspaceFolder}/lib/**/*.js"
],
// Note that this requires NIST-SP-800-53 Revisions 4 and 5 in the vault/ directory
"args": [
"--leftCatalog",
"vault/NIST_SP-800-53_rev4_catalog.json",
"--rightCatalog",
"vault/NIST_SP-800-53_rev5-FPD_catalog.json",
"--ignore",
"/catalog/back-matter/resources,id",
]
}
]
}
14 changes: 7 additions & 7 deletions docs/CLI_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,26 @@ Options:
-w, --write <filename> File to output difference document to (default: "")
--disableMemoization Disable the caching of array object (only use in low-memory scenarios) (default: false)
--excludeContent Exclude "leftElement" and "rightElement" objects, reducing comparison size (default: false)
--ignoreCase Ignore string comparisons with different cases (e.g. "Action" vs "action") (default: false)
-v, --verbose Print more output (default: false)
-h, --help display help for command
```

## Basic Comparison

To do a basic comparison, only the `--leftCatalog` and `--rightCatalog` options are required. Note that this will print a potentially huge object to your console's output.
To do a basic comparison, only the `--leftCatalog` and `--rightCatalog` options are required. Note that this will print a lot to your console's output, so you may want to pipe the output through a pager such as `less`.
```
$ oscal-deep-diff --leftCatalog "NIST_SP-800-53_rev4_catalog.json" --rightCatalog "NIST_SP-800-53_rev5-FPD_catalog.json"
(output trimmed)
```
To save this output to a file, you can either use redirection:
```
oscal-deep-diff --leftCatalog "NIST_SP-800-53_rev4_catalog.json" --rightCatalog "NIST_SP-800-53_rev5-FPD_catalog.json" > output.json
```
Or you can use the provided `--write` flag
To save this output to a file, you can use the provided `--write` flag to output the JSON form:
```
$ oscal-deep-diff --leftCatalog "NIST_SP-800-53_rev4_catalog.json" --rightCatalog "NIST_SP-800-53_rev5-FPD_catalog.json" --write "output.json"
Saving compared document to output.json
```

This tool is designed to fit a wide array of circumstances. Because of this, the default unconstrained comparison may not suit your needs. The comparison behavior can be modified using the above flags.

## Constraints

To constrain a comparison, you can specify patterns of properties that should be ignored using the `--ignore`` flag, and you can specify how arrays of objects should be matched using the `--constraints` flag. Consult the [constraints document](./constraints.md) for more details.
Expand All @@ -48,4 +47,5 @@ To constrain a comparison, you can specify patterns of properties that should be

* `--verbose`: When specified, the system will print additional debug information, such as the time it took to complete the comparison.
* `--disableMemoization`: When specified, the comparator will not cache the results of sub-comparisons, significantly sacrificing runtime in order to decrease memory usage. See issue #4 for more details.
* `--excludeContent`: when specified, the output document will not include any content from the left or right documents, only displaying the JSON pointers of changed elements. See #13 for more details.
* `--excludeContent`: when specified, the output document will not include any content from the left or right documents, only displaying the JSON pointers of changed elements. See #13 for more details.
* `--ignoreCase`: comparisons between strings do not take case into account.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@oscal/oscal-deep-diff",
"version": "0.0.2",
"version": "0.0.3",
"description": "A deep diff tool",
"main": "./lib/index.js",
"bin": {
Expand Down
16 changes: 16 additions & 0 deletions src/cli-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as fs from 'fs';
import { Command } from 'commander';
import { Comparison } from './comparisons';
import { RedFG, ResetConsole, GreenFG, YellowFG } from './utils';

export function loadJSON(documentPath: string): object {
// TODO: support URL paths?
Expand All @@ -16,6 +18,7 @@ interface CLIOptions {
write: string;
disableMemoization: boolean;
excludeContent: boolean;
ignoreCase: boolean;
verbose: boolean;
}

Expand All @@ -35,8 +38,21 @@ export function parseOptions(): CLIOptions {
.option('-w, --write <filename>', 'File to output difference document to', '')
.option('--disableMemoization', 'Disable the caching of array object (only use in low-memory scenarios)', false)
.option('--excludeContent', 'Exclude "leftElement" and "rightElement" objects, reducing comparison size', false)
.option('--ignoreCase', 'Ignore string comparisons with different cases (e.g. "Action" vs "action")')
.option('-v, --verbose', 'Print more output', false)
.parse(process.argv);
// specially cast rawOptions object to CLIOptions interface (force typing)
return (rawOptions as unknown) as CLIOptions;
}



export function printComparison(comparison: Comparison) {
console.log(`Comparison between ${RedFG}${comparison.leftDocument}${ResetConsole} and ${GreenFG}${comparison.rightDocument}${ResetConsole}:`);
for (const change of comparison.changes) {
console.log(`${YellowFG}---${ResetConsole}`);
change.printChange();
}
console.log(`${YellowFG}---${ResetConsole}`);
console.log(`Top level changes: ${comparison.changes.length}`);
}
6 changes: 3 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Comparator } from './comparator';
import { Condition } from './utils';
import { MatchConstraintsContainer } from './matching';
import { loadJSON, parseOptions } from './cli-utils';
import { loadJSON, parseOptions, printComparison } from './cli-utils';
import * as fs from 'fs';
import { excludeContentReplacer } from './comparisons';

Expand All @@ -11,6 +11,7 @@ const comparator = new Comparator();

comparator.verbose = options.verbose;
comparator.memoizationEnabled = !options.disableMemoization;
comparator.ignoreCase = options.ignoreCase;

if (options.ignore !== '') {
// parse ignore constraints (commander veriadic options leads to unstable results)
Expand Down Expand Up @@ -43,6 +44,5 @@ if (options.write !== '') {
}
fs.writeFileSync(options.write, outputStringified);
} else {
// print full output to stdout
console.log(outputStringified);
printComparison(comparator.comparison);
}
11 changes: 11 additions & 0 deletions src/comparator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export class Comparator {
this._memoizationEnabled = memoizationEnabled;
}

private _ignoreCase: boolean = false;

public set ignoreCase(ignoreCase: boolean) {
this._ignoreCase = ignoreCase;
}

private cache = new MemoizationCache();

private _constraints: MatchConstraintsContainer;
Expand Down Expand Up @@ -334,6 +340,11 @@ export class Comparator {
} else if (type === 'object') {
// elements are both objects, compare each sub-element in the object
return this.compareObjects(oldElement, oldPointer, newElement, newPointer, currentChanges);
} else if (type === 'string' && this._ignoreCase) {
if ((oldElement as string).toLowerCase() !== (newElement as string).toLowerCase()) {
currentChanges.push(new PropertyChanged(oldElement, oldPointer, newElement, newPointer));
}
return 0;
} else {
// elements can be considered a primitive type
if (oldElement !== newElement) {
Expand Down
55 changes: 51 additions & 4 deletions src/comparisons.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export class PropertyAdded {
import { GreenFG, ResetConsole, RedFG, YellowFG } from "./utils";

abstract class Printable {
abstract printChange(): void;
}

export class PropertyAdded implements Printable {
change = 'property_added';
leftParentPointer: string;
rightPointer: string;
Expand All @@ -9,9 +15,16 @@ export class PropertyAdded {
this.rightPointer = rightPointer;
this.addedElement = addedElement;
}

printChange(prefix=''): void {
console.log(`${prefix}Change type: ${this.change}`);
console.log(`${prefix}Right pointer: ${this.rightPointer}`);
console.log(`${prefix}Left parent pointer: ${this.leftParentPointer}`);
console.log(`${prefix}Added element: ${GreenFG}`, this.addedElement, ResetConsole);
}
}

export class PropertyDeleted {
export class PropertyDeleted implements Printable {
change = 'property_deleted';
leftPointer: string;
deletedElement: any;
Expand All @@ -22,9 +35,16 @@ export class PropertyDeleted {
this.deletedElement = deletedElement;
this.rightParentPointer = rightParentPointer;
}

printChange(prefix=''): void {
console.log(`${prefix}Change type: ${this.change}`);
console.log(`${prefix}Left pointer: ${this.leftPointer}`);
console.log(`${prefix}Right parent pointer: ${this.rightParentPointer}`);
console.log(`${prefix}Deleted element: ${RedFG}`, this.deletedElement, ResetConsole);
}
}

export class PropertyChanged {
export class PropertyChanged implements Printable {
change = 'property_changed';
leftPointer: string;
leftElement: any;
Expand All @@ -37,6 +57,14 @@ export class PropertyChanged {
this.rightPointer = rightPointer;
this.rightElement = rightElement;
}

printChange(prefix=''): void {
console.log(`${prefix}Change type: ${this.change}`);
console.log(`${prefix}Right pointer: ${this.rightPointer}`);
console.log(`${prefix}Left pointer: ${this.leftPointer}`);
console.log(`${prefix}Left element: ${RedFG}`, this.leftElement, ResetConsole);
console.log(`${prefix}Right element: ${GreenFG}`, this.rightElement, ResetConsole);
}
}

export interface LeftArrayItem {
Expand All @@ -55,7 +83,7 @@ export interface ArraySubElement {
changes: Change[];
}

export class ArrayChanged {
export class ArrayChanged implements Printable {
change = 'array_changed';
leftPointer: string;
rightPointer: string;
Expand Down Expand Up @@ -90,6 +118,25 @@ export class ArrayChanged {
this.matchProperty = matchProperty;
this.matchMethod = matchMethod;
}

printChange(prefix=''): void {
console.log(`${prefix}Change type: ${this.change}`);
console.log(`${prefix}Left pointer: ${RedFG}${this.leftPointer}${ResetConsole}`);
console.log(`${prefix}Right pointer: ${GreenFG}${this.rightPointer}${ResetConsole}`);
console.log(`${prefix}Sub-changes:`)
const newPrefix = prefix + ' ';
for (const elementPair of this.subChanges) {
console.log(`${newPrefix}${YellowFG}---${ResetConsole}`);
console.log(`${newPrefix}Left Element: ${RedFG}`, elementPair.leftPointer, ResetConsole);
console.log(`${newPrefix}Right Element: ${GreenFG}`, elementPair.leftPointer, ResetConsole);
console.log(`${newPrefix}Changes:`)
for (const change of elementPair.changes) {
console.log(`${newPrefix + ' '}${YellowFG}---${ResetConsole}`);
change.printChange(newPrefix + ' ');
}
console.log(`${newPrefix + ' '}${YellowFG}---${ResetConsole}`);
}
}
}

export type Change = PropertyAdded | PropertyDeleted | PropertyChanged | ArrayChanged;
Expand Down
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,8 @@ export function countSubElements(element: any): number {
}
return count;
}

export const RedFG = '\x1b[31m';
export const GreenFG = '\x1b[32m';
export const YellowFG = '\x1b[33m';
export const ResetConsole = '\x1b[0m';

0 comments on commit dc307b1

Please sign in to comment.