Skip to content

Commit ef67eed

Browse files
authored
KitBuilder.wrap so that we can call any JS library (#191)
With this we are able to import any JS lib and then make nodes out of them. + Maps input names to argument positions in the function + Maps output to `result` if Number, String, Array + Maps output to Node Output names if an Object.
1 parent 7d900b2 commit ef67eed

File tree

4 files changed

+317
-0
lines changed

4 files changed

+317
-0
lines changed

seeds/breadboard/.eslintrc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"rules": {
3+
"@typescript-eslint/ban-types": ["warn", {
4+
"types": {
5+
"Function": false
6+
},
7+
"extendDefaults": true
8+
}]
9+
}
10+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Wrapping a library as a Kit
2+
3+
If you aren't familiar with the concept of Breadboard Kits, please read the [Kits](kits.md) guide first.
4+
5+
## Why wrap a library as a Kit?
6+
7+
Breadboard Kits are a great way to share functionality with other Breadboard users. The `KitBuilder` class is a great tool for encapsulating your own logic in to something that can be imported in to your Breadboards.
8+
9+
Sometimes though you might want to import a library directly from `npm` and not have to worry about creating custom `InputValues` and `OutputValues` and then handling the mapping of those values to the library's API.
10+
11+
`KitBuilder.wrap` solves this for you. It allows you to use any functions from any library in your project as a Breadboard Kit and returns a strongly typed `Kit` that you can add in to your Breadboard.
12+
13+
## How to wrap a library
14+
15+
We are going to create a little example that validates a JSON object against a JSON schema. We will use the [jsonschema](https://www.npmjs.com/package/jsonschema) library from `npm`.
16+
17+
### Step 1: Create a board
18+
19+
```TypeScript
20+
const board = new Board({
21+
title: "Test Echo",
22+
description: "Test Breadboard Kit",
23+
version: "0.0.1",
24+
});
25+
```
26+
27+
### Step 2: Import the library
28+
29+
```TypeScript
30+
const js = await import("jsonschema");
31+
```
32+
33+
### Step 3: Wrap the library and add it to the board
34+
35+
```TypeScript
36+
const MyKit = KitBuilder.wrap({ url: "test" }, { ...js.default });
37+
// The validate method is the only method that will be exposed from the library (it's the only function, the other properties are just Classes)
38+
39+
const myKit = board.addKit(MyKit);
40+
```
41+
42+
### Step 4: Use the library in a board
43+
44+
We are going to use the `validate` function. The `validate` function takes 3 arguments: 'instance', 'schema', and 'options'. We will use 3 `input` nodes to supply these arguments.
45+
46+
```TypeScript
47+
48+
const inputA = board.input();
49+
const inputB = board.input();
50+
const inputC = board.input();
51+
52+
const validateNode = myKit.validate();
53+
54+
inputA.wire("a->instance", validateNode);
55+
inputB.wire("b->schema", validateNode);
56+
inputC.wire("c->options", validateNode);
57+
```
58+
59+
Now that we have the inputs wired up, we can wire the wire the `validateNode` to the output of the board.
60+
61+
In this case, we need to look to see if there are any values on the `errors` value.
62+
63+
64+
65+
```TypeScript
66+
// result because it's just a string from a dynamic function
67+
validateNode.wire("errors->", board.output());
68+
```
69+
70+
Finally, we run the board with the parameters for the inputs, and wait until there is one output.
71+
72+
```TypeScript
73+
const output = await board.runOnce({
74+
"a": { "hello": "world" },
75+
"b": { "type": "object" },
76+
"c": { allowUnknownAttributes: true }
77+
});
78+
79+
console.log(output); // { errors: [] } ! YAY.
80+
```
81+
82+
Yay. We have successfully wrapped a library as a Breadboard Kit.

seeds/breadboard/src/kits/builder.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Kit,
1212
KitConstructor,
1313
NodeFactory,
14+
NodeHandler,
1415
NodeHandlers,
1516
OutputValues,
1617
} from "../types.js";
@@ -23,6 +24,10 @@ export type KitBuilderOptions = {
2324
namespacePrefix?: string;
2425
};
2526

27+
/* eslint-disable @typescript-eslint/no-explicit-any */
28+
type FunctionsKeysOnly<T> = ({ [P in keyof T]: T[P] extends (...args: any[]) => void ? P : never })[keyof T];
29+
type FunctionsOnly<T> = Pick<T, FunctionsKeysOnly<T>>;
30+
2631
export class KitBuilder {
2732
url: string;
2833
title?: string;
@@ -98,4 +103,43 @@ export class KitBuilder {
98103
}
99104
} as KitConstructor<GenericKit<Handlers>>;
100105
}
106+
107+
static wrap<F extends Record<string, Function>>(params: KitBuilderOptions, functions: F): KitConstructor<GenericKit<{ [x in keyof FunctionsOnly<F>]: NodeHandler }>> {
108+
109+
const createHandler = (previous: NodeHandlers, current: [string, Function]) => {
110+
const [name, fn] = current;
111+
112+
previous[name] = {
113+
invoke: async (inputs: InputValues) => {
114+
const argNames = fn.toString().match(/\((.*?)\)/)?.[1].split(",") ?? [];
115+
116+
// Validate the input names.
117+
for (const argName of argNames) {
118+
if (argName.trim() in inputs === false) { // Maybe we should use hasOwnProperty here?
119+
throw new Error(`Missing input: ${argName.trim()}. Valid inputs are: ${Object.keys(inputs).join(", ")}`);
120+
}
121+
}
122+
123+
const args = argNames.map((argName: string) => inputs[argName.trim()]);
124+
125+
const results = await fn(...args);
126+
127+
if (typeof results !== "object" || Array.isArray(results)) {
128+
// Number, Boolean, Array, String, will output to `result`.
129+
return { result: results };
130+
}
131+
132+
// Objects will destructured into the output.
133+
return { ...results };
134+
}
135+
};
136+
return previous;
137+
}
138+
139+
const handlers = Object.entries(functions).reduce<NodeHandlers>(createHandler, {});
140+
141+
const builder = new KitBuilder(params);
142+
143+
return builder.build(handlers) as KitConstructor<GenericKit<{ [x in keyof FunctionsOnly<F>]: NodeHandler }>>;
144+
}
101145
}

seeds/breadboard/tests/kits.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import test from "ava";
2+
import { KitBuilder } from "../src/kits/builder.js";
3+
import { Board } from "../src/board.js";
4+
5+
test("KitBuilder can wrap a function", async (t) => {
6+
7+
// A normal function that will be wrapped.
8+
const echo = (input: string) => input;
9+
const test = (input: string) => input;
10+
11+
const MyKit = KitBuilder.wrap({ url: "test" }, { echo, test });
12+
13+
const board = new Board({
14+
title: "Test Echo",
15+
description: "Test Breadboard Kit",
16+
version: "0.0.1",
17+
});
18+
19+
const myKit = board.addKit(MyKit);
20+
21+
t.true(myKit.echo instanceof Function);
22+
t.true(myKit.test instanceof Function);
23+
});
24+
25+
26+
test("KitBuilder can call a function that returns a string", async (t) => {
27+
28+
// A normal function that will be wrapped.
29+
const echo = (echo_this: string) => echo_this;
30+
31+
const MyKit = KitBuilder.wrap({ url: "test" }, { echo });
32+
33+
const board = new Board({
34+
title: "Test Echo",
35+
description: "Test Breadboard Kit",
36+
version: "0.0.1",
37+
});
38+
39+
const myKit = board.addKit(MyKit);
40+
41+
const input = board.input();
42+
const echoNode = myKit.echo();
43+
44+
input.wire("an_input->echo_this", echoNode);
45+
// result because it's just a string from a dynamic function
46+
echoNode.wire("result->an_output", board.output());
47+
48+
const output = await board.runOnce({
49+
"an_input": "hello world"
50+
});
51+
52+
t.is((<string>output["an_output"]), "hello world");
53+
54+
});
55+
56+
test("KitBuilder can call a function that returns an object", async (t) => {
57+
58+
// A normal function that will be wrapped.
59+
const echo = (echo_this: string) => {
60+
return { "out": echo_this, "other": "stuff" }
61+
};
62+
63+
const MyKit = KitBuilder.wrap({ url: "test" }, { echo });
64+
65+
const board = new Board({
66+
title: "Test Echo",
67+
description: "Test Breadboard Kit",
68+
version: "0.0.1",
69+
});
70+
71+
const myKit = board.addKit(MyKit);
72+
73+
const input = board.input();
74+
const echoNode = myKit.echo();
75+
76+
input.wire("an_input->echo_this", echoNode);
77+
// result because it's just a string from a dynamic function
78+
echoNode.wire("out->an_output", board.output());
79+
80+
const output = await board.runOnce({
81+
"an_input": "hello world"
82+
});
83+
84+
t.is((<string>output["an_output"]), "hello world");
85+
86+
});
87+
88+
test("KitBuilder can call a function that has more than one input", async (t) => {
89+
90+
// A normal function that will be wrapped.
91+
const add = (a: number, b: number) => {
92+
return a + b;
93+
};
94+
95+
const MyKit = KitBuilder.wrap({ url: "test" }, { add });
96+
97+
const board = new Board({
98+
title: "Test Echo",
99+
description: "Test Breadboard Kit",
100+
version: "0.0.1",
101+
});
102+
103+
const myKit = board.addKit(MyKit);
104+
105+
const inputA = board.input();
106+
const inputB = board.input();
107+
108+
const addNode = myKit.add();
109+
110+
inputA.wire("a->a", addNode);
111+
inputB.wire("b->b", addNode);
112+
// result because it's just a string from a dynamic function
113+
addNode.wire("result->", board.output());
114+
115+
const output = await board.runOnce({
116+
"a": 1,
117+
"b": 2
118+
});
119+
120+
t.is((<number>output["result"]), 3);
121+
});
122+
123+
test("KitBuilder can call a function from an external import", async (t) => {
124+
125+
const js = await import("jsonschema");
126+
127+
// Wrap the jsonschema validate function in a kit and expose function as a node.
128+
const MyKit = KitBuilder.wrap({ url: "test" }, { validate: js.default.validate });
129+
130+
const board = new Board({
131+
title: "Test Echo",
132+
description: "Test Breadboard Kit",
133+
version: "0.0.1",
134+
});
135+
136+
const myKit = board.addKit(MyKit);
137+
138+
const inputA = board.input();
139+
const inputB = board.input();
140+
const inputC = board.input();
141+
142+
const validateNode = myKit.validate();
143+
144+
inputA.wire("a->instance", validateNode);
145+
inputB.wire("b->schema", validateNode);
146+
inputC.wire("c->options", validateNode);
147+
148+
// result because it's just a string from a dynamic function
149+
validateNode.wire("errors->", board.output());
150+
151+
const output = await board.runOnce({
152+
"a": { "hello": "world" },
153+
"b": { "type": "object" },
154+
"c": { allowUnknownAttributes: true }
155+
});
156+
157+
const result = js.default.validate({ "hello": "world" }, { "type": "object" }, { allowUnknownAttributes: true });
158+
159+
t.is(((<Array<string>>output["errors"]).length), result.errors.length);
160+
});
161+
162+
test("KitBuilder can splat all the functions in the extenral library and make nodes", async (t) => {
163+
164+
const js = await import("jsonschema");
165+
166+
// Wrap the jsonschema validate function in a kit and expose function as a node.
167+
const MyKit = KitBuilder.wrap({ url: "test" }, { ...js.default });
168+
169+
const board = new Board({
170+
title: "Test Echo",
171+
description: "Test Breadboard Kit",
172+
version: "0.0.1",
173+
});
174+
175+
const myKit = board.addKit(MyKit);
176+
177+
myKit.validate()
178+
179+
// We really need to pick a library with more than one function.
180+
t.true(myKit.validate instanceof Function);
181+
});

0 commit comments

Comments
 (0)