Skip to content

Commit b5c1afe

Browse files
committed
feat: adding merge schema function
1 parent ebf1f50 commit b5c1afe

1 file changed

Lines changed: 166 additions & 7 deletions

File tree

src/util/schema.util.ts

Lines changed: 166 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,39 +124,46 @@ export const getSchema = (
124124
parameterType: ts.Type,
125125
functionDeclarations: FunctionDeclaration[],
126126
functions: FunctionDefinition[],
127-
suggestions: boolean = true
127+
suggestions: boolean = true,
128+
suggestionType?: ts.Type,
128129
): Schema => {
129130

130131
if ((parameterType.flags & ts.TypeFlags.TypeParameter) !== 0) {
131132
const decl = parameterType.symbol?.declarations?.[0]
132133
if (decl && ts.isTypeParameterDeclaration(decl) && decl.constraint) {
133134
// getTypeFromTypeNode statt getBaseConstraintOfType → aliasSymbol bleibt erhalten
134135
const constraintType = checker.getTypeFromTypeNode(decl.constraint)
135-
return getSchema(checker, node, constraintType, functionDeclarations, functions, suggestions)
136+
return getSchema(checker, node, constraintType, functionDeclarations, functions, suggestions, suggestionType)
136137
}
137138
}
138139

140+
// Suggestions are filtered by what the surrounding function accepts, not by
141+
// the narrower type a current value happens to narrow the node-side to.
142+
// Example: `<T>(value: T)` with a current boolean literal must still surface
143+
// every reference in scope, because the function takes anything.
144+
const typeForSuggestions = suggestionType ?? parameterType;
145+
139146
// Collect all available suggestions for this parameter
140147
const combinedSuggestions = suggestions ? {
141148
suggestions: [
142-
...getValues(parameterType, checker),
149+
...getValues(typeForSuggestions, checker),
143150
...(node ? getReferences(
144151
checker,
145152
node,
146-
parameterType,
153+
typeForSuggestions,
147154
checker.getSymbolsInScope(node, ts.SymbolFlags.Variable)
148155
) : []),
149156
...getNodes(
150157
checker,
151158
functionDeclarations,
152159
functions,
153-
parameterType
160+
typeForSuggestions
154161
),
155162
...getSubFlows(
156163
checker,
157164
functionDeclarations,
158165
functions,
159-
parameterType
166+
typeForSuggestions
160167
),
161168
],
162169
} : {};
@@ -274,9 +281,161 @@ export const getSchema = (
274281
};
275282
}
276283

277-
// Fallback for unknown or generic types
284+
// Fallback for unknown or generic types — still surface any collected
285+
// suggestions (e.g. references in scope) so the UI is never silently empty.
278286
return {
279287
input: "generic",
288+
...combinedSuggestions,
289+
};
290+
};
291+
292+
/**
293+
* Merges a function-declared parameter schema with the schema derived from the
294+
* concrete node value. The function schema is treated as the source of truth for
295+
* the structural shape (input kind, properties, items); the node schema only
296+
* contributes additional suggestions and, when the function schema is generic,
297+
* a fallback shape.
298+
*
299+
* Rules:
300+
* - If the function schema is generic, follow the node schema — but never as a
301+
* select. A single literal value (e.g. "Test") narrowing a generic T must not
302+
* collapse the input into a select with one option; it should remain free-form
303+
* text/number/boolean matching the literal kind.
304+
* - Otherwise use the function schema's input kind and merge suggestions from both.
305+
* Recurse into `properties` (for data) and `items` (for list) so nested generics
306+
* inside concrete containers are handled the same way.
307+
*
308+
* Suggestions are concatenated and de-duplicated by structural equality.
309+
*
310+
* @param functionSchema - The schema derived from the declared function parameter type
311+
* @param nodeSchema - The schema derived from the node's concrete (narrowed) parameter type
312+
* @returns A single merged schema
313+
*/
314+
export const mergeSchemas = (
315+
functionSchema: Schema | undefined,
316+
nodeSchema: Schema,
317+
valueProvided: boolean = false,
318+
): Schema => {
319+
if (!functionSchema) {
320+
return liftGenericIfValued(demoteSelect(nodeSchema), valueProvided);
321+
}
322+
323+
if (functionSchema.input === "generic") {
324+
return liftGenericIfValued(demoteSelect(nodeSchema), valueProvided);
325+
}
326+
327+
const suggestions = mergeSuggestions(
328+
functionSchema.suggestions,
329+
nodeSchema.suggestions,
330+
);
331+
332+
if (functionSchema.input === "data") {
333+
const fProps = functionSchema.properties ?? {};
334+
const nProps =
335+
nodeSchema.input === "data" ? (nodeSchema.properties ?? {}) : {};
336+
const properties: Record<string, Schema | Schema[]> = {};
337+
const keys = new Set([...Object.keys(fProps), ...Object.keys(nProps)]);
338+
for (const key of keys) {
339+
const f = fProps[key];
340+
const n = nProps[key];
341+
properties[key] = mergeProperty(f, n);
342+
}
343+
return {
344+
...functionSchema,
345+
properties,
346+
...(suggestions ? {suggestions} : {}),
347+
};
348+
}
349+
350+
if (functionSchema.input === "list") {
351+
const fItems = functionSchema.items ?? [];
352+
const nItems =
353+
nodeSchema.input === "list" ? (nodeSchema.items ?? []) : [];
354+
const items =
355+
fItems.length === nItems.length && fItems.length > 0
356+
? fItems.map((f, i) => mergeSchemas(f, nItems[i]))
357+
: fItems;
358+
return {
359+
...functionSchema,
360+
items,
361+
...(suggestions ? {suggestions} : {}),
362+
};
363+
}
364+
365+
return {
366+
...functionSchema,
367+
...(suggestions ? {suggestions} : {}),
368+
};
369+
};
370+
371+
const mergeProperty = (
372+
f: Schema | Schema[] | undefined,
373+
n: Schema | Schema[] | undefined,
374+
): Schema | Schema[] => {
375+
if (f && !Array.isArray(f) && n && !Array.isArray(n)) {
376+
return mergeSchemas(f, n);
377+
}
378+
return (f ?? n)!;
379+
};
380+
381+
const mergeSuggestions = (
382+
a: Input["suggestions"],
383+
b: Input["suggestions"],
384+
): Input["suggestions"] | undefined => {
385+
const all = [...(a ?? []), ...(b ?? [])];
386+
if (all.length === 0) return undefined;
387+
const seen = new Set<string>();
388+
const result: NonNullable<Input["suggestions"]> = [];
389+
for (const item of all) {
390+
const key = JSON.stringify(item);
391+
if (seen.has(key)) continue;
392+
seen.add(key);
393+
result.push(item);
394+
}
395+
return result;
396+
};
397+
398+
// Generic means "we extracted nothing structural from either side". That is the
399+
// right answer for an empty parameter slot, but if the user has provided a value
400+
// then the merge already had the node-side schema to draw from — if both sides
401+
// still came out generic (e.g. the value resolved to `any`/`unknown`), the most
402+
// useful open shape is `data`. Primitive values never reach this branch: their
403+
// node schema is text / number / boolean / select-then-demoted, so the merge
404+
// produces a concrete kind before this lift runs.
405+
const liftGenericIfValued = (schema: Schema, valueProvided: boolean): Schema => {
406+
if (!valueProvided || schema.input !== "generic") return schema;
407+
return {
408+
input: "data",
409+
properties: {},
410+
required: [],
411+
...(schema.suggestions ? {suggestions: schema.suggestions} : {}),
412+
};
413+
};
414+
415+
const demoteSelect = (schema: Schema): Schema => {
416+
if (schema.input !== "select") return schema;
417+
const suggestions = schema.suggestions ?? [];
418+
const literalKinds = new Set<string>();
419+
for (const s of suggestions) {
420+
const value = (s as LiteralValue).value;
421+
const kind = typeof value;
422+
if (kind === "string" || kind === "number" || kind === "boolean") {
423+
literalKinds.add(kind);
424+
}
425+
}
426+
const target: PrimitiveInput["input"] =
427+
literalKinds.size === 1
428+
? (
429+
{
430+
string: "text",
431+
number: "number",
432+
boolean: "boolean",
433+
} as const
434+
)[[...literalKinds][0] as "string" | "number" | "boolean"]
435+
: "text";
436+
return {
437+
input: target,
438+
...(suggestions.length > 0 ? {suggestions} : {}),
280439
};
281440
};
282441

0 commit comments

Comments
 (0)