@lifi/compose-spec
Version:
Public wire-format types and schemas for Compose flows
100 lines (86 loc) • 3.26 kB
text/typescript
import type { Ref } from './flowSchema.js';
export type ParsedRef =
| { readonly scope: 'input'; readonly port: string }
| { readonly scope: 'output'; readonly node: string; readonly port: string }
| { readonly scope: 'context'; readonly key: ContextKey };
export type RefScope = ParsedRef['scope'];
export type ContextKey = 'sender' | 'executionAddress';
export const isContextKey = (key: string): key is ContextKey =>
key === 'sender' || key === 'executionAddress';
/**
* Ref prefixes that parseRef intercepts before the fallback `output` scope.
* A node whose id matches one of these would produce ambiguous refs
* (e.g. `context.sender` could be a context ref or node-output ref).
* Validation must reject these as node ids.
*/
export const RESERVED_REF_SCOPES: ReadonlySet<string> = new Set([
'input',
'context',
'literal',
]);
export const isRef = (v: unknown): v is Ref =>
typeof v === 'object' &&
v !== null &&
'$ref' in v &&
typeof v.$ref === 'string';
export const parseRef = (ref: Ref): ParsedRef => {
const dot = ref.$ref.indexOf('.');
if (dot === -1)
throw new Error(`Invalid ref: "${ref.$ref}" (must contain a dot)`);
const prefix = ref.$ref.slice(0, dot);
const suffix = ref.$ref.slice(dot + 1);
if (prefix === 'input') return { scope: 'input', port: suffix };
if (prefix === 'context') {
if (!isContextKey(suffix))
throw new Error(`Unknown context key: "${suffix}"`);
return { scope: 'context', key: suffix };
}
return { scope: 'output', node: prefix, port: suffix };
};
/** A ref path: a dotpath such as `input.amount` or `nodeId.port`. */
export type RefPath = `${string}.${string}`;
const buildRef = (path: RefPath): Ref => {
if (path.indexOf('.') < 1)
throw new Error(
`Invalid ref: "${path}" (must be a dotpath like "input.name" or "nodeId.port")`,
);
const built: Ref = { $ref: path };
parseRef(built); // rejects an unknown `context.*` key
return built;
};
/**
* Builds a bind ref. `ref.input(port)` for a flow input, `ref.context.*` for a
* context ref, and a dotpath for a node output (`ref('deposit.shares')`).
* Prefer the named forms: `parseRef` reads an unrecognised prefix as a node id,
* so a misspelled `input.`/`context.` silently becomes an output ref.
*/
export interface RefBuilder {
(path: RefPath): Ref;
input(port: string): Ref;
readonly context: { readonly [K in ContextKey]: Ref };
}
export const ref: RefBuilder = Object.assign(buildRef, {
input: (port: string): Ref => buildRef(`input.${port}`),
context: {
sender: buildRef('context.sender'),
executionAddress: buildRef('context.executionAddress'),
},
});
export const foldRef = <R>(
ref: ParsedRef,
cases: {
readonly input: (port: string) => R;
readonly context: (key: ContextKey) => R;
readonly output: (node: string, port: string) => R;
},
): R => {
if (ref.scope === 'input') return cases.input(ref.port);
if (ref.scope === 'context') return cases.context(ref.key);
return cases.output(ref.node, ref.port);
};
export const refKey = (ref: Ref): string =>
foldRef(parseRef(ref), {
input: (port) => `input:${port}`,
context: (key) => `context:${key}`,
output: (node, port) => `output:${node}:${port}`,
});