Skip to content

Commit

Permalink
vizgraph 'dot' support + MetaInfo cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
Geoffrey Hendrey committed Nov 1, 2023
1 parent dba7003 commit eb63d39
Show file tree
Hide file tree
Showing 7 changed files with 443 additions and 35 deletions.
6 changes: 6 additions & 0 deletions src/JsonPointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,10 @@ export default class JSONPointer {
if (refTokens.length === 0) { return ''; }
return '/' + refTokens.map(JSONPointer.escape).join('/');
}

static parent(pointer){
const asArray = Array.isArray(pointer);
const refTokens = Array.isArray(pointer) ? pointer : JSONPointer.parse(pointer);
return asArray?refTokens.slice(0,-1):this.compile(refTokens.slice(0,-1));
}
}
22 changes: 15 additions & 7 deletions src/MetaInfoProducer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import jsonata from "jsonata";
import TemplateProcessor from "./TemplateProcessor.js";
import { default as jp } from './JsonPointer.js';

export interface MetaInfo{
materialized__:boolean,
jsonPointer__: JsonPointerStructureArray|JsonPointerString;
parent__:JsonPointerStructureArray|JsonPointerString;
dependees__:JsonPointerStructureArray[]|JsonPointerString[];
dependencies__:JsonPointerStructureArray[]|JsonPointerString[];
absoluteDependencies__:JsonPointerStructureArray[]|JsonPointerString[];
treeHasExpressions__: boolean;
tags__:Set<string>;
exprRootPath__?: string;
expr__?: string;
compiledExpr__?: jsonata.Expression;
temp__?:boolean; //temp field indicates this field is !${...} and will be removed after template is processed
parentJsonPointer__?:JsonPointerStructureArray|JsonPointerString
exprTargetJsonPointer__?:JsonPointerStructureArray|JsonPointerString
data__?:any
}

export type JsonPointerStructureArray = string[];
export type JsonPointerStructureArray = (string|number)[];
export type JsonPointerString = string;

export default class MetaInfoProducer {
Expand All @@ -32,18 +37,20 @@ export default class MetaInfoProducer {

static async getMetaInfos(template):Promise<MetaInfo[]> {

const stack:MetaInfo[] = [];
const emit:MetaInfo[] = [];
const stack: MetaInfo[] = [];
const emit: MetaInfo[] = [];

async function getPaths(o, path = []) {
async function getPaths(o, path: JsonPointerStructureArray = []) {
const type = typeof o;
const metaInfo:MetaInfo = {
const metaInfo: MetaInfo = {
"materialized__": true,
"jsonPointer__": path,
"dependees__": [],
"dependencies__": [],
"absoluteDependencies__": [],
"treeHasExpressions__": false,
"tags__": new Set(),
"parent__": jp.parent(path)
};
stack.push(metaInfo);
if (Array.isArray(o)) {
Expand All @@ -61,7 +68,7 @@ export default class MetaInfoProducer {
const match = MetaInfoProducer.EMBEDDED_EXPR_REGEX.exec(o);
const getMatchGroup = (groupName) => match && match.groups[groupName];

const keyEndsWithDollars = typeof path[path.length - 1] === 'string' && path[path.length - 1].endsWith('$');
const keyEndsWithDollars = typeof path[path.length - 1] === 'string' && String(path[path.length - 1]).endsWith('$');
const tag = getMatchGroup('tag');
const exclamationPoint = !!getMatchGroup('tempVariable');
const leadingSlash = getMatchGroup('slash');
Expand All @@ -77,6 +84,7 @@ export default class MetaInfoProducer {
"exprRootPath__": slashOrCdUp,
"expr__": expr,
"jsonPointer__": path,
"exprTargetJsonPointer__": jp.parent(path)
};
if (tag) {
stack[stack.length - 1].tags__.add(tag);
Expand Down
68 changes: 40 additions & 28 deletions src/TemplateProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export default class TemplateProcessor {
/** Allows caller to set a callback to propagate initialization into their framework */
public onInit: () => void;


public static fromString(template:string, context = {}, options={} ):TemplateProcessor{
let inferredType: "JSON" | "YAML" | "UNKNOWN" = "UNKNOWN";

Expand Down Expand Up @@ -208,7 +209,7 @@ export default class TemplateProcessor {
this.executionPlans = {}; //clear execution plans
let parsedJsonPtr = jp.parse(jsonPtr);
parsedJsonPtr = isEqual(parsedJsonPtr, [""]) ? [] : parsedJsonPtr; //correct [""] to []
let metaInfos = await this.createMetaInfos(template, parsedJsonPtr);
const metaInfos = await this.createMetaInfos(template, parsedJsonPtr);
this.metaInfoByJsonPointer[jsonPtr] = metaInfos; //dictionary for template meta info, by import path (jsonPtr)
this.sortMetaInfos(metaInfos);
this.populateTemplateMeta(metaInfos);
Expand Down Expand Up @@ -412,15 +413,15 @@ export default class TemplateProcessor {

let metaInfos = initialMetaInfos.reduce((acc, metaInfo) => {
metaInfo.jsonPointer__ = [...rootJsonPtr, ...metaInfo.jsonPointer__];
metaInfo.parentJsonPointer__ = metaInfo.jsonPointer__.slice(0, -1);
metaInfo.exprTargetJsonPointer__ = metaInfo.jsonPointer__.slice(0, -1);
const cdUpPath = metaInfo.exprRootPath__;

if (cdUpPath) {
const cdUpParts = cdUpPath.match(/\.\.\//g);
if (cdUpParts) {
metaInfo.parentJsonPointer__ = metaInfo.parentJsonPointer__.slice(0, -cdUpParts.length);
metaInfo.exprTargetJsonPointer__ = metaInfo.exprTargetJsonPointer__.slice(0, -cdUpParts.length);
} else if (cdUpPath.match(/^\/$/g)) {
metaInfo.parentJsonPointer__ = [];
metaInfo.exprTargetJsonPointer__ = [];
} else {
const jsonPtr = jp.compile(metaInfo.jsonPointer__);
const msg = `unexpected 'path' expression '${cdUpPath} (see https://github.com/cisco-open/stated#rerooting-expressions)`;
Expand Down Expand Up @@ -466,7 +467,7 @@ export default class TemplateProcessor {
private populateTemplateMeta(metaInfos) {
metaInfos.forEach(meta => {
const initialDependenciesPathParts = this.removeLeadingDollarsFromDependencies(meta);
meta.absoluteDependencies__ = this.makeDepsAbsolute(meta.parentJsonPointer__, initialDependenciesPathParts);
meta.absoluteDependencies__ = this.makeDepsAbsolute(meta.exprTargetJsonPointer__, initialDependenciesPathParts);
meta.dependencies__ = initialDependenciesPathParts;
//so if we will never allow replacement of the entire root document. But modulo that if-statement we can setup the templateMeta
if (meta.jsonPointer__.length > 0) {
Expand All @@ -481,36 +482,50 @@ export default class TemplateProcessor {
private static compileToJsonPointer(meta) {
meta.absoluteDependencies__ = [...new Set(meta.absoluteDependencies__.map(jp.compile))];
meta.dependencies__ = meta.dependencies__.map(jp.compile);
meta.parentJsonPointer__ = jp.compile(meta.parentJsonPointer__);
meta.exprTargetJsonPointer__ = jp.compile(meta.exprTargetJsonPointer__);
meta.jsonPointer__ = jp.compile(meta.jsonPointer__);
meta.parent__ = jp.compile(meta.parent__);
}

private setupDependees(metaInfos) {
metaInfos.forEach(i => {
i.absoluteDependencies__?.forEach(ptr => {
if (!jp.has(this.templateMeta, ptr)) {
jp.set(this.templateMeta, ptr, {
const parent = jp.parent(ptr);
const nonMaterialized = {
"materialized__": false,
"jsonPointer__": ptr,
"dependees__": [],
"dependencies__": [],
"absoluteDependencies__": [],
"tags__": new Set()
});
"dependees__": [], //a non-materialized node has a dependency on the parent node
"dependencies__": [], //we are passed the phase where dependencies have been converted to absolute so we can skip populating this
"absoluteDependencies__": [], //parent.length===0?[]:[parent], //empty parent is root document; tracking dep's on root is silly
"tags__": new Set<string>(),
"treeHasExpressions__": false,
parent__: parent
};
jp.set(this.templateMeta, ptr, nonMaterialized);
metaInfos.push(nonMaterialized); //create metaInfos node for non-materialized node

}
const meta = jp.get(this.templateMeta, ptr);
//so there is still the possibility that the node in the templateMeta existed, but it was just created
//as an empty object or array node when a "deeper" json pointer was set. Like /view/0/0/0/name would
//result in 2 empty intermediate array objects. And then someone can have a dependency on /view/0 or
///view/0/0 neither of which would have had their metadata properly defaulted
if(meta.jsonPointer__ === undefined){
meta.materialized__ = false;
meta.jsonPointer__ = ptr;
meta.dependees__ = [];
meta.dependencies__ = [];
meta.absoluteDependencies__ = [];
meta.tags__ = [];
const parent = jp.parent(ptr);
const nonMaterialized = {
"materialized__": false,
"jsonPointer__": ptr,
"dependees__": [],
"dependencies__": [],
"absoluteDependencies__": [], //parent.length===0?[]:[parent],
"tags__": new Set<string>(),
"treeHasExpressions__": false,
parent__: parent
};
merge(meta, nonMaterialized);
}

meta.dependees__.push(i.jsonPointer__);
});
});
Expand Down Expand Up @@ -624,7 +639,7 @@ export default class TemplateProcessor {
}
}
}
const searchUpForExpression = (childNode)=> {
const searchUpForExpression = (childNode):MetaInfo=> {
let pathParts = jp.parse(childNode.jsonPointer__);
while (pathParts.length > 1) {
pathParts = pathParts.slice(0, -1); //get the parent expression
Expand Down Expand Up @@ -899,10 +914,10 @@ export default class TemplateProcessor {

private async _evaluateExprNode(jsonPtr) {
let evaluated;
const {compiledExpr__, callback__, parentJsonPointer__, jsonPointer__, expr__} = jp.get(this.templateMeta, jsonPtr);
const {compiledExpr__, callback__, exprTargetJsonPointer__, jsonPointer__, expr__} = jp.get(this.templateMeta, jsonPtr);
let target;
try {
target = jp.get(this.output, parentJsonPointer__); //an expression is always relative to a target
target = jp.get(this.output, exprTargetJsonPointer__); //an expression is always relative to a target
const safe = this.withErrorHandling.bind(this);
evaluated = await compiledExpr__.evaluate(
target,
Expand Down Expand Up @@ -978,13 +993,10 @@ export default class TemplateProcessor {
//----------------- utility functions ----------------//
const queueParent = (jsonPtr)=>{
//search "up" from this currentPtr to find any dependees of the ancestors of currentPtr
const parts = jp.parse(jsonPtr);
if(parts.length>1) {
const _parentPointer = jp.compile(parts.slice(0, parts.length - 1));
if (!visited.has(_parentPointer)) {
queue.push(_parentPointer);
visited.add(_parentPointer);
}
const parentPointer = jp.parent(jsonPtr);//jp.compile(parts.slice(0, parts.length - 1));
if (parentPointer !== '' && !visited.has(parentPointer)) {
queue.push(parentPointer);
visited.add(parentPointer);
}
}

Expand Down
88 changes: 88 additions & 0 deletions src/VizGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Convert an array of MetaInfo objects into DOT notation for graph representation.
* @param metaInfos - Array of MetaInfo objects
* @returns DOT notation string
*/
import TemplateProcessor from "./TemplateProcessor.js";
import {default as jp} from "./JsonPointer.js"
export default class VizGraph {
public static dot(tp: TemplateProcessor) {
const metaInfos = tp.metaInfoByJsonPointer['/'];
let dotString = `digraph MetaInfoGraph {\n`;
dotString += ' node [fontname="Arial", fontsize=12];\n'; // Set default node font
const bgColor = "#282a36";
dotString += ` bgcolor="${bgColor}";`; // Dracula background color

for (let metaInfo of metaInfos) {
let sourcePointer = metaInfo.jsonPointer__;

// Skip nodes with an empty jsonPointer__
if (sourcePointer === "") {
continue;
}

// Convert data__ based on its type
let dataPreview = "undefined";
let data;
if(metaInfo.temp__ === true){
dataPreview = "--REMOVED (! var)--"
}else if(jp.has(tp.output, metaInfo.jsonPointer__)){
data = jp.get(tp.output, metaInfo.jsonPointer__);
if (typeof data === 'object' && data !== null) {
dataPreview = Array.isArray(data) ? '[...]' : '{...}';
} else if (data !== undefined) {
dataPreview = String(data).substr(0, 10);
}
}else{
dataPreview = "--WARNING: data not found--";
}

// Color and style logic
let fillColor = '#e2dfdf'; // Muted gray
let fontColor = '#44475a'; // Dracula comment color
let style = "filled"
if (!metaInfo.materialized__) {
fillColor = '#ffb86c'; // Muted gold
style = style + ",dashed";
} else if (metaInfo.expr__) {
fillColor = '#87c095'; // Muted green
fontColor = '#f8f8f2'; // Dracula foreground color
}

// Node label
let label = `${sourcePointer}\nData: ${dataPreview}`;
if (metaInfo.expr__) {
label += `\n$\{${metaInfo.expr__}\}`; // Display expression within ${}
}
dotString += ` "${sourcePointer}" [label="${label}", style="${style}", fillcolor="${fillColor}", fontcolor="${fontColor}" ];\n`;
}

for (let metaInfo of metaInfos) {
const sourcePointer = metaInfo.jsonPointer__;

// Add dependencies__ edges
for (let dependency of metaInfo.absoluteDependencies__) {
const targetPointer = dependency;
dotString += ` "${sourcePointer}" -> "${targetPointer}" [label="depends on", color="#bd93f9", fontcolor="#bd93f9" ];\n`; // Dracula purple
}

/*
// Add dependees__ edges
for (let dependee of metaInfo.dependees__) {
const targetPointer = dependee;
dotString += ` "${sourcePointer}" -> "${targetPointer}" [label="dependee", color="#ff79c6", fontcolor="#ff79c6"];\n`; // Dracula pink
}
*/

// Add parentJsonPointer__ edge
if (metaInfo.parent__) {
const parentPointer = metaInfo.parent__;
dotString += ` "${sourcePointer}" -> "${parentPointer}" [label="parent", color="#8be9fd", fontcolor="#8be9fd"];\n`; // Dracula cyan
}
}

dotString += '}\n';
return dotString;
}
}
Loading

0 comments on commit eb63d39

Please sign in to comment.