Minor refactoring of @hediet/debug-visualizer-data-extraction's API. Improved documentation.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { ExtractedData } from "./DataExtractionResult";
|
||||
|
||||
export type CommonDataType =
|
||||
| CommonDataTypes.Text
|
||||
| CommonDataTypes.Svg
|
||||
| CommonDataTypes.Html
|
||||
| CommonDataTypes.DotGraph
|
||||
| CommonDataTypes.Tree
|
||||
| CommonDataTypes.Ast
|
||||
| CommonDataTypes.Graph;
|
||||
|
||||
export function isCommonDataType<T>(
|
||||
data: ExtractedData,
|
||||
kind: T
|
||||
@@ -14,15 +23,6 @@ export function isCommonDataType<T>(
|
||||
|
||||
type Narrow<T, TKind> = T extends TKind ? T : never;
|
||||
|
||||
export type CommonDataType =
|
||||
| CommonDataTypes.Text
|
||||
| CommonDataTypes.Svg
|
||||
| CommonDataTypes.Html
|
||||
| CommonDataTypes.DotGraph
|
||||
| CommonDataTypes.Tree
|
||||
| CommonDataTypes.Ast
|
||||
| CommonDataTypes.Graph;
|
||||
|
||||
export module CommonDataTypes {
|
||||
export interface Text {
|
||||
kind: { text: true };
|
||||
|
||||
@@ -17,4 +17,4 @@ export type DataExtractorInfo = {
|
||||
|
||||
export type DataExtractorId = {
|
||||
__brand: "DataExtractorId";
|
||||
};
|
||||
} & string;
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
import { ExtractedData } from "../DataExtractionResult";
|
||||
|
||||
export interface DataExtractor<T extends ExtractedData> {
|
||||
/**
|
||||
* Must be unique among all data extractors.
|
||||
*/
|
||||
id: string;
|
||||
getExtractions(
|
||||
data: unknown,
|
||||
extractionCollector: ExtractionCollector<T>,
|
||||
evalFn: <TEval>(expression: string) => TEval
|
||||
context: DataExtractorContext
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface ExtractionCollector<T extends ExtractedData> {
|
||||
/**
|
||||
* Suggests a possible extraction.
|
||||
*/
|
||||
addExtraction(extraction: DataExtraction<T>): void;
|
||||
}
|
||||
|
||||
export interface DataExtractorContext {
|
||||
/**
|
||||
* Evaluates an expression in the context of the active stack frame.
|
||||
*/
|
||||
evalFn: <TEval>(expression: string) => TEval;
|
||||
}
|
||||
|
||||
export interface DataExtraction<TData extends ExtractedData> {
|
||||
// the higher the better
|
||||
/**
|
||||
* Higher priorities are preferred.
|
||||
*/
|
||||
priority: number;
|
||||
|
||||
/**
|
||||
* A unique id identifying this extraction among all extractions.
|
||||
* Required to express extraction preferences.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* A user friendly name of this extraction.
|
||||
*/
|
||||
name: string;
|
||||
extractData(): TData;
|
||||
}
|
||||
@@ -24,7 +47,7 @@ export interface DataExtraction<TData extends ExtractedData> {
|
||||
export function getExtractions<T extends ExtractedData>(
|
||||
extractor: DataExtractor<T>,
|
||||
data: unknown,
|
||||
evalFn: <T>(expression: string) => T
|
||||
context: DataExtractorContext
|
||||
): DataExtraction<T>[] {
|
||||
const extractions = new Array<DataExtraction<T>>();
|
||||
const collector: ExtractionCollector<T> = {
|
||||
@@ -32,6 +55,6 @@ export function getExtractions<T extends ExtractedData>(
|
||||
extractions.push(extraction);
|
||||
},
|
||||
};
|
||||
extractor.getExtractions(data, collector, evalFn);
|
||||
extractor.getExtractions(data, collector, context);
|
||||
return extractions;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,35 @@ import {
|
||||
ExtractionCollector,
|
||||
} from "./DataExtractor";
|
||||
|
||||
export interface DataExtractorApi {
|
||||
/**
|
||||
* Registers a single extractor.
|
||||
* @preferExisting if `true`, an existing extractor is not overwritten.
|
||||
*/
|
||||
registerExtractor<TExtractedData extends ExtractedData>(
|
||||
extractor: DataExtractor<TExtractedData>,
|
||||
preferExisting?: boolean
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Registers multiple extractors.
|
||||
* @preferExisting if `true`, an existing extractor is not overwritten.
|
||||
*/
|
||||
registerExtractors(
|
||||
extractors: DataExtractor<ExtractedData>[],
|
||||
preferExisting?: boolean
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Extracts data from `value`.
|
||||
*/
|
||||
getData(
|
||||
value: unknown,
|
||||
evalFn: <T>(expression: string) => T,
|
||||
preferredDataExtractorId: string | undefined
|
||||
): JSONString<DataResult>;
|
||||
}
|
||||
|
||||
export type DataResult =
|
||||
| {
|
||||
kind: "Data";
|
||||
@@ -21,26 +50,18 @@ export interface JSONString<T> extends String {
|
||||
__brand: { json: T };
|
||||
}
|
||||
|
||||
export interface DataExtractorApi {
|
||||
registerExtractor<TExtractedData extends ExtractedData>(
|
||||
extractor: DataExtractor<TExtractedData>
|
||||
): void;
|
||||
registerExtractors(extractors: DataExtractor<ExtractedData>[]): void;
|
||||
getData(
|
||||
value: unknown,
|
||||
evalFn: <T>(expression: string) => T,
|
||||
preferredDataExtractorId: string | undefined
|
||||
): JSONString<DataResult>;
|
||||
}
|
||||
|
||||
declare const window: any;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function selfContainedInitDataExtractorApi(): boolean {
|
||||
const obj = typeof window === "object" ? (window as any) : (global as any);
|
||||
const globalObj =
|
||||
typeof window === "object" ? (window as any) : (global as any);
|
||||
const key = "@hediet/data-extractor/v1";
|
||||
const prefix = key + "::";
|
||||
|
||||
let api: DataExtractorApi | undefined = obj[key];
|
||||
let api: DataExtractorApi | undefined = globalObj[key];
|
||||
if (api) {
|
||||
return false;
|
||||
}
|
||||
@@ -49,29 +70,38 @@ export function selfContainedInitDataExtractorApi(): boolean {
|
||||
return JSON.stringify(data) as any;
|
||||
}
|
||||
function getExtractors(): DataExtractor<ExtractedData>[] {
|
||||
return Object.entries(obj)
|
||||
return Object.entries(globalObj)
|
||||
.filter(([key, value]) => key.startsWith(prefix))
|
||||
.map(([key, value]) => value as DataExtractor<ExtractedData>);
|
||||
}
|
||||
obj["$asData"] = (x: unknown) => x;
|
||||
obj[key] = api = {
|
||||
registerExtractor(extractor) {
|
||||
obj[prefix + extractor.id] = extractor;
|
||||
globalObj["$asData"] = (x: unknown) => x;
|
||||
globalObj[key] = api = {
|
||||
registerExtractor(extractor, preferExisting) {
|
||||
const key = prefix + extractor.id;
|
||||
if (preferExisting && key in globalObj) {
|
||||
// don't overwrite existing data extractor
|
||||
return;
|
||||
}
|
||||
globalObj[key] = extractor;
|
||||
},
|
||||
registerExtractors(extractors: DataExtractor<ExtractedData>[]) {
|
||||
registerExtractors(
|
||||
extractors: DataExtractor<ExtractedData>[],
|
||||
preferExisting
|
||||
) {
|
||||
for (const e of extractors) {
|
||||
this.registerExtractor(e);
|
||||
this.registerExtractor(e, preferExisting);
|
||||
}
|
||||
},
|
||||
getData(value, evalFn, preferredDataExtractorId) {
|
||||
const extractions = new Array<DataExtraction<ExtractedData>>();
|
||||
const collector: ExtractionCollector<ExtractedData> = {
|
||||
const extractionCollector: ExtractionCollector<ExtractedData> = {
|
||||
addExtraction(extraction) {
|
||||
extractions.push(extraction);
|
||||
},
|
||||
};
|
||||
|
||||
for (const e of getExtractors()) {
|
||||
e.getExtractions(value, collector, evalFn);
|
||||
e.getExtractions(value, extractionCollector, { evalFn });
|
||||
}
|
||||
extractions.sort((a, b) => b.priority - a.priority);
|
||||
let usedExtraction = extractions[0];
|
||||
@@ -111,10 +141,10 @@ export function selfContainedInitDataExtractorApi(): boolean {
|
||||
};
|
||||
|
||||
api.registerExtractor({
|
||||
id: "toString",
|
||||
getExtractions(data, collector) {
|
||||
id: "to-string",
|
||||
getExtractions: (data, collector) => {
|
||||
collector.addExtraction({
|
||||
id: "toString",
|
||||
id: "to-string",
|
||||
name: "To String",
|
||||
priority: 100,
|
||||
extractData() {
|
||||
@@ -131,8 +161,15 @@ export function selfContainedInitDataExtractorApi(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This code is used to detect if the API has not been initialized yet.
|
||||
* @internal
|
||||
*/
|
||||
export const ApiHasNotBeenInitializedCode = "EgH0cybXij1jYUozyakO" as const;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi {
|
||||
const obj = typeof window === "object" ? (window as any) : (global as any);
|
||||
const key = "@hediet/data-extractor/v1";
|
||||
@@ -147,6 +184,9 @@ export function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data extractor API.
|
||||
*/
|
||||
export function getDataExtractorApi(): DataExtractorApi {
|
||||
selfContainedInitDataExtractorApi();
|
||||
return selfContainedGetInitializedDataExtractorApi();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ExtractedData } from "../../DataExtractionResult";
|
||||
import { DataExtractor, ExtractionCollector } from "..";
|
||||
import { DataExtractor, ExtractionCollector, DataExtractorContext } from "..";
|
||||
|
||||
export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
|
||||
readonly id = "as-is";
|
||||
getExtractions(
|
||||
data: unknown,
|
||||
extractionCollector: ExtractionCollector<ExtractedData>,
|
||||
evalFn: <TEval>(expression: string) => TEval
|
||||
context: DataExtractorContext
|
||||
): void {
|
||||
if (typeof data !== "object" || !data || !("kind" in data)) {
|
||||
return;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { ExtractedData } from "../../DataExtractionResult";
|
||||
import { DataExtractor, ExtractionCollector } from "..";
|
||||
import {
|
||||
ExtractionCollector,
|
||||
DataExtractor,
|
||||
DataExtractorContext,
|
||||
} from "../DataExtractor";
|
||||
|
||||
export class GetDebugVisualizationDataExtractor
|
||||
implements DataExtractor<ExtractedData> {
|
||||
readonly id = "get-debug-visualization";
|
||||
getExtractions(
|
||||
data: unknown,
|
||||
extractionCollector: ExtractionCollector<ExtractedData>,
|
||||
evalFn: <TEval>(expression: string) => TEval
|
||||
collector: ExtractionCollector<ExtractedData>,
|
||||
context: DataExtractorContext
|
||||
): void {
|
||||
if (
|
||||
typeof data !== "object" ||
|
||||
@@ -17,10 +21,10 @@ export class GetDebugVisualizationDataExtractor
|
||||
return;
|
||||
}
|
||||
|
||||
extractionCollector.addExtraction({
|
||||
collector.addExtraction({
|
||||
id: this.id,
|
||||
name: "Use Method 'getDebugVisualization'",
|
||||
priority: 500,
|
||||
priority: 600,
|
||||
extractData() {
|
||||
return (data as any).getDebugVisualization();
|
||||
},
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import * as ts from "typescript";
|
||||
import { DataExtractor, ExtractionCollector } from "../DataExtractor";
|
||||
import {
|
||||
DataExtractor,
|
||||
ExtractionCollector,
|
||||
DataExtractorContext,
|
||||
} from "../DataExtractor";
|
||||
import { CommonDataTypes } from "../../CommonDataTypes";
|
||||
|
||||
// This class is self contained and can be injected into both nodejs and browser environments.
|
||||
export class TypeScriptAstDataExtractor
|
||||
implements DataExtractor<CommonDataTypes.Ast> {
|
||||
readonly id = "TypeScriptAst";
|
||||
readonly id = "typescript-ast";
|
||||
|
||||
getExtractions(
|
||||
data: unknown,
|
||||
collector: ExtractionCollector<CommonDataTypes.Ast>,
|
||||
evalFn: <TEval>(expression: string) => TEval
|
||||
{ evalFn }: DataExtractorContext
|
||||
): void {
|
||||
if (!data) {
|
||||
return;
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
import { getDataExtractorApi } from "../DataExtractorApi";
|
||||
import { TypeScriptAstDataExtractor } from "./TypeScriptDataExtractors";
|
||||
import { AsIsDataExtractor } from "./AsIsDataExtractor";
|
||||
import { GetDebugVisualizationDataExtractor } from "./GetDebugVisualizationDataExtractor";
|
||||
export { TypeScriptAstDataExtractor } from "./TypeScriptDataExtractors";
|
||||
export { AsIsDataExtractor } from "./AsIsDataExtractor";
|
||||
export { GetDebugVisualizationDataExtractor } from "./GetDebugVisualizationDataExtractor";
|
||||
|
||||
export {
|
||||
AsIsDataExtractor,
|
||||
TypeScriptAstDataExtractor,
|
||||
GetDebugVisualizationDataExtractor,
|
||||
};
|
||||
|
||||
export function registerAll() {
|
||||
const api = getDataExtractorApi();
|
||||
for (const item of [
|
||||
new TypeScriptAstDataExtractor(),
|
||||
new AsIsDataExtractor(),
|
||||
new GetDebugVisualizationDataExtractor(),
|
||||
]) {
|
||||
api.registerExtractor(item);
|
||||
}
|
||||
}
|
||||
export { registerDefaultDataExtractors } from "./registerDefaultDataExtractors";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getDataExtractorApi } from "../DataExtractorApi";
|
||||
import { TypeScriptAstDataExtractor } from "./TypeScriptDataExtractors";
|
||||
import { AsIsDataExtractor } from "./AsIsDataExtractor";
|
||||
import { GetDebugVisualizationDataExtractor } from "./GetDebugVisualizationDataExtractor";
|
||||
|
||||
/**
|
||||
* The default data extractors should be registered by vs code anyways.
|
||||
* This however ensures that the most up to date extractors are registered.
|
||||
*/
|
||||
export function registerDefaultDataExtractors() {
|
||||
const api = getDataExtractorApi();
|
||||
for (const item of [
|
||||
new TypeScriptAstDataExtractor(),
|
||||
new AsIsDataExtractor(),
|
||||
new GetDebugVisualizationDataExtractor(),
|
||||
]) {
|
||||
api.registerExtractor(item);
|
||||
}
|
||||
}
|
||||
@@ -4,47 +4,10 @@ import {
|
||||
NodeGraphData,
|
||||
} from "../CommonDataTypes";
|
||||
|
||||
export function createGraphFromPointers<T>(
|
||||
roots: Record<string, T | undefined | null>,
|
||||
infoSelector: (
|
||||
item: T
|
||||
) => {
|
||||
id?: string | number;
|
||||
edges: ({ to: T } & Omit<EdgeGraphData, "from" | "to">)[];
|
||||
} & Omit<NodeGraphData, "id">
|
||||
): CommonDataTypes.Graph {
|
||||
const marker = {};
|
||||
|
||||
interface Pointer {
|
||||
marker: {};
|
||||
name: string;
|
||||
value: T | null | undefined;
|
||||
}
|
||||
|
||||
const items = Object.entries(roots).map<Pointer>(([name, value]) => ({
|
||||
marker,
|
||||
name,
|
||||
value,
|
||||
}));
|
||||
|
||||
const includeLabelInName = false;
|
||||
|
||||
return createGraph<T | Pointer>(items, item => {
|
||||
if ("marker" in item && item["marker"] === marker) {
|
||||
return {
|
||||
id: "label____" + item.name,
|
||||
color: "orange",
|
||||
label: item.name,
|
||||
edges: [{ to: item.value!, color: "orange", label: "" }].filter(
|
||||
t => !!t.to
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return infoSelector(item as T);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of roots, it creates a graph by following their edges recursively.
|
||||
* Tracks cycles.
|
||||
*/
|
||||
export function createGraph<T>(
|
||||
roots: T[],
|
||||
infoSelector: (
|
||||
@@ -103,3 +66,46 @@ export function createGraph<T>(
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a labeled list of roots, it creates a graph by following their edges recursively.
|
||||
* Tracks cycles.
|
||||
*/
|
||||
export function createGraphFromPointers<T>(
|
||||
roots: Record<string, T | undefined | null>,
|
||||
infoSelector: (
|
||||
item: T
|
||||
) => {
|
||||
id?: string | number;
|
||||
edges: ({ to: T } & Omit<EdgeGraphData, "from" | "to">)[];
|
||||
} & Omit<NodeGraphData, "id">
|
||||
): CommonDataTypes.Graph {
|
||||
const marker = {};
|
||||
|
||||
interface Pointer {
|
||||
marker: {};
|
||||
name: string;
|
||||
value: T | null | undefined;
|
||||
}
|
||||
|
||||
const items = Object.entries(roots).map<Pointer>(([name, value]) => ({
|
||||
marker,
|
||||
name,
|
||||
value,
|
||||
}));
|
||||
|
||||
return createGraph<T | Pointer>(items, item => {
|
||||
if ("marker" in item && item["marker"] === marker) {
|
||||
return {
|
||||
id: "label____" + item.name,
|
||||
color: "orange",
|
||||
label: item.name,
|
||||
edges: [{ to: item.value!, color: "orange", label: "" }].filter(
|
||||
t => !!t.to
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return infoSelector(item as T);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user