Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create External Plugin that reads from disk or url #3114

Merged
merged 4 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`analysis/pluginDeclaration to/fromJSON snapshots on an empty declaration 1`] = `"[{\\"type\\":\\"sourcecred/pluginDeclarations\\",\\"version\\":\\"0.1.0\\"},[{\\"edgePrefix\\":\\"E\\\\u0000\\",\\"edgeTypes\\":[],\\"name\\":\\"empty\\",\\"nodePrefix\\":\\"N\\\\u0000\\",\\"nodeTypes\\":[],\\"userTypes\\":[]}]]"`;
exports[`analysis/pluginDeclaration to/fromJSON snapshots on an empty declaration 1`] = `"[[{\\"type\\":\\"sourcecred/pluginDeclarations\\",\\"version\\":\\"0.1.0\\"},{\\"edgePrefix\\":\\"E\\\\u0000\\",\\"edgeTypes\\":[],\\"name\\":\\"empty\\",\\"nodePrefix\\":\\"N\\\\u0000\\",\\"nodeTypes\\":[],\\"userTypes\\":[]}]]"`;

exports[`analysis/pluginDeclaration to/fromJSON snapshots on an non-empty declaration 1`] = `"[{\\"type\\":\\"sourcecred/pluginDeclarations\\",\\"version\\":\\"0.1.0\\"},[{\\"edgePrefix\\":\\"E\\\\u0000\\",\\"edgeTypes\\":[{\\"backwardName\\":\\"is pointed to\\",\\"defaultWeight\\":{\\"backwards\\":3,\\"forwards\\":2},\\"description\\":\\"a type\\",\\"forwardName\\":\\"points\\",\\"prefix\\":\\"E\\\\u0000edge\\\\u0000\\"}],\\"name\\":\\"non-empty\\",\\"nodePrefix\\":\\"N\\\\u0000\\",\\"nodeTypes\\":[{\\"defaultWeight\\":2,\\"description\\":\\"a type\\",\\"name\\":\\"node\\",\\"pluralName\\":\\"nodes\\",\\"prefix\\":\\"N\\\\u0000node\\\\u0000\\"}],\\"userTypes\\":[]}]]"`;
exports[`analysis/pluginDeclaration to/fromJSON snapshots on an non-empty declaration 1`] = `"[[{\\"type\\":\\"sourcecred/pluginDeclarations\\",\\"version\\":\\"0.1.0\\"},{\\"edgePrefix\\":\\"E\\\\u0000\\",\\"edgeTypes\\":[{\\"backwardName\\":\\"is pointed to\\",\\"defaultWeight\\":{\\"backwards\\":3,\\"forwards\\":2},\\"description\\":\\"a type\\",\\"forwardName\\":\\"points\\",\\"prefix\\":\\"E\\\\u0000edge\\\\u0000\\"}],\\"name\\":\\"non-empty\\",\\"nodePrefix\\":\\"N\\\\u0000\\",\\"nodeTypes\\":[{\\"defaultWeight\\":2,\\"description\\":\\"a type\\",\\"name\\":\\"node\\",\\"pluralName\\":\\"nodes\\",\\"prefix\\":\\"N\\\\u0000node\\\\u0000\\"}],\\"userTypes\\":[]}]]"`;
10 changes: 4 additions & 6 deletions packages/sourcecred/src/analysis/pluginDeclaration.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,15 @@ export type PluginDeclaration = {|
+userTypes: $ReadOnlyArray<NodeType>,
|};

export type PluginDeclarations = $ReadOnlyArray<PluginDeclaration>;

export function toJSON(decs: PluginDeclarations): PluginDeclarationsJSON {
return toCompat(COMPAT_INFO, decs);
export function toJSON(dec: PluginDeclaration): PluginDeclarationJSON {
return toCompat(COMPAT_INFO, dec);
}

export function fromJSON(json: PluginDeclarationsJSON): PluginDeclarations {
export function fromJSON(json: PluginDeclarationJSON): PluginDeclaration {
return fromCompat(COMPAT_INFO, json);
}

export type PluginDeclarationsJSON = Compatible<PluginDeclarations>;
export type PluginDeclarationJSON = Compatible<PluginDeclaration>;

export function combineTypes(
decs: $ReadOnlyArray<PluginDeclaration>
Expand Down
12 changes: 6 additions & 6 deletions packages/sourcecred/src/analysis/pluginDeclaration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,22 @@ describe("analysis/pluginDeclaration", () => {

describe("to/fromJSON", () => {
it("works round-trip on an empty declaration", () => {
const json = toJSON([emptyDeclaration]);
const json = toJSON(emptyDeclaration);
const result = fromJSON(json);
expect(result).toEqual([emptyDeclaration]);
expect(result).toEqual(emptyDeclaration);
});
it("snapshots on an empty declaration", () => {
// stringify to avoid having literal NUL bytes in our source.
expect(stringify(toJSON([emptyDeclaration]))).toMatchSnapshot();
expect(stringify([toJSON(emptyDeclaration)])).toMatchSnapshot();
});
it("works round-trip on an non-empty declaration", () => {
const json = toJSON([nonEmptyDeclaration]);
const json = toJSON(nonEmptyDeclaration);
const result = fromJSON(json);
expect(result).toEqual([nonEmptyDeclaration]);
expect(result).toEqual(nonEmptyDeclaration);
});
it("snapshots on an non-empty declaration", () => {
// stringify to avoid having literal NUL bytes in our source.
expect(stringify(toJSON([nonEmptyDeclaration]))).toMatchSnapshot();
expect(stringify([toJSON(nonEmptyDeclaration)])).toMatchSnapshot();
});
});
});
30 changes: 21 additions & 9 deletions packages/sourcecred/src/api/bundledDeclarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,39 @@ import {declaration as discordDeclaration} from "../plugins/discord/declaration"
import {declaration as initiativesDeclaration} from "../plugins/initiatives/declaration";
import {declaration as ethereumDeclaration} from "../plugins/ethereum/declaration";
import {type RawInstanceConfig} from "./rawInstanceConfig";
import {type PluginId} from "./pluginId";
import {type PluginId, getPluginOwner} from "./pluginId";
import {DataStorage} from "../core/storage";
import {
ExternalPlugin,
ExternalPluginIdOwner,
} from "../plugins/external/plugin";

export function bundledDeclarations(): {[pluginId: string]: PluginDeclaration} {
return {
export async function getPluginDeclaration(
pluginId: PluginId,
storage: DataStorage
): Promise<PluginDeclaration> {
const mapping = {
Comment on lines +17 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate this refactoring

"sourcecred/github": githubDeclaration,
"sourcecred/discourse": discourseDeclaration,
"sourcecred/discord": discordDeclaration,
"sourcecred/initiatives": initiativesDeclaration,
"sourcecred/ethereum": ethereumDeclaration,
};
if (mapping[pluginId.toString()]) return mapping[pluginId.toString()];
if (getPluginOwner(pluginId) === ExternalPluginIdOwner)
return await new ExternalPlugin({pluginId, storage}).declaration();
throw "Bad declaration: " + JSON.stringify(pluginId);
}

export function upgradeRawInstanceConfig(
raw: RawInstanceConfig
): $ReadOnlyMap<PluginId, PluginDeclaration> {
const allBundledPlugins = bundledDeclarations();
export async function upgradeRawInstanceConfig(
raw: RawInstanceConfig,
storage: DataStorage
): Promise<$ReadOnlyMap<PluginId, PluginDeclaration>> {
const bundledPlugins = new Map();
for (const id of raw.bundledPlugins) {
const plugin = allBundledPlugins[id];
const plugin = await getPluginDeclaration(id, storage);
if (plugin == null) {
throw new Error("bad bundled plugin: " + JSON.stringify(id));
throw new Error("bad bundled declaration: " + JSON.stringify(id));
}
bundledPlugins.set(id, plugin);
}
Expand Down
17 changes: 15 additions & 2 deletions packages/sourcecred/src/api/bundledPlugins.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
// @flow

import type {Plugin} from "./plugin";
import {type PluginId, getPluginOwner} from "./pluginId";
import {GithubPlugin} from "../plugins/github/plugin";
import {DiscoursePlugin} from "../plugins/discourse/plugin";
import {DiscordPlugin} from "../plugins/discord/plugin";
import {InitiativesPlugin} from "../plugins/initiatives/plugin";
import {EthereumPlugin} from "../plugins/ethereum/plugin";
import {
ExternalPlugin,
ExternalPluginIdOwner,
} from "../plugins/external/plugin";
import {DiskStorage} from "../core/storage/disk";

/**
* Returns an object mapping owner-name pairs to CLI plugin
* declarations; keys are like `sourcecred/github`.
*/
// TODO(@decentralion): Fix the type signature here.
export function bundledPlugins(): {[pluginId: string]: Plugin} {
return {
export function getPlugin(pluginId: PluginId): ?Plugin {
const mapping = {
"sourcecred/github": new GithubPlugin(),
"sourcecred/discourse": new DiscoursePlugin(),
"sourcecred/discord": new DiscordPlugin(),
"sourcecred/initiatives": new InitiativesPlugin(),
"sourcecred/ethereum": new EthereumPlugin(),
};
if (mapping[pluginId.toString()]) return mapping[pluginId.toString()];
if (getPluginOwner(pluginId) === ExternalPluginIdOwner)
return new ExternalPlugin({
pluginId,
storage: new DiskStorage(process.cwd()),
});
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious: why not an implicit undefined return here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will change, probably just a years-old habit from the 1 year in college I spent writing java lol.

}
8 changes: 6 additions & 2 deletions packages/sourcecred/src/api/instance/readInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import {
type DependenciesConfig,
} from "../dependenciesConfig";
import {type Budget} from "../../core/mintBudget";
import {parser as pluginBudgetParser} from "../pluginBudgetConfig";
import {
rawParser as pluginBudgetParser,
upgrade as pluginBudgetUpgrader,
} from "../pluginBudgetConfig";
import {
rawParser as rawConfigParser,
type RawInstanceConfig,
Expand Down Expand Up @@ -261,12 +264,13 @@ export class ReadInstance implements ReadOnlyInstance {

async readPluginsBudget(): Promise<Budget | null> {
const budgetPath = pathJoin(...BUDGET_PATH);
return loadJsonWithDefault(
const raw = await loadJsonWithDefault(
this._storage,
budgetPath,
pluginBudgetParser,
() => null
);
return raw ? await pluginBudgetUpgrader(raw, this._storage) : null;
}

createPluginDirectory(
Expand Down
5 changes: 2 additions & 3 deletions packages/sourcecred/src/api/instanceConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import * as P from "../util/combo";
import {Plugin} from "./plugin";
import {bundledPlugins as getAllBundledPlugins} from "./bundledPlugins";
import {getPlugin} from "./bundledPlugins";
import {rawParser, type RawInstanceConfig} from "./rawInstanceConfig";
import * as pluginId from "./pluginId";

Expand All @@ -11,10 +11,9 @@ export type InstanceConfig = {|
|};

function upgrade(raw: RawInstanceConfig): InstanceConfig {
const allBundledPlugins = getAllBundledPlugins();
const bundledPlugins = new Map();
for (const id of raw.bundledPlugins) {
const plugin = allBundledPlugins[id];
const plugin = getPlugin(id);
if (plugin == null) {
throw new Error("bad bundled plugin: " + JSON.stringify(id));
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sourcecred/src/api/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {IdentityProposal} from "../core/ledger/identityProposal";

export interface Plugin {
+id: PluginId;
declaration(): PluginDeclaration;
declaration(): Promise<PluginDeclaration>;
load(PluginDirectoryContext, TaskReporter): Promise<void>;
graph(
PluginDirectoryContext,
Expand Down
34 changes: 19 additions & 15 deletions packages/sourcecred/src/api/pluginBudgetConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
timestampISOParser,
} from "../util/timestamp";
import * as C from "../util/combo";
import {bundledDeclarations} from "./bundledDeclarations";
import {getPluginDeclaration} from "./bundledDeclarations";
import {DataStorage} from "../core/storage";

/**
* This module contains logic for setting Cred minting budgets over time on a per-plugin basis.
Expand Down Expand Up @@ -55,7 +56,7 @@ const rawPeriodParser: C.Parser<RawBudgetPeriod> = C.object({
budget: C.number,
});

const rawParser: C.Parser<RawPluginBudgetConfig> = C.object({
export const rawParser: C.Parser<RawPluginBudgetConfig> = C.object({
intervalLength: intervalLengthParser,
plugins: C.dict(C.array(rawPeriodParser), pluginIdParser),
});
Expand All @@ -64,18 +65,21 @@ function upgradeRawPeriod(p: RawBudgetPeriod): BudgetPeriod {
return {budgetValue: p.budget, startTimeMs: fromISO(p.startTime)};
}

function upgrade(config: RawPluginBudgetConfig): Budget {
const entries = Object.keys(config.plugins).map((key) => {
const id = pluginIdFromString(key);
const declaration = bundledDeclarations()[id];
if (id == null) {
throw new Error(`No available plugin with id ${id}`);
}
const prefix = declaration.nodePrefix;
const periods = config.plugins[id].map(upgradeRawPeriod);
return {prefix, periods};
});
export async function upgrade(
config: RawPluginBudgetConfig,
storage: DataStorage
): Promise<Budget> {
const entries = await Promise.all(
Object.keys(config.plugins).map(async (key) => {
const id = pluginIdFromString(key);
const declaration = await getPluginDeclaration(id, storage);
if (id == null) {
throw new Error(`No available plugin with id ${id}`);
}
const prefix = declaration.nodePrefix;
const periods = config.plugins[id].map(upgradeRawPeriod);
return {prefix, periods};
})
);
return {entries, intervalLength: config.intervalLength};
}

export const parser: C.Parser<Budget> = C.fmap(rawParser, upgrade);
8 changes: 8 additions & 0 deletions packages/sourcecred/src/api/pluginId.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ export function fromString(s: string): PluginId {
return s;
}

export function getPluginOwner(id: PluginId): string {
return id.split("/")[0];
}

export function getPluginName(id: PluginId): string {
return id.split("/")[1];
}

export const parser: P.Parser<PluginId> = P.fmap(P.string, fromString);
4 changes: 4 additions & 0 deletions packages/sourcecred/src/core/ledger/identityProposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const parser: C.Parser<IdentityProposal> = C.object({
type: identityTypeParser,
});

export const identityProposalsParser: C.Parser<
$ReadOnlyArray<IdentityProposal>
> = C.array(parser);

/**
* Given a Ledger and an IdentityProposal, ensure that some Ledger account
* exists for the proposed identity and return the identity ID.
Expand Down
2 changes: 1 addition & 1 deletion packages/sourcecred/src/plugins/discord/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function getTokenFromEnv(): DiscordToken {
export class DiscordPlugin implements Plugin {
id: PluginId = pluginIdFromString("sourcecred/discord");

declaration(): PluginDeclaration {
async declaration(): Promise<PluginDeclaration> {
return declaration;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/sourcecred/src/plugins/discourse/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async function repository(
export class DiscoursePlugin implements Plugin {
id: PluginId = pluginIdFromString("sourcecred/discourse");

declaration(): PluginDeclaration {
async declaration(): Promise<PluginDeclaration> {
return declaration;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/sourcecred/src/plugins/ethereum/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function loadEthJson(ctx: PluginDirectoryContext) {
export class EthereumPlugin implements Plugin {
id: PluginId = pluginIdFromString("sourcecred/ethereum");

declaration(): PluginDeclaration {
async declaration(): Promise<PluginDeclaration> {
return declaration;
}

Expand Down
75 changes: 75 additions & 0 deletions packages/sourcecred/src/plugins/external/defaultDeclaration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// @flow

import deepFreeze from "deep-freeze";
import type {PluginDeclaration} from "../../analysis/pluginDeclaration";
import {type PluginId, getPluginName, getPluginOwner} from "../../api/pluginId";
import {NodeAddress, EdgeAddress, type NodeAddressT} from "../../core/graph";

export function contributionNodeType(
id: PluginId
): {|
+defaultWeight: number,
+description: string,
+name: string,
+pluralName: string,
+prefix: NodeAddressT,
|} {
return deepFreeze({
name: "Contribution",
pluralName: "Contributions",
prefix: NodeAddress.fromParts([
getPluginOwner(id),
getPluginName(id),
"CONTRIBUTION",
]),
defaultWeight: 1,
description: "NodeType for a generic contribution",
});
}

export function participantNodeType(
id: PluginId
): {|
+defaultWeight: number,
+description: string,
+name: string,
+pluralName: string,
+prefix: NodeAddressT,
|} {
return deepFreeze({
name: "Participant",
pluralName: "Participant",
prefix: NodeAddress.fromParts([
getPluginOwner(id),
getPluginName(id),
"PARTICIPANT",
]),
defaultWeight: 1,
description: "NodeType for a generic participant",
});
}

function participatedInEdgeType(id: PluginId) {
return deepFreeze({
forwardName: "participated in",
backwardName: "had participation from",
defaultWeight: {forwards: 1 / 2, backwards: 1},
prefix: EdgeAddress.fromParts([
getPluginOwner(id),
getPluginName(id),
"PARTICIPATES_IN",
]),
description: "NodeType for a generic participant-contribution relationship",
});
}

export function declaration(id: PluginId): PluginDeclaration {
return deepFreeze({
name: getPluginName(id),
nodePrefix: NodeAddress.fromParts([getPluginOwner(id), getPluginName(id)]),
edgePrefix: EdgeAddress.fromParts([getPluginOwner(id), getPluginName(id)]),
nodeTypes: [participantNodeType(id), contributionNodeType(id)],
edgeTypes: [participatedInEdgeType(id)],
userTypes: [participantNodeType(id)],
});
}
Loading