Skip to content

Commit 61afdf8

Browse files
committed
feat: implement getSchema function for generating structured schemas from TypeScript types
1 parent c769394 commit 61afdf8

1 file changed

Lines changed: 339 additions & 0 deletions

File tree

src/util/schema.util.ts

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
import ts, { FunctionDeclaration } from "typescript";
2+
import { getValues } from "./values.util";
3+
import { getReferences } from "./references.util";
4+
import { getNodes } from "./nodes.util";
5+
import {
6+
FunctionDefinition,
7+
LiteralValue,
8+
NodeFunction,
9+
ReferenceValue,
10+
} from "@code0-tech/sagittarius-graphql-types";
11+
12+
13+
/**
14+
* Base interface for all input types.
15+
* Provides common properties for suggestions and input metadata.
16+
*/
17+
interface Input {
18+
/** The type of input (string representation) */
19+
input?: string;
20+
/** Array of suggested values (functions, references, or literals) */
21+
suggestions?: (NodeFunction | ReferenceValue | LiteralValue)[];
22+
}
23+
24+
/**
25+
* Represents a generic input type with no specific structure.
26+
* Used as a fallback when the type cannot be determined.
27+
*/
28+
interface GenericInput {
29+
input?: "generic";
30+
}
31+
32+
/**
33+
* Represents a sub-flow input type (callable/function type).
34+
* Used for types that have call signatures.
35+
*/
36+
interface SubFlowInput {
37+
input?: "sub-flow";
38+
}
39+
40+
/**
41+
* Represents primitive input types: boolean, number, text, or select.
42+
* Extends the base Input interface to include suggestions.
43+
*/
44+
interface PrimitiveInput extends Input {
45+
input?: "boolean" | "number" | "text" | "select";
46+
}
47+
48+
/**
49+
* Represents a data object input type with structured properties.
50+
* Includes property definitions and required field tracking.
51+
*/
52+
interface DataInput extends Input {
53+
input?: "data";
54+
/** Record mapping property names to their schemas */
55+
properties?: Record<string, Schema | Schema[]>;
56+
/** Array of required property names */
57+
required?: string[];
58+
}
59+
60+
/**
61+
* Represents a list/array input type with item schemas.
62+
* Supports homogeneous or heterogeneous arrays.
63+
*/
64+
interface ListInput extends Input {
65+
input?: "list";
66+
/** Schema or array of schemas for list items */
67+
items?: Schema | Schema[];
68+
}
69+
70+
/**
71+
* Represents a complex type input with properties and required fields.
72+
* Similar to DataInput but used for type definitions.
73+
*/
74+
interface TypeInput extends Input {
75+
input?: "type";
76+
/** Record mapping property names to their schemas */
77+
properties?: Record<string, Schema | Schema[]>;
78+
/** Array of required property names */
79+
required?: string[];
80+
}
81+
82+
/**
83+
* Union type representing all possible schema input types.
84+
* Discriminated union based on the 'input' field.
85+
*/
86+
export type Schema =
87+
| PrimitiveInput
88+
| DataInput
89+
| ListInput
90+
| TypeInput
91+
| SubFlowInput
92+
| GenericInput;
93+
94+
95+
/**
96+
* Generates a schema definition for a given TypeScript type.
97+
*
98+
* This function analyzes a TypeScript type and produces a structured schema
99+
* that describes how the type should be presented and validated. It handles:
100+
* - Primitive types (boolean, number, string)
101+
* - Union types of primitives
102+
* - Array/Tuple types
103+
* - Complex object types with properties
104+
* - Sub-flow types (callables)
105+
*
106+
* For each type, the function also collects suggestions from:
107+
* - Literal values from the type definition
108+
* - Variable references available in scope
109+
* - Function node suggestions based on parameter type
110+
*
111+
* @param checker - TypeScript type checker for type analysis
112+
* @param node - The variable declaration node being analyzed
113+
* @param parameterType - The type to generate a schema for
114+
* @param functionDeclarations - Array of function declaration nodes
115+
* @param functions - Array of function definitions for matching
116+
* @returns A Schema object describing how to handle the parameter type
117+
*/
118+
export const getSchema = (
119+
checker: ts.TypeChecker,
120+
node: ts.VariableDeclaration,
121+
parameterType: ts.Type,
122+
functionDeclarations: FunctionDeclaration[],
123+
functions: FunctionDefinition[]
124+
): Schema => {
125+
// Collect all available suggestions for this parameter
126+
const literalValueSuggestions = getValues(parameterType);
127+
const referenceSuggestions = getReferences(
128+
checker,
129+
node!,
130+
parameterType,
131+
checker.getSymbolsInScope(node!, ts.SymbolFlags.Variable)
132+
);
133+
const nodeSuggestions = getNodes(
134+
checker,
135+
functionDeclarations,
136+
functions,
137+
parameterType
138+
);
139+
const suggestions = {
140+
suggestions: [
141+
...literalValueSuggestions,
142+
...referenceSuggestions,
143+
...nodeSuggestions,
144+
],
145+
};
146+
147+
// Check primitive literal union first (e.g., "a" | "b" | "c")
148+
if (isPrimitiveLiteralUnion(parameterType)) {
149+
return { input: "select", ...suggestions };
150+
}
151+
152+
// Check individual primitive types
153+
if (isBoolean(parameterType)) {
154+
return { input: "boolean", ...suggestions };
155+
}
156+
if (isNumber(parameterType)) {
157+
return { input: "number", ...suggestions };
158+
}
159+
if (isString(parameterType)) {
160+
return { input: "text", ...suggestions };
161+
}
162+
163+
// Check if type has call signatures (is callable/sub-flow)
164+
if (isSubFlow(parameterType)) {
165+
return { input: "sub-flow", ...suggestions };
166+
}
167+
168+
// Handle array and tuple types
169+
if (isArrayType(checker, parameterType)) {
170+
const itemType = checker.getTypeArguments(
171+
parameterType as ts.TypeReference
172+
)[0];
173+
const itemTypes = itemType.isUnion() ? itemType.types : [itemType];
174+
const itemSchemas = itemTypes.map((itemType) =>
175+
getSchema(checker, node, itemType, functionDeclarations, functions)
176+
);
177+
178+
return {
179+
input: "list",
180+
items: itemSchemas.length === 1 ? itemSchemas[0] : itemSchemas,
181+
...suggestions,
182+
};
183+
}
184+
185+
// Handle complex object types with properties
186+
if ((parameterType.flags & ts.TypeFlags.Object) !== 0) {
187+
const properties: Record<string, Schema | Schema[]> = {};
188+
const required: string[] = [];
189+
190+
// Iterate through all properties of the object type
191+
for (const property of checker.getPropertiesOfType(parameterType)) {
192+
const declaration =
193+
property.valueDeclaration ?? property.declarations?.[0];
194+
195+
if (!declaration) continue;
196+
197+
const propertyType = checker.getTypeOfSymbolAtLocation(
198+
property,
199+
declaration
200+
);
201+
202+
// Determine if the property is optional
203+
const isOptional =
204+
(property.flags & ts.SymbolFlags.Optional) !== 0 ||
205+
(propertyType.isUnion() &&
206+
propertyType.types.some(
207+
(t) => (t.flags & ts.TypeFlags.Undefined) !== 0
208+
));
209+
210+
// Filter out undefined type from union types
211+
const propertyTypes = propertyType.isUnion()
212+
? propertyType.types.filter(
213+
(t) => (t.flags & ts.TypeFlags.Undefined) === 0
214+
)
215+
: [propertyType];
216+
217+
// Recursively generate schemas for property types
218+
const propertySchemas = propertyTypes.map((type) =>
219+
getSchema(checker, node, type, functionDeclarations, functions)
220+
);
221+
222+
properties[property.name] =
223+
propertySchemas.length === 1 ? propertySchemas[0] : propertySchemas;
224+
225+
// Track required properties
226+
if (!isOptional) {
227+
required.push(property.name);
228+
}
229+
}
230+
231+
return {
232+
input: "data",
233+
properties,
234+
required,
235+
...suggestions,
236+
};
237+
}
238+
239+
// Fallback for unknown or generic types
240+
return {
241+
input: "generic",
242+
};
243+
};
244+
245+
/**
246+
* Checks if a type is a boolean type (either boolean or boolean literal).
247+
*
248+
* This function checks for both the general boolean type and specific boolean
249+
* literal types (true, false).
250+
*
251+
* @param type - The type to check
252+
* @returns True if the type is a boolean or boolean literal, false otherwise
253+
*/
254+
function isBoolean(type: ts.Type): boolean {
255+
return (
256+
(type.flags & ts.TypeFlags.Boolean) !== 0 ||
257+
(type.flags & ts.TypeFlags.BooleanLiteral) !== 0
258+
);
259+
}
260+
261+
/**
262+
* Checks if a type is a number type (either number or number literal).
263+
*
264+
* This function checks for both the general number type and specific numeric
265+
* literal types (42, 3.14, etc.).
266+
*
267+
* @param type - The type to check
268+
* @returns True if the type is a number or number literal, false otherwise
269+
*/
270+
function isNumber(type: ts.Type): boolean {
271+
return (
272+
(type.flags & ts.TypeFlags.Number) !== 0 ||
273+
(type.flags & ts.TypeFlags.NumberLiteral) !== 0
274+
);
275+
}
276+
277+
/**
278+
* Checks if a type is a string type (either string or string literal).
279+
*
280+
* This function checks for both the general string type and specific string
281+
* literal types ("hello", "world", etc.).
282+
*
283+
* @param type - The type to check
284+
* @returns True if the type is a string or string literal, false otherwise
285+
*/
286+
function isString(type: ts.Type): boolean {
287+
return (
288+
(type.flags & ts.TypeFlags.String) !== 0 ||
289+
(type.flags & ts.TypeFlags.StringLiteral) !== 0
290+
);
291+
}
292+
293+
/**
294+
* Checks if a type is any primitive type (string, number, or boolean).
295+
*
296+
* @param type - The type to check
297+
* @returns True if the type is a string, number, or boolean, false otherwise
298+
*/
299+
function isPrimitive(type: ts.Type): boolean {
300+
return isString(type) || isNumber(type) || isBoolean(type);
301+
}
302+
303+
/**
304+
* Checks if a type is a union of primitive types only.
305+
*
306+
* This function is used to identify union types that can be represented as
307+
* a select input with predefined options (e.g., "a" | "b" | "c" or 1 | 2 | 3).
308+
*
309+
* @param type - The type to check
310+
* @returns True if the type is a union where all members are primitives, false otherwise
311+
*/
312+
function isPrimitiveLiteralUnion(type: ts.Type): boolean {
313+
if (!type.isUnion()) return false;
314+
return type.types.every(isPrimitive);
315+
}
316+
317+
/**
318+
* Checks if a type is an array or tuple type.
319+
*
320+
* @param checker - TypeScript type checker for type analysis
321+
* @param type - The type to check
322+
* @returns True if the type is an array or tuple, false otherwise
323+
*/
324+
function isArrayType(checker: ts.TypeChecker, type: ts.Type): boolean {
325+
return checker.isArrayType(type) || checker.isTupleType(type);
326+
}
327+
328+
/**
329+
* Checks if a type is a callable type (has call signatures).
330+
*
331+
* A type with call signatures can be invoked like a function. This is used
332+
* to identify sub-flow types that represent workflow steps.
333+
*
334+
* @param type - The type to check
335+
* @returns True if the type has call signatures, false otherwise
336+
*/
337+
export function isSubFlow(type: ts.Type): boolean {
338+
return type.getCallSignatures().length > 0;
339+
}

0 commit comments

Comments
 (0)