mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-18 15:38:10 +00:00
Add AdapterSet and FallbackAdapter (#640)
Issue #631 revealed that our current plugin-handling code is fragile - we aren't robust to having nodes from a plugin without having that plugin present in the frontend. This commit adds `StaticAdapterSet` and `DynamicAdapterSet`, which are abstractions over finding the matching plugin adapter or type for a node or edge. It's a robust abstraction, because the adapter sets always include the `StaticFallbackAdapter` or `DynamicFallbackAdapter`, which can match any node, so we'll never get an error like #631 due to not having an adapter / type available. Also relevant: #465 Test plan: Unit tests included.
This commit is contained in:
parent
33763a9f35
commit
9edd7ac069
153
src/app/adapters/adapterSet.js
Normal file
153
src/app/adapters/adapterSet.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph";
|
||||||
|
import {NodeTrie, EdgeTrie} from "../../core/trie";
|
||||||
|
import type {Repo} from "../../core/repo";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
StaticPluginAdapter,
|
||||||
|
DynamicPluginAdapter,
|
||||||
|
NodeType,
|
||||||
|
EdgeType,
|
||||||
|
} from "./pluginAdapter";
|
||||||
|
|
||||||
|
import {FallbackStaticAdapter} from "./fallbackAdapter";
|
||||||
|
|
||||||
|
export class StaticAdapterSet {
|
||||||
|
_adapters: $ReadOnlyArray<StaticPluginAdapter>;
|
||||||
|
_adapterNodeTrie: NodeTrie<StaticPluginAdapter>;
|
||||||
|
_adapterEdgeTrie: EdgeTrie<StaticPluginAdapter>;
|
||||||
|
_typeNodeTrie: NodeTrie<NodeType>;
|
||||||
|
_typeEdgeTrie: EdgeTrie<EdgeType>;
|
||||||
|
|
||||||
|
constructor(adapters: $ReadOnlyArray<StaticPluginAdapter>) {
|
||||||
|
this._adapters = [new FallbackStaticAdapter(), ...adapters];
|
||||||
|
this._adapterNodeTrie = new NodeTrie();
|
||||||
|
this._adapterEdgeTrie = new EdgeTrie();
|
||||||
|
this._typeNodeTrie = new NodeTrie();
|
||||||
|
this._typeEdgeTrie = new EdgeTrie();
|
||||||
|
const usedPluginNames = new Set();
|
||||||
|
this._adapters.forEach((a) => {
|
||||||
|
const name = a.name();
|
||||||
|
if (usedPluginNames.has(name)) {
|
||||||
|
throw new Error(`Multiple plugins with name "${name}"`);
|
||||||
|
}
|
||||||
|
usedPluginNames.add(name);
|
||||||
|
this._adapterNodeTrie.add(a.nodePrefix(), a);
|
||||||
|
this._adapterEdgeTrie.add(a.edgePrefix(), a);
|
||||||
|
});
|
||||||
|
this.nodeTypes().forEach((t) => this._typeNodeTrie.add(t.prefix, t));
|
||||||
|
this.edgeTypes().forEach((t) => this._typeEdgeTrie.add(t.prefix, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
adapters(): $ReadOnlyArray<StaticPluginAdapter> {
|
||||||
|
return this._adapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeTypes(): NodeType[] {
|
||||||
|
return [].concat(...this._adapters.map((x) => x.nodeTypes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeTypes(): EdgeType[] {
|
||||||
|
return [].concat(...this._adapters.map((x) => x.edgeTypes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterMatchingNode(x: NodeAddressT): StaticPluginAdapter {
|
||||||
|
const adapters = this._adapterNodeTrie.get(x);
|
||||||
|
if (adapters.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invariant violation: Fallback adapter matches all nodes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return adapters[adapters.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterMatchingEdge(x: EdgeAddressT): StaticPluginAdapter {
|
||||||
|
const adapters = this._adapterEdgeTrie.get(x);
|
||||||
|
if (adapters.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invariant violation: Fallback adapter matches all edges"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return adapters[adapters.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
typeMatchingNode(x: NodeAddressT): NodeType {
|
||||||
|
const types = this._typeNodeTrie.get(x);
|
||||||
|
if (types.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invariant violation: Fallback adapter's type matches all nodes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return types[types.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
typeMatchingEdge(x: EdgeAddressT): EdgeType {
|
||||||
|
const types = this._typeEdgeTrie.get(x);
|
||||||
|
if (types.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invariant violation: Fallback adapter's type matches all edges"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return types[types.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
load(repo: Repo): Promise<DynamicAdapterSet> {
|
||||||
|
return Promise.all(this._adapters.map((a) => a.load(repo))).then(
|
||||||
|
(adapters) => new DynamicAdapterSet(this, adapters)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DynamicAdapterSet {
|
||||||
|
_adapters: $ReadOnlyArray<DynamicPluginAdapter>;
|
||||||
|
_staticAdapterSet: StaticAdapterSet;
|
||||||
|
_adapterNodeTrie: NodeTrie<DynamicPluginAdapter>;
|
||||||
|
_adapterEdgeTrie: EdgeTrie<DynamicPluginAdapter>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
staticAdapterSet: StaticAdapterSet,
|
||||||
|
adapters: $ReadOnlyArray<DynamicPluginAdapter>
|
||||||
|
) {
|
||||||
|
this._staticAdapterSet = staticAdapterSet;
|
||||||
|
this._adapters = adapters;
|
||||||
|
this._adapterNodeTrie = new NodeTrie();
|
||||||
|
this._adapterEdgeTrie = new EdgeTrie();
|
||||||
|
this._adapters.forEach((a) => {
|
||||||
|
this._adapterNodeTrie.add(a.static().nodePrefix(), a);
|
||||||
|
this._adapterEdgeTrie.add(a.static().edgePrefix(), a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterMatchingNode(x: NodeAddressT): DynamicPluginAdapter {
|
||||||
|
const adapters = this._adapterNodeTrie.get(x);
|
||||||
|
if (adapters.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invariant violation: Fallback adapter matches all nodes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return adapters[adapters.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterMatchingEdge(x: EdgeAddressT): DynamicPluginAdapter {
|
||||||
|
const adapters = this._adapterEdgeTrie.get(x);
|
||||||
|
if (adapters.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invariant violation: Fallback adapter matches all edges"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return adapters[adapters.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
adapters(): $ReadOnlyArray<DynamicPluginAdapter> {
|
||||||
|
return this._adapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
graph(): Graph {
|
||||||
|
return Graph.merge(this._adapters.map((x) => x.graph()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static() {
|
||||||
|
return this._staticAdapterSet;
|
||||||
|
}
|
||||||
|
}
|
213
src/app/adapters/adapterSet.test.js
Normal file
213
src/app/adapters/adapterSet.test.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
NodeAddress,
|
||||||
|
EdgeAddress,
|
||||||
|
type NodeAddressT,
|
||||||
|
Graph,
|
||||||
|
} from "../../core/graph";
|
||||||
|
import type {DynamicPluginAdapter} from "./pluginAdapter";
|
||||||
|
import {StaticAdapterSet} from "./adapterSet";
|
||||||
|
import {FallbackStaticAdapter, FALLBACK_NAME} from "./fallbackAdapter";
|
||||||
|
import {makeRepo, type Repo} from "../../core/repo";
|
||||||
|
|
||||||
|
describe("app/adapters/adapterSet", () => {
|
||||||
|
class TestStaticPluginAdapter {
|
||||||
|
loadingMock: Function;
|
||||||
|
constructor() {
|
||||||
|
this.loadingMock = jest.fn();
|
||||||
|
}
|
||||||
|
name() {
|
||||||
|
return "other plugin";
|
||||||
|
}
|
||||||
|
nodePrefix() {
|
||||||
|
return NodeAddress.fromParts(["other"]);
|
||||||
|
}
|
||||||
|
edgePrefix() {
|
||||||
|
return EdgeAddress.fromParts(["other"]);
|
||||||
|
}
|
||||||
|
nodeTypes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "other1",
|
||||||
|
defaultWeight: 0,
|
||||||
|
prefix: NodeAddress.fromParts(["other", "1"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other2",
|
||||||
|
defaultWeight: 0,
|
||||||
|
prefix: NodeAddress.fromParts(["other", "2"]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
edgeTypes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
forwardName: "others_1",
|
||||||
|
backwardName: "othered_by_1",
|
||||||
|
prefix: EdgeAddress.fromParts(["other", "1"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forwardName: "others_2",
|
||||||
|
backwardName: "othered_by_2",
|
||||||
|
prefix: EdgeAddress.fromParts(["other", "2"]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
load(_unused_repo: Repo) {
|
||||||
|
return this.loadingMock().then(() => new TestDynamicPluginAdapter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestDynamicPluginAdapter implements DynamicPluginAdapter {
|
||||||
|
graph() {
|
||||||
|
return new Graph().addNode(NodeAddress.fromParts(["other1", "example"]));
|
||||||
|
}
|
||||||
|
nodeDescription(x: NodeAddressT) {
|
||||||
|
return `Node from the test plugin: ${NodeAddress.toString(x)}`;
|
||||||
|
}
|
||||||
|
static() {
|
||||||
|
return new TestStaticPluginAdapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("StaticAdapterSet", () => {
|
||||||
|
function example() {
|
||||||
|
const x = new TestStaticPluginAdapter();
|
||||||
|
const fallback = new FallbackStaticAdapter();
|
||||||
|
const sas = new StaticAdapterSet([x]);
|
||||||
|
return {x, fallback, sas};
|
||||||
|
}
|
||||||
|
it("errors if two plugins have the same name", () => {
|
||||||
|
const x = new TestStaticPluginAdapter();
|
||||||
|
const shouldError = () => new StaticAdapterSet([x, x]);
|
||||||
|
expect(shouldError).toThrowError("Multiple plugins with name");
|
||||||
|
});
|
||||||
|
it("always includes the fallback plugin", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
expect(sas.adapters()[0].name()).toBe(FALLBACK_NAME);
|
||||||
|
});
|
||||||
|
it("includes the manually provided plugin adapters", () => {
|
||||||
|
const {x, sas} = example();
|
||||||
|
expect(sas.adapters()[1].name()).toBe(x.name());
|
||||||
|
});
|
||||||
|
it("aggregates NodeTypes across plugins", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const nodeTypes = sas.nodeTypes();
|
||||||
|
expect(nodeTypes).toHaveLength(3);
|
||||||
|
});
|
||||||
|
it("aggregates EdgeTypes across plugins", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const edgeTypes = sas.edgeTypes();
|
||||||
|
expect(edgeTypes).toHaveLength(3);
|
||||||
|
});
|
||||||
|
it("finds adapter matching a node", () => {
|
||||||
|
const {x, sas} = example();
|
||||||
|
const matching = sas.adapterMatchingNode(
|
||||||
|
NodeAddress.fromParts(["other", "foo"])
|
||||||
|
);
|
||||||
|
expect(matching.name()).toBe(x.name());
|
||||||
|
});
|
||||||
|
it("finds adapter matching an edge", () => {
|
||||||
|
const {x, sas} = example();
|
||||||
|
const matching = sas.adapterMatchingEdge(
|
||||||
|
EdgeAddress.fromParts(["other", "foo"])
|
||||||
|
);
|
||||||
|
expect(matching.name()).toBe(x.name());
|
||||||
|
});
|
||||||
|
it("finds fallback adapter for unregistered node", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const adapter = sas.adapterMatchingNode(NodeAddress.fromParts(["weird"]));
|
||||||
|
expect(adapter.name()).toBe(FALLBACK_NAME);
|
||||||
|
});
|
||||||
|
it("finds fallback adapter for unregistered edge", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const adapter = sas.adapterMatchingEdge(EdgeAddress.fromParts(["weird"]));
|
||||||
|
expect(adapter.name()).toBe(FALLBACK_NAME);
|
||||||
|
});
|
||||||
|
it("finds type matching a node", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const type = sas.typeMatchingNode(
|
||||||
|
NodeAddress.fromParts(["other", "1", "foo"])
|
||||||
|
);
|
||||||
|
expect(type.name).toBe("other1");
|
||||||
|
});
|
||||||
|
it("finds type matching an edge", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const type = sas.typeMatchingEdge(
|
||||||
|
EdgeAddress.fromParts(["other", "1", "foo"])
|
||||||
|
);
|
||||||
|
expect(type.forwardName).toBe("others_1");
|
||||||
|
});
|
||||||
|
it("finds fallback type for unregistered node", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const type = sas.typeMatchingNode(
|
||||||
|
NodeAddress.fromParts(["wombat", "1", "foo"])
|
||||||
|
);
|
||||||
|
expect(type.name).toBe("(unknown node)");
|
||||||
|
});
|
||||||
|
it("finds fallback type for unregistered edge", () => {
|
||||||
|
const {sas} = example();
|
||||||
|
const type = sas.typeMatchingEdge(
|
||||||
|
EdgeAddress.fromParts(["wombat", "1", "foo"])
|
||||||
|
);
|
||||||
|
expect(type.forwardName).toBe("(unknown edge→)");
|
||||||
|
});
|
||||||
|
it("loads a dynamicAdapterSet", async () => {
|
||||||
|
const {x, sas} = example();
|
||||||
|
x.loadingMock.mockResolvedValue();
|
||||||
|
const das = await sas.load(makeRepo("foo", "bar"));
|
||||||
|
expect(das).toEqual(expect.anything());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DynamicAdapterSet", () => {
|
||||||
|
async function example() {
|
||||||
|
const x = new TestStaticPluginAdapter();
|
||||||
|
const sas = new StaticAdapterSet([x]);
|
||||||
|
x.loadingMock.mockResolvedValue();
|
||||||
|
const das = await sas.load(makeRepo("foo", "bar"));
|
||||||
|
return {x, sas, das};
|
||||||
|
}
|
||||||
|
it("allows retrieval of the original StaticAdapterSet", async () => {
|
||||||
|
const {sas, das} = await example();
|
||||||
|
expect(das.static()).toBe(sas);
|
||||||
|
});
|
||||||
|
it("allows accessing the dynamic adapters", async () => {
|
||||||
|
const {sas, das} = await example();
|
||||||
|
expect(das.adapters().map((a) => a.static().name())).toEqual(
|
||||||
|
sas.adapters().map((a) => a.name())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("allows retrieval of the aggregated graph", async () => {
|
||||||
|
const {das} = await example();
|
||||||
|
const expectedGraph = Graph.merge(das.adapters().map((x) => x.graph()));
|
||||||
|
expect(das.graph().equals(expectedGraph)).toBe(true);
|
||||||
|
});
|
||||||
|
it("finds adapter matching a node", async () => {
|
||||||
|
const {x, das} = await example();
|
||||||
|
const matching = das.adapterMatchingNode(
|
||||||
|
NodeAddress.fromParts(["other", "foo"])
|
||||||
|
);
|
||||||
|
expect(matching.static().name()).toBe(x.name());
|
||||||
|
});
|
||||||
|
it("finds adapter matching an edge", async () => {
|
||||||
|
const {x, das} = await example();
|
||||||
|
const matching = das.adapterMatchingEdge(
|
||||||
|
EdgeAddress.fromParts(["other", "foo"])
|
||||||
|
);
|
||||||
|
expect(matching.static().name()).toBe(x.name());
|
||||||
|
});
|
||||||
|
it("finds fallback adapter for unregistered node", async () => {
|
||||||
|
const {das} = await example();
|
||||||
|
const adapter = das.adapterMatchingNode(NodeAddress.fromParts(["weird"]));
|
||||||
|
expect(adapter.static().name()).toBe(FALLBACK_NAME);
|
||||||
|
});
|
||||||
|
it("finds fallback adapter for unregistered edge", async () => {
|
||||||
|
const {das} = await example();
|
||||||
|
const adapter = das.adapterMatchingEdge(EdgeAddress.fromParts(["weird"]));
|
||||||
|
expect(adapter.static().name()).toBe(FALLBACK_NAME);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
61
src/app/adapters/fallbackAdapter.js
Normal file
61
src/app/adapters/fallbackAdapter.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
Graph,
|
||||||
|
NodeAddress,
|
||||||
|
type NodeAddressT,
|
||||||
|
EdgeAddress,
|
||||||
|
} from "../../core/graph";
|
||||||
|
import type {Repo} from "../../core/repo";
|
||||||
|
|
||||||
|
import type {StaticPluginAdapter, DynamicPluginAdapter} from "./pluginAdapter";
|
||||||
|
|
||||||
|
export const FALLBACK_NAME = "FALLBACK_ADAPTER";
|
||||||
|
|
||||||
|
export class FallbackStaticAdapter implements StaticPluginAdapter {
|
||||||
|
name() {
|
||||||
|
return FALLBACK_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodePrefix() {
|
||||||
|
return NodeAddress.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
edgePrefix() {
|
||||||
|
return EdgeAddress.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeTypes() {
|
||||||
|
return [
|
||||||
|
{name: "(unknown node)", prefix: NodeAddress.empty, defaultWeight: 1},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeTypes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
forwardName: "(unknown edge→)",
|
||||||
|
backwardName: "(unknown edge←)",
|
||||||
|
prefix: EdgeAddress.empty,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
load(_unused_repo: Repo) {
|
||||||
|
return Promise.resolve(new FallbackDynamicAdapter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FallbackDynamicAdapter implements DynamicPluginAdapter {
|
||||||
|
graph() {
|
||||||
|
return new Graph();
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeDescription(x: NodeAddressT) {
|
||||||
|
return NodeAddress.toString(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static() {
|
||||||
|
return new FallbackStaticAdapter();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user