GraphStream

This commit is contained in:
Timo Hocker 2020-05-06 20:24:37 +02:00
parent acf8004497
commit 8d6d91e562
21 changed files with 357 additions and 81 deletions

2
Jenkinsfile vendored
View File

@ -5,7 +5,7 @@ pipeline {
VERSION = VersionNumber([ VERSION = VersionNumber([
versionNumberString: versionNumberString:
'${BUILDS_ALL_TIME}', '${BUILDS_ALL_TIME}',
versionPrefix: '1.0.', versionPrefix: '1.1.',
worstResultForIncrement: 'SUCCESS' worstResultForIncrement: 'SUCCESS'
]) ])
} }

View File

@ -1,10 +0,0 @@
export enum GraphLayouts {
neato = 'neato',
dot = 'dot',
circo = 'circo',
fdp = 'fdp',
sfdp = 'sfdp',
osage = 'osage',
twopi = 'twopi',
patchwork = 'patchwork'
}

10
lib/Helper.ts Normal file
View File

@ -0,0 +1,10 @@
function validate_name (name: string): string {
const new_name = name
.replace (/[^a-z0-9]/giu, '')
.replace (/^[0-9]+/iu, '');
if (new_name === '')
throw new Error (`invalid node name ${name}`);
return new_name;
}
export { validate_name };

View File

@ -1,33 +0,0 @@
enum EdgeStyles {
default = '',
solid = 'solid',
dashed = 'dashed',
dotted='dotted',
bold='bold'
}
enum NodeStyles {
default = '',
solid='solid',
dashed='dashed',
dotted='dotted',
bold='bold',
rounded='rounded',
diagonals='diagonals',
filled='filled',
striped='striped',
wedged='wedged',
invisible='invis'
}
enum GraphStyles {
solid = 'solid',
dashed = 'dashed',
dotted = 'dotted',
bold = 'bold',
rounded = 'rounded',
filled = 'filled',
striped = 'striped'
}
export { EdgeStyles, NodeStyles, GraphStyles };

View File

@ -1,4 +1,4 @@
import { EdgeStyles } from './Styles'; import { EdgeStyles } from '../enums/Styles';
import { Color } from './Color'; import { Color } from './Color';
export class Edge { export class Edge {

View File

@ -1,3 +1,5 @@
import { validate_name } from '../Helper';
export class Element { export class Element {
private _name = ''; private _name = '';
protected parent_name: string; protected parent_name: string;
@ -13,11 +15,7 @@ export class Element {
} }
public set name (val: string) { public set name (val: string) {
const new_name = val.replace (/[^a-z0-9]/giu, '') this._name = validate_name (val);
.replace (/^[0-9]+/iu, '');
if (new_name === '')
throw new Error (`invalid node name ${val}`);
this._name = new_name;
} }
public get parent (): string { public get parent (): string {

View File

@ -1,9 +1,9 @@
import { GraphStyles, NodeStyles } from '../enums/Styles';
import { GraphLayouts } from '../enums/GraphLayouts';
import { Element } from './Element'; import { Element } from './Element';
import { Edge } from './Edge'; import { Edge } from './Edge';
import { Node } from './Node'; import { Node } from './Node';
import { GraphStyles, NodeStyles } from './Styles';
import { Color } from './Color'; import { Color } from './Color';
import { GraphLayouts } from './GraphLayouts';
interface NodeOptions { interface NodeOptions {
name: string; name: string;

160
lib/classes/GraphStream.ts Normal file
View File

@ -0,0 +1,160 @@
/* eslint-disable line-comment-position */
/* eslint-disable no-inline-comments */
import { Transform } from 'stream';
import { GraphStreamJSON } from '../interfaces/GraphStreamJSON';
import { GraphStreamCommand } from '../enums/GraphStreamCommand';
interface Stringable {
// eslint-disable-next-line @typescript-eslint/naming-convention
toString(): string;
}
export class GraphStream extends Transform {
private _path: string[] = [];
private _state = '';
private _directional = false;
public get path (): string {
return this._path.join ('_');
}
private get level (): string {
return ' '.repeat (this._path.length);
}
private expect_state (instr: GraphStreamCommand): void {
const states = [];
if ([
'cug',
'cdg'
].includes (instr))
states.push ('');
if ([
'eg',
'cn',
'csg',
'ce'
].includes (instr))
states.push ('en', 'at', 'eg', 'cug', 'cdg', 'csg', 'ce');
switch (instr) {
case 'en':
states.push ('cn', 'at');
break;
case 'at':
states.push ('cn', 'cug', 'cdg', 'csg');
break;
default:
break;
}
if (!states.includes (this._state)) {
throw new Error (`invalid state to execute command ${instr}
expected: ${states.join (', ')}`);
}
}
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/naming-convention, complexity, max-lines-per-function
public _transform (
chunk: string,
encoding: string,
callback: ((error?: Error) => unknown)
): void {
const instr = JSON.parse (chunk) as GraphStreamJSON;
this.expect_state (instr.type);
switch (instr.type) {
case 'cug': // create unordered graph
this._path.push (instr.args[0]);
this.push (`graph ${this.path} {\n`);
this._directional = false;
break;
case 'cdg': // create directional graph
this._path.push (instr.args[0]);
this.push (`digraph ${this.path} {\n`);
this._directional = true;
break;
case 'csg': // create subgraph
this.push (
`${this.level}subgraph cluster_${this.path}_${instr.args[0]} {\n`
);
this._path.push (instr.args[0]);
break;
case 'eg': // end graph
this._path.pop ();
this.push (`${this.level}}\n\n`);
break;
case 'cn': // create node
this.push (`${this.level}${this.path}_${instr.args[0]}`);
break;
case 'en': // end node
this.push ('\n');
break;
case 'at': // add attributes
if (this._state === 'cn') {
this.push (` [${instr.args.join (', ')}]`);
}
else {
this.push (`${this.level}${
instr.args.join (`\n${this.level}`)
.replace (/"/gu, '')
}\n\n`);
}
break;
case 'ce':
this.push (`${this.level}${
instr.args[0]
} -${this._directional ? '>' : '-'} ${instr.args[1]}\n`);
break;
default:
break;
}
this._state = instr.type;
callback ();
}
public write (
instr: GraphStreamJSON
): boolean {
return super.write (JSON.stringify (instr), 'utf-8');
}
public create_node (name: string): boolean {
return this.write ({ type: 'cn', args: [ name ] });
}
public end_node (): boolean {
return this.write ({ type: 'en', args: [] });
}
public create_graph (name: string, type: 'u'|'d'|'s' = 's'): boolean {
const instr_type = `c${type}g` as GraphStreamCommand;
return this.write ({ type: instr_type, args: [ name ] });
}
public end_graph (): boolean {
return this.write ({ type: 'eg', args: [] });
}
public attributes (attrs: Record<string, Stringable>): boolean {
const solved = [];
for (const attr of Object.keys (attrs)) {
const val = attrs[attr].toString ();
if ((/\n/u).test (val))
solved.push (`${attr} = <${val}>`);
else
solved.push (`${attr} = "${val}"`);
}
return this.write ({ type: 'at', args: solved });
}
public create_edge (origin: string, target: string): boolean {
return this.write ({
type: 'ce',
args: [
origin,
target
]
});
}
}

View File

@ -1,5 +1,5 @@
import { NodeStyles } from '../enums/Styles';
import { Element } from './Element'; import { Element } from './Element';
import { NodeStyles } from './Styles';
import { Color } from './Color'; import { Color } from './Color';
export class Node extends Element { export class Node extends Element {

View File

@ -0,0 +1,9 @@
export type GraphLayouts =
'neato'
| 'dot'
| 'circo'
| 'fdp'
| 'sfdp'
| 'osage'
| 'twopi'
| 'patchwork'

View File

@ -0,0 +1,11 @@
/* eslint-disable line-comment-position */
/* eslint-disable no-inline-comments */
export type GraphStreamCommand =
'cn'| // create node
'en'| // end node
'cug'| // create unordered graph
'cdg'| // create directional graph
'csg'| // create subgraph
'eg'| // end graph
'at'| // add attributes
'ce' // create edge

32
lib/enums/Styles.ts Normal file
View File

@ -0,0 +1,32 @@
type EdgeStyles =
''
|'solid'
|'dashed'
|'dotted'
|'bold'
type NodeStyles =
''
|'solid'
|'dashed'
|'dotted'
|'bold'
|'rounded'
|'diagonals'
|'filled'
|'striped'
|'wedged'
|'invis'
type GraphStyles =
'solid'
| 'dashed'
| 'dotted'
| 'bold'
| 'rounded'
| 'filled'
| 'striped'
export { EdgeStyles, NodeStyles, GraphStyles };

View File

@ -1,8 +1,12 @@
export { Graph } from './Graph'; import { Graph } from './classes/Graph';
export { Node } from './Node';
export { Element } from './Element'; export * from './classes/Color';
export { Edge } from './Edge'; export * from './classes/Edge';
export { Color } from './Color'; export * from './classes/Element';
export { EdgeStyles, NodeStyles, GraphStyles } from './Styles'; export { Graph };
export { GraphLayouts } from './GraphLayouts'; export * from './classes/GraphStream';
export { Graphable } from './Graphable'; export * from './classes/Node';
export * from './enums/GraphLayouts';
export * from './enums/Styles';
export * from './interfaces/Graphable';
export default Graph;

View File

@ -0,0 +1,6 @@
import { GraphStreamCommand } from '../enums/GraphStreamCommand';
export interface GraphStreamJSON {
type: GraphStreamCommand;
args: string[];
}

View File

@ -1,4 +1,4 @@
import { Graph } from './Graph'; import { Graph } from '../classes/Graph';
export interface Graphable { export interface Graphable {
to_graph(g: Graph, ...args: unknown[]): unknown; to_graph(g: Graph, ...args: unknown[]): unknown;

View File

@ -1,11 +1,11 @@
import test from 'ava'; import test from 'ava';
import { Edge, Color, EdgeStyles } from '../lib'; import { Edge, Color } from '../lib';
test ('serialize', (t) => { test ('serialize', (t) => {
const e = new Edge ('foo', 'bar', false); const e = new Edge ('foo', 'bar', false);
e.color = Color.white; e.color = Color.white;
e.style = EdgeStyles.dashed; e.style = 'dashed';
const serialized = e.toString (); const serialized = e.toString ();
t.is (serialized, 'foo -- bar [style="dashed",color="#ffffff"]'); t.is (serialized, 'foo -- bar [style="dashed",color="#ffffff"]');
@ -15,7 +15,7 @@ test ('serialize directional', (t) => {
const e = new Edge ('foo', 'bar', true); const e = new Edge ('foo', 'bar', true);
e.color = Color.white; e.color = Color.white;
e.style = EdgeStyles.dashed; e.style = 'dashed';
const serialized = e.toString (); const serialized = e.toString ();
t.is (serialized, 'foo -> bar [style="dashed",color="#ffffff"]'); t.is (serialized, 'foo -> bar [style="dashed",color="#ffffff"]');

View File

@ -1,5 +1,5 @@
import test from 'ava'; import test from 'ava';
import { Graph, GraphStyles, Color, NodeStyles, GraphLayouts } from '../lib'; import { Graph, Color } from '../lib';
const result = `digraph foo { const result = `digraph foo {
subgraph cluster_foo_baz { subgraph cluster_foo_baz {
@ -56,23 +56,23 @@ test ('serialize', (t) => {
graph.add_node ('asd'); graph.add_node ('asd');
graph.add_node ((n) => { graph.add_node ((n) => {
n.name = 'test'; n.name = 'test';
n.style = NodeStyles.bold; n.style = 'bold';
n.color = Color.gray; n.color = Color.gray;
}); });
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
graph.add_graph ((g) => { graph.add_graph ((g) => {
g.style = GraphStyles.dotted; g.style = 'dotted';
g.color = Color.gray; g.color = Color.gray;
g.name = 'nested'; g.name = 'nested';
// eslint-disable-next-line no-shadow, max-nested-callbacks // eslint-disable-next-line no-shadow, max-nested-callbacks
g.add_graph ((g) => { g.add_graph ((g) => {
g.style = GraphStyles.dotted; g.style = 'dotted';
g.color = Color.gray; g.color = Color.gray;
}); });
}); });
graph.style = GraphStyles.bold; graph.style = 'bold';
graph.color = Color.red; graph.color = Color.red;
}); });
@ -111,11 +111,11 @@ test ('non directional', (t) => {
test ('attributes', (t) => { test ('attributes', (t) => {
const g = new Graph ('attr'); const g = new Graph ('attr');
g.layout = GraphLayouts.neato; g.layout = 'neato';
g.overlap = false; g.overlap = false;
g.splines = true; g.splines = true;
g.color = Color.black; g.color = Color.black;
g.style = GraphStyles.bold; g.style = 'bold';
t.is (g.toString (), attributes); t.is (g.toString (), attributes);
}); });

View File

@ -1,5 +1,5 @@
import test from 'ava'; import test from 'ava';
import { NodeStyles, Node, Color } from '../lib'; import { Node, Color } from '../lib';
const serialized_simple const serialized_simple
= 'bar_foo [label="baz", style="dashed", color="#00ff00"]'; = 'bar_foo [label="baz", style="dashed", color="#00ff00"]';
@ -13,7 +13,7 @@ test ('serialize simple', (t) => {
const g = new Node ('foo', 'bar', 'baz'); const g = new Node ('foo', 'bar', 'baz');
g.color = Color.green; g.color = Color.green;
g.style = NodeStyles.dashed; g.style = 'dashed';
const serialized = g.toString (); const serialized = g.toString ();
@ -25,7 +25,7 @@ test ('serialize table', (t) => {
const g = new Node ('foo', 'bar', 'baz'); const g = new Node ('foo', 'bar', 'baz');
g.color = Color.green; g.color = Color.green;
g.style = NodeStyles.invisible; g.style = 'invis';
g.table_contents = [ g.table_contents = [
[ [

89
test/Stream.ts Normal file
View File

@ -0,0 +1,89 @@
import test from 'ava';
import { GraphStream, Color } from '../lib/index';
const simple = `graph foo {
}
`;
const complex = `digraph foo {
subgraph cluster_foo_baz {
color = #ff0000
style = bold
subgraph cluster_foo_baz_nested {
color = #808080
style = dotted
subgraph cluster_foo_baz_nested_unnamed {
color = #808080
style = dotted
}
}
foo_baz_asd [label = "asd"]
foo_baz_test [style = "bold", color = "#808080"]
}
foo_baz [label = "baz"]
foo_foo [label = "foo"]
foo_foo -> foo_baz
}
`;
test ('stream graph', (t) => new Promise ((resolve) => {
let output = '';
const stream = (new GraphStream);
stream.on ('data', (data) => {
output += data;
});
stream.on ('end', () => {
t.is (output, simple);
resolve ();
});
stream.create_graph ('foo', 'u');
stream.end_graph ();
stream.end ();
}));
// eslint-disable-next-line max-statements
test ('complex stream graph', (t) => new Promise ((resolve) => {
let output = '';
const stream = (new GraphStream);
stream.on ('data', (data) => {
output += data;
});
stream.on ('end', () => {
t.is (output, complex);
t.log (output);
resolve ();
});
stream.create_graph ('foo', 'd');
stream.create_graph ('baz');
stream.attributes ({ color: Color.red, style: 'bold' });
stream.create_graph ('nested');
stream.attributes ({ color: Color.gray, style: 'dotted' });
stream.create_graph ('unnamed');
stream.attributes ({ color: Color.gray, style: 'dotted' });
stream.end_graph ();
stream.end_graph ();
stream.create_node ('asd');
stream.attributes ({ label: 'asd' });
stream.end_node ();
stream.create_node ('test');
stream.attributes ({ style: 'bold', color: Color.gray });
stream.end_node ();
stream.end_graph ();
stream.create_node ('baz');
stream.attributes ({ label: 'baz' });
stream.end_node ();
stream.create_node ('foo');
stream.attributes ({ label: 'foo' });
stream.end_node ();
stream.create_edge (`${stream.path}_foo`, `${stream.path}_baz`);
stream.end_graph ();
stream.end ();
}));

View File

@ -241,18 +241,18 @@
eslint-plugin-import "^2.20.1" eslint-plugin-import "^2.20.1"
"@sapphirecode/eslint-config-ts@^1.0.22": "@sapphirecode/eslint-config-ts@^1.0.22":
version "1.0.38" version "1.0.40"
resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config-ts/-/eslint-config-ts-1.0.38.tgz#9c2c80be2b6bee6a0c8dc2a9d0504869d46f5882" resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config-ts/-/eslint-config-ts-1.0.40.tgz#e646383a933ff64d2604bd6ddd2f89e1aa7dfb1a"
integrity sha512-sOzBGUmpc12/j5Z2opMTqpU/XztZOdRQy+/+0wLMLATs9PVHW55oprvWLDQoWttH763v2DwU3PNvoXILq1eHGw== integrity sha512-h0m5ohVZB8jKubT567gjykpkVN/ybVkbKNWT9q5GhltECVB9g/Y6Jh6E9Fqg1pTz+DE+iHqr7CO9+stVVLwc4w==
dependencies: dependencies:
"@sapphirecode/eslint-config-es6" "^1.0.1" "@sapphirecode/eslint-config-es6" "^1.0.1"
"@typescript-eslint/eslint-plugin" "^2.26.0" "@typescript-eslint/eslint-plugin" "^2.26.0"
"@typescript-eslint/parser" "^2.26.0" "@typescript-eslint/parser" "^2.26.0"
"@sapphirecode/eslint-config@^2.0.1": "@sapphirecode/eslint-config@^2.0.1":
version "2.0.23" version "2.0.24"
resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config/-/eslint-config-2.0.23.tgz#1669742219dab81ac83914d8826d88f06492ba2c" resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config/-/eslint-config-2.0.24.tgz#e9a033767ce0ab1242dc3af700abb58b1c68ab65"
integrity sha512-GO5KDZ58z59raCUkX7CzlATkqGETsOts2S/kOoxNhZR6+rdXQqkrA8BI4/degzZiWnKYn3KkQhyYvXv7fXKkFg== integrity sha512-Fk5opga9EEMT4mXhmcg2KVKV1bnB8JAQWklMQay7ROw3QK5oP7A0KIJMlamYhXmHgctjGjlEEuJeFi10pO5QuA==
dependencies: dependencies:
eslint-plugin-node "^11.0.0" eslint-plugin-node "^11.0.0"
eslint-plugin-sort-requires-by-path "^1.0.2" eslint-plugin-sort-requires-by-path "^1.0.2"