Skip to content

Commit fc50fd8

Browse files
committed
Implements sb.jinja helper
1 parent 9d11744 commit fc50fd8

File tree

6 files changed

+167
-24
lines changed

6 files changed

+167
-24
lines changed

examples/jinja.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env -S npx ts-node --transpileOnly
2+
3+
import { Substrate, Box, sb } from "substrate";
4+
5+
async function main() {
6+
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7+
8+
const substrate = new Substrate({
9+
apiKey: SUBSTRATE_API_KEY,
10+
baseUrl: "https://api-staging.substrate.run",
11+
});
12+
13+
const a = new Box({ value: ["a", "b"] });
14+
const b = new Box({ value: { x: "x" } });
15+
16+
const f = sb.jinja('as=[{% for a in as %}{{a}},{% endfor%}], b={{b["x"]}}, c={{c}}', {
17+
as: a.future.value,
18+
b: b.future.value,
19+
c: "1234",
20+
});
21+
22+
const c = new Box({ value: f });
23+
24+
const res = await substrate.run(a, b, c);
25+
console.log(JSON.stringify(res.json, null, 2));
26+
}
27+
main();

src/Future.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export class JQ extends Directive {
122122
rawValue: (val: JQCompatible) => ({ future_id: null, val }),
123123
};
124124

125-
override next(...items: TraceProp[]) {
125+
override next(..._items: TraceProp[]) {
126126
return new JQ(this.query, this.target);
127127
}
128128

@@ -189,6 +189,84 @@ export class StringConcat extends Directive {
189189
}
190190
}
191191

192+
type JinjaTemplate =
193+
| {
194+
future_id: string;
195+
val: null;
196+
}
197+
| { val: string; future_id: null };
198+
199+
export type JinjaVariables = {
200+
[key: string]:
201+
| string
202+
| number
203+
| boolean
204+
| (string | number | boolean)[]
205+
| JinjaVariables
206+
| Future<any>;
207+
};
208+
209+
export class Jinja extends Directive {
210+
template: string | Future<string>;
211+
variables: JinjaVariables;
212+
items: Future<any>[];
213+
214+
static templateJSON(template: string | Future<string>): JinjaTemplate {
215+
return template instanceof Future
216+
? // @ts-ignore
217+
{ val: null, future_id: template._id }
218+
: { val: template, future_id: null };
219+
}
220+
221+
constructor(template: string | Future<string>, variables: JinjaVariables) {
222+
super();
223+
this.template = template;
224+
this.variables = variables;
225+
226+
// use items to contain all of the futures from the inputs
227+
const futures = new Set<Future<any>>();
228+
const collectFutures = (obj: any) => {
229+
if (Array.isArray(obj)) {
230+
for (let item of obj) {
231+
collectFutures(item);
232+
}
233+
}
234+
235+
if (obj instanceof Future) {
236+
futures.add(obj);
237+
return;
238+
}
239+
240+
if (obj && typeof obj === "object") {
241+
for (let key of Object.keys(obj)) {
242+
collectFutures(obj[key]);
243+
}
244+
}
245+
};
246+
collectFutures([template, variables]);
247+
this.items = Array.from(futures)
248+
}
249+
250+
override next(..._items: any[]) {
251+
return new Jinja(this.template, this.variables);
252+
}
253+
254+
override async result(): Promise<string> {
255+
return this.template instanceof Future
256+
? // @ts-ignore
257+
await this.template._result()
258+
: this.template;
259+
}
260+
261+
override toJSON(): any {
262+
return {
263+
type: "jinja",
264+
template: Jinja.templateJSON(this.template),
265+
variables: replaceWithPlaceholders(this.variables),
266+
};
267+
}
268+
}
269+
192270
export abstract class Future<T> {
193271
protected _directive: Directive;
194272
protected _id: string = "";
@@ -263,6 +341,13 @@ export class FutureString extends Future<string> {
263341
return FutureString.concat(...[this, ...items]);
264342
}
265343

344+
static jinja(
345+
template: string | FutureString,
346+
variables: JinjaVariables,
347+
): FutureString {
348+
return new FutureString(new Jinja(template, variables));
349+
}
350+
266351
protected override async _result(): Promise<string> {
267352
return super._result();
268353
}
@@ -315,3 +400,27 @@ export class FutureAnyObject extends Future<Object> {
315400
return super._result();
316401
}
317402
}
403+
404+
/**
405+
* @internal
406+
* Given some value, recursively replace `Future` instances with SB Placeholder
407+
*/
408+
export const replaceWithPlaceholders = (val: any): any => {
409+
if (Array.isArray(val)) {
410+
return val.map((item) => replaceWithPlaceholders(item));
411+
}
412+
413+
if (val instanceof Future) {
414+
// @ts-expect-error (accessing protected method toPlaceholder)
415+
return val.toPlaceholder();
416+
}
417+
418+
if (val && typeof val === "object") {
419+
return Object.keys(val).reduce((acc: any, k: any) => {
420+
acc[k] = replaceWithPlaceholders(val[k]);
421+
return acc;
422+
}, {});
423+
}
424+
425+
return val;
426+
};

src/Node.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { idGenerator } from "substrate/idGenerator";
2-
import { Future, FutureAnyObject, Trace } from "substrate/Future";
2+
import { Future, FutureAnyObject, Trace, replaceWithPlaceholders } from "substrate/Future";
33
import { SubstrateResponse } from "substrate/SubstrateResponse";
44
import { NodeError, SubstrateError } from "substrate/Error";
55
import { AnyNode } from "substrate/Nodes";
@@ -102,30 +102,10 @@ export abstract class Node {
102102
}
103103

104104
toJSON() {
105-
const withPlaceholders = (obj: any): any => {
106-
if (Array.isArray(obj)) {
107-
return obj.map((item) => withPlaceholders(item));
108-
}
109-
110-
if (obj instanceof Future) {
111-
// @ts-expect-error (accessing protected method toPlaceholder)
112-
return obj.toPlaceholder();
113-
}
114-
115-
if (obj && typeof obj === "object") {
116-
return Object.keys(obj).reduce((acc: any, k: any) => {
117-
acc[k] = withPlaceholders(obj[k]);
118-
return acc;
119-
}, {});
120-
}
121-
122-
return obj;
123-
};
124-
125105
return {
126106
id: this.id,
127107
node: this.node,
128-
args: withPlaceholders(this.args),
108+
args: replaceWithPlaceholders(this.args),
129109
_should_output_globally: !this.hide,
130110
...(this.cache_age && { _cache_age: this.cache_age }),
131111
...(this.cache_keys && { _cache_keys: this.cache_keys }),

src/Substrate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class Substrate {
112112
* @throws {Error} when the client encounters an error making the request.
113113
*/
114114
async runSerialized(
115-
nodes: Node[],
115+
nodes: Node[] = [],
116116
endpoint: string = "/compose",
117117
): Promise<SubstrateResponse> {
118118
const serialized = Substrate.serialize(...nodes);

src/sb.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const sb = {
55
concat: FutureString.concat,
66
jq: FutureAnyObject.jq,
77
interpolate: FutureString.interpolate,
8+
jinja: FutureString.jinja,
89
streaming: {
910
fromSSEResponse: StreamingResponse.fromReponse,
1011
},

tests/Future.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,5 +205,31 @@ describe("Future", () => {
205205
// @ts-expect-error
206206
expect(i2._result()).resolves.toEqual("hello12");
207207
});
208+
209+
describe(".jinja", () => {
210+
test(".toJSON", () => {
211+
const x = FutureString.concat("1", "2", "3");
212+
const f = FutureString.jinja("template: x={{x}} y={{y}}", {
213+
x,
214+
y: "abc",
215+
});
216+
217+
const json = f.toJSON();
218+
219+
expect(json).toEqual({
220+
// @ts-ignore
221+
id: f._id,
222+
directive: {
223+
type: "jinja",
224+
template: { future_id: null, val: "template: x={{x}} y={{y}}" },
225+
variables: {
226+
// @ts-ignore (_id)
227+
x: { __$$SB_GRAPH_OP_ID$$__: x._id },
228+
y: "abc",
229+
},
230+
},
231+
});
232+
});
233+
});
208234
});
209235
});

0 commit comments

Comments
 (0)