diff --git a/examples/jinja.ts b/examples/jinja.ts new file mode 100755 index 0000000..5fd78dc --- /dev/null +++ b/examples/jinja.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env -S npx ts-node --transpileOnly + +import { Substrate, Box, sb } from "substrate"; + +async function main() { + const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"]; + + const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY }); + + const a = new Box({ value: ["a", "b"] }); + const b = new Box({ value: { x: "x" } }); + + const f = sb.jinja( + 'as=[{% for a in as %}{{a}},{% endfor%}], b={{b["x"]}}, c={{c}}', + { + as: a.future.value, + b: b.future.value, + c: "1234", + }, + ); + + const c = new Box({ value: f }); + + const res = await substrate.run(a, b, c); + console.log(JSON.stringify(res.json, null, 2)); +} +main(); diff --git a/src/EventSource.ts b/src/EventSource.ts index fa5677d..941399a 100644 --- a/src/EventSource.ts +++ b/src/EventSource.ts @@ -13,7 +13,7 @@ * Creates a new EventSource parser. * @example * - * // create a parser and read start reading a string + * // create a parser and start reading a string * let parser = createParser() * for (const message of parser.getMessages(str)) { * // ... @@ -52,7 +52,7 @@ export function createParser() { buffer = buffer ? buffer + chunk : chunk; // Strip any UTF8 byte order mark (BOM) at the start of the stream. - // Note that we do not strip any non - UTF8 BOM, as eventsource streams are + // Note that we do not strip any non-UTF8 BOM, as EventSource streams are // always decoded as UTF8 as per the specification. if (isFirstChunk && hasBom(buffer)) { buffer = buffer.slice(BOM.length); @@ -121,11 +121,10 @@ export function createParser() { } if (position === length) { - // If we consumed the entire buffer to read the event, reset the buffer + // If we consumed the entire buffer, reset the buffer buffer = ""; } else if (position > 0) { - // If there are bytes left to process, set the buffer to the unprocessed - // portion of the buffer only + // If there are bytes left to process, set the buffer to the unprocessed portion only buffer = buffer.slice(position); } } @@ -137,7 +136,7 @@ export function createParser() { lineLength: number, ) { if (lineLength === 0) { - // We reached the last line of this event + // We reached the end of this event if (data.length > 0) { yield { type: "event", diff --git a/src/Future.ts b/src/Future.ts index 75559a5..c1981dc 100644 --- a/src/Future.ts +++ b/src/Future.ts @@ -122,7 +122,7 @@ export class JQ extends Directive { rawValue: (val: JQCompatible) => ({ future_id: null, val }), }; - override next(...items: TraceProp[]) { + override next(..._items: TraceProp[]) { return new JQ(this.query, this.target); } @@ -189,6 +189,84 @@ export class StringConcat extends Directive { } } +type JinjaTemplate = + | { + future_id: string; + val: null; + } + | { val: string; future_id: null }; + +export type JinjaVariables = { + [key: string]: + | string + | number + | boolean + | (string | number | boolean)[] + | JinjaVariables + | Future; +}; + +export class Jinja extends Directive { + template: string | Future; + variables: JinjaVariables; + items: Future[]; + + static templateJSON(template: string | Future): JinjaTemplate { + return template instanceof Future + ? // @ts-ignore + { val: null, future_id: template._id } + : { val: template, future_id: null }; + } + + constructor(template: string | Future, variables: JinjaVariables) { + super(); + this.template = template; + this.variables = variables; + + // use items to contain all of the futures from the inputs + const futures = new Set>(); + const collectFutures = (obj: any) => { + if (Array.isArray(obj)) { + for (let item of obj) { + collectFutures(item); + } + } + + if (obj instanceof Future) { + futures.add(obj); + return; + } + + if (obj && typeof obj === "object") { + for (let key of Object.keys(obj)) { + collectFutures(obj[key]); + } + } + }; + collectFutures([template, variables]); + this.items = Array.from(futures); + } + + override next(..._items: any[]) { + return new Jinja(this.template, this.variables); + } + + override async result(): Promise { + return this.template instanceof Future + ? // @ts-ignore + await this.template._result() + : this.template; + } + + override toJSON(): any { + return { + type: "jinja", + template: Jinja.templateJSON(this.template), + variables: replaceWithPlaceholders(this.variables), + }; + } +} + export abstract class Future { protected _directive: Directive; protected _id: string = ""; @@ -263,6 +341,13 @@ export class FutureString extends Future { return FutureString.concat(...[this, ...items]); } + static jinja( + template: string | FutureString, + variables: JinjaVariables, + ): FutureString { + return new FutureString(new Jinja(template, variables)); + } + protected override async _result(): Promise { return super._result(); } @@ -315,3 +400,27 @@ export class FutureAnyObject extends Future { return super._result(); } } + +/** + * @internal + * Given some value, recursively replace `Future` instances with SB Placeholder + */ +export const replaceWithPlaceholders = (val: any): any => { + if (Array.isArray(val)) { + return val.map((item) => replaceWithPlaceholders(item)); + } + + if (val instanceof Future) { + // @ts-expect-error (accessing protected method toPlaceholder) + return val.toPlaceholder(); + } + + if (val && typeof val === "object") { + return Object.keys(val).reduce((acc: any, k: any) => { + acc[k] = replaceWithPlaceholders(val[k]); + return acc; + }, {}); + } + + return val; +}; diff --git a/src/Node.ts b/src/Node.ts index e642348..7b4ba60 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -1,5 +1,10 @@ import { idGenerator } from "substrate/idGenerator"; -import { Future, FutureAnyObject, Trace } from "substrate/Future"; +import { + Future, + FutureAnyObject, + Trace, + replaceWithPlaceholders, +} from "substrate/Future"; import { SubstrateResponse } from "substrate/SubstrateResponse"; import { NodeError, SubstrateError } from "substrate/Error"; import { AnyNode } from "substrate/Nodes"; @@ -102,30 +107,10 @@ export abstract class Node { } toJSON() { - const withPlaceholders = (obj: any): any => { - if (Array.isArray(obj)) { - return obj.map((item) => withPlaceholders(item)); - } - - if (obj instanceof Future) { - // @ts-expect-error (accessing protected method toPlaceholder) - return obj.toPlaceholder(); - } - - if (obj && typeof obj === "object") { - return Object.keys(obj).reduce((acc: any, k: any) => { - acc[k] = withPlaceholders(obj[k]); - return acc; - }, {}); - } - - return obj; - }; - return { id: this.id, node: this.node, - args: withPlaceholders(this.args), + args: replaceWithPlaceholders(this.args), _should_output_globally: !this.hide, ...(this.cache_age && { _cache_age: this.cache_age }), ...(this.cache_keys && { _cache_keys: this.cache_keys }), diff --git a/src/Substrate.ts b/src/Substrate.ts index 4ecb6cc..3f93944 100644 --- a/src/Substrate.ts +++ b/src/Substrate.ts @@ -112,7 +112,7 @@ export class Substrate { * @throws {Error} when the client encounters an error making the request. */ async runSerialized( - nodes: Node[], + nodes: Node[] = [], endpoint: string = "/compose", ): Promise { const serialized = Substrate.serialize(...nodes); diff --git a/src/sb.ts b/src/sb.ts index f94848c..f334409 100644 --- a/src/sb.ts +++ b/src/sb.ts @@ -5,6 +5,7 @@ export const sb = { concat: FutureString.concat, jq: FutureAnyObject.jq, interpolate: FutureString.interpolate, + jinja: FutureString.jinja, streaming: { fromSSEResponse: StreamingResponse.fromReponse, }, diff --git a/tests/Future.test.ts b/tests/Future.test.ts index 1eda437..70a5370 100644 --- a/tests/Future.test.ts +++ b/tests/Future.test.ts @@ -205,5 +205,31 @@ describe("Future", () => { // @ts-expect-error expect(i2._result()).resolves.toEqual("hello12"); }); + + describe(".jinja", () => { + test(".toJSON", () => { + const x = FutureString.concat("1", "2", "3"); + const f = FutureString.jinja("template: x={{x}} y={{y}}", { + x, + y: "abc", + }); + + const json = f.toJSON(); + + expect(json).toEqual({ + // @ts-ignore + id: f._id, + directive: { + type: "jinja", + template: { future_id: null, val: "template: x={{x}} y={{y}}" }, + variables: { + // @ts-ignore (_id) + x: { __$$SB_GRAPH_OP_ID$$__: x._id }, + y: "abc", + }, + }, + }); + }); + }); }); });