Minor refactoring of @hediet/debug-visualizer-data-extraction's API. Improved documentation.

This commit is contained in:
Henning Dieterichs
2020-02-10 16:00:17 +01:00
parent a0597d428b
commit 6614198c2a
19 changed files with 400 additions and 123 deletions

View File

@@ -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 };

View File

@@ -17,4 +17,4 @@ export type DataExtractorInfo = {
export type DataExtractorId = {
__brand: "DataExtractorId";
};
} & string;

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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();
},

View File

@@ -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;

View File

@@ -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";

View File

@@ -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);
}
}

View File

@@ -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);
}
});
}