@pesto-io/pesto-zod
Version:
Pesto module to intantiate zod schema from Typescript source code as string.
342 lines (341 loc) • 13.5 kB
TypeScript
import { ObjectLiteralExpression } from "ts-morph";
import { type AnyZodObject } from "zod";
/**
* The {@ZodSchemaParser } class will parse a string assumed to be a zod schema source code, and will instantiate the Zod Schema.
*/
export declare class ZodSchemaParser {
protected zodSchemaAsString: string;
/**
* The unique ID of
* this {@ZodSchemaParser } instance.
*
* This uuid is used to generate a unique
* name for the sourceFile
*
*/
private unique_id;
/**
* The ts-morph API Project instance which
* will used to compile/parse the typescript code which is a zodSchema
*/
private project;
private tsConfigRootdir;
/**
* The filename of the file in which the source
* code to process will be saved to.
*/
private filename;
/**
* The source file obeject used by the TS compiler API
*/
private sourceFile;
/**
* Represents a variable Declaration Statement
* which is assigned as value, a zodSchema.
*
* E.g.:
*
* const doesntMatter = z.object({
* title: z.string(),
* tags: z.array(z.string()),
* image: z.string().optional(),
* })
*/
/**
* The ts-morph / TypeScript Compiler API
* Variable Declaration of the Zod Schema, in the
* built ts-morph Project
*
* Represents a variable Declaration Statement
* which is assigned as value, a zodSchema.
*
* E.g.:
*
* <code>
* import { z } from "zod";
*
* ////
* // Below that's the variable declaration represented:
* ////
* const weDontCare = z.object({
* // ...
* })
* </code>
*/
private zodSchemaVarDeclaration;
/**
* The ts-morph / TypeScript Compiler API type checker
*/
private typeChecker;
/**
* Represents the full statement importing zod:
*
* <code>
* // the zodImport is the full below line.
* import { z } from "zod";
* </code>
*
* The {@ZodSchemaParser } will always use
* 'import { z } from "zod";' as the zod import,
* since it is not provided by the user of the {@ZodSchemaParser } class.
*
* Why? because we don't care what is
* the zod import, as long as the zod is imported, we
* care about instiating the Zod Schema.
*
*/
private zodImport;
/**
* The name of the zod import.
*
* E.g.:
*
* <code>
* // the name of the zod import is 'z'
* import { z } from "zod";
* </code>
*
* <code>
* // the name of the zod import is 'myZod'
* import { z as myZod } from "zod";
* </code>
*
* The {@ZodSchemaParser } will always use 'z' as
* the name of the zod import, since it is not
* provided by the user of the {@ZodSchemaParser } class.
*
* Why? because we don't care what is
* the name of the zod import, we
* care about instiating the Zod Schema.
*
*/
private nameOfTheZodImport;
/**
* This property represents
* This property is initialized by
* the {@ZodSchemaParser [initZodObjectLiteral(): void]}
* method.
*/
private zodObjectLiteral;
/**
*
* Example values of <pre>zodSchemaAsString</pre> :
* ---
*
* Example 1:
* ----------
*
* <code>
* z.object({
* title: z.string(),
* subtitle: z.string(),
* summary: z.string(),
* category: z.array(z.string()),
* image: z.string(),
* tags: z.array(z.string()),
* })
* </code>
*
* Example 2:
* ----------
*
* <code>
* z.object({
* title: z.string(),
* hereAnother: z.object({
* reseau: z.object({
* cesar: z.array(z.string()),
* }),
* imLackingIdea: z.boolean().optional(),
* itsForATest: z.boolean(),
* }),
* tags: z.array(z.string( ) ),
* another: z.boolean().nullish(),
* exampleCategory: z.array(z.array(z.string().nullable())).optional(),
* example2Category: z.nullable(z.array(z.boolean())).optional(),
* example3Category: z.optional(z.number()).array(),
* example4Category: z.array(z.number()).optional(),
* image: z.string().optional(),
* somethingElseNested: z.object({
* firstname: z.string().array(),
* lastname: z.string(),
* color: z.string(),
* two: z.boolean().optional(),
* three: z.number().array().optional(),
* four: z.array(z.number()).optional(),
* }),
* department: z.object({
* divisionName: z.string(),
* secrecyTags: z.array(z.string()).optional(),
* }),
* })
* </code>
*
* @param zodSchemaAsString the text of the zod schema, without any variable declaration, just the zod schema alone. see above example.
* @param p_tsConfigRootdir the path to the folder used to set the <pre>rootDir</pre> TypeScript compiler configuration property value. (typically found in any <pre>tsconfig.json</pre> file)
*/
constructor(zodSchemaAsString: string, p_tsConfigRootdir?: string);
experiment(): void;
/**
* This method assumes that the provided Zod Schema is of the following form:
*
* <code>
* z.object({
* // and here some JSON properties, like usual for a zod schema.
* })
* </code>
*
* But the provided zod schema could be of the following forms:
*
*
* <code>
* z.tuple([
* z.object({first: z.array(z.string())}),
* z.object({second: z.array(z.string())}),
* z.object({third: z.array(z.string())}),
* ])
* </code>
*
*
* <code>
* z.array(z.string()).optional()
* </code>
*
*
* <code>
* z.tuple([
* z.array(z.string()),
* z.boolean().optional(),
* z.tuple([
* z.object({something: z.array(z.string())}),
* z.object({somethingElse: z.array(z.string())}),
* ]),
* ])
* </code>
*
* So, what I need to do here, to generalize to
* any zod schema declaration, is a
* function, able to determine what is the
* first top zod function called, and from there, I
* will have 3 cases:
*
* - [[CASE-1]] The top zod function call is the "object" zod function.
* - [[CASE-2]] The top zod function call is the "tuple" zod function.
* - [[CASE-3]] The top zod function call is any other zod function.
*
* Those 3 cases will be processed as the first recurrent call in the {@ZodSchemaParser#parse()} method.
*
* For those 3 cases, we will then have 3 cases for the parameter provided to the top zod function call:
*
* - [[CASE-1]] A zod Object Literal of 'ts-morph' type {@ObjectLiteralExpression }: that case I already worked on it, with my 2 reccurrent functions.
* - [[CASE-2]] An Array Literal of 'ts-morph' type {@ArrayLiteralExpression }
* - [[CASE-3]] A Function Literal, of 'ts-morph' type {@FunctionExpression }
*
* So what makes sense as a property here is
* not <pre>this.zodObjectLiteral</pre>, of
* type {@ObjectLiteralExpression }, but instead
* <pre>this.topZodFunctionCall</pre>
* of type {@FunctionExpression }
*
* And we are going to init
* that <pre>this.topZodFunctionCall</pre> {@FunctionExpression }
*
* And we will have method like:
*
* isZodTopFunctionCallObject()
* isZodTopFunctionCallTuple()
* isZodTopFunctionCallNeitherTupleNorObject()
* ---------------------------------------------
* Algorithm to catch the top zod function call:
* - to begin with, we test if the first traversed node, for which <pre>node.print()</pre> is equal to the initializer of the <pre>this.zodSchemaVarDeclaration</pre>, ends with <pre>()</pre>. If yes, then we pass the entire node to be processed by our recurrent function, and we have to get rid of the whole chain of the whole chain of function calls...: Here note that the zod framework fgives us a rule that simplifies a lot the work. That rule is : if in a chain of zod function calls, there is one fuction which has a parameter passed to, then we know that this function with parameter is the first function call (on the left), anyone can try to give me any zod chained function calls, that does not comply with that rule, that anyone will fail to im my opinion, yet, we wiil see if it happens, i will just assume this rule as axiomatic.
* >>> OHHHH I know, I know, I know :
* if the first traversed node, ends with "()"
* then this means that the top function call is
* the last on the right.
* since it has no parameter, then we
* recurently call the algorithm on
* the caller of that function:
* > Until we have only one function call left, and
* the caller is the zod named import
* > that only one function call left then either has parameters of not
* > if it does not have parameters, then we have all
* the informations we need to instantiate the zod schema
* > if it does have parameter(s), then we have our 3 cases for which we have to launch the reccurence:
*
* - [[CASE-1]] A zod Object Literal of 'ts-morph' type {@ObjectLiteralExpression }: that case I already worked on it, with my 2 reccurrent functions.
* - [[CASE-2]] An Array Literal of 'ts-morph' type {@ArrayLiteralExpression }
* - [[CASE-3]] A Function Literal, of 'ts-morph' type {@FunctionExpression }
*
* So, in the case of the zod schema to be a chain of zod function calls "right at the start", well then we have to reduce it
*
* Ok, our initialized "topZodFunction" is acutally:
* the top zod function call which has parameter(s), because in a zod expression, in a chain of call, there can only be one function which takes parameter(s) in.
* Ok, we have a definition..
*
* Its there a funny operation on a tree, in graph theory: it i sa bit like "pull a tree by its hair, to chnge the root of the tree"...
* This funny operation is simpe, yet very ineresting:
* - choose any node, that we will call "the pulled node" (but its more interesting if you chosse a node which has a good number of descendants)
* - you then "invert" all paths from that node, to any of its ascendants:
* - that way, the pulled node suddenly endsup being the root node of the tree
* - that operation at least makes sense on a graph that is a tree: it has only single root node.
* I dot know I think there are useful mathematical results we could get out of considering pulling tree nodes that are neighbors, like that.
*
* - to begin with, we ignore the first traversed node, for which <pre>node.print()</pre> is equal to the initializer of the <pre>this.zodSchemaVarDeclaration</pre>
* - then we will ignodre the second traversed node, which will the the zod named import
* - after that,
* @returns the Zod object, instantiated by parsing <pre>this.zodSchemaVarDeclaration</pre>
*/
initZodObjectLiteral(): void;
/**
* This method validates that the
* source code in the source file built
* based on the constructor-provided
* string <pre>zodSchemaAsString</pre>,
* assumed to be a zod schema, sucessfully
* compiles with the TypeScript Compiler.
* -
* https://ts-morph.com/setup/diagnostics
* @throws an Error if the source code does not compile as TypeScript source code
*/
private validate;
/**
* This method parses the source code to intantiate the Zod Schema
* @returns the Zod object, instantiated by parsing <code>this.zodSchemaVarDeclaration}</code>
*/
parse(): AnyZodObject;
/**
*
* This method instantiates a JSON Object which will be used to create a zod schema <pre>z.object(theJSONObject)</pre>
* This method uses recurrence
* @returns the JSON Object which will be used to create a zod schema <pre>z.object(theJSONObject)</pre>
*/
instantiateZodJsonConfig(objectLiteralNode: ObjectLiteralExpression): any;
/**
* This method parses the source code to intantiate the Zod Schema
* @returns the Zod object, instantiated by parsing <code>this.zodSchemaVarDeclaration}</code>
*/
demoDFStraversal(): AnyZodObject;
/**
* The method which evaluates a tree of zod funtion calls
* the tree is provided as a simple string, using the two methods:
*
* <pre>this.instantiateLeafFunctionCallFrom</pre>
* <pre>this.instantiateFunctionCalledWithParamsFrom</pre>
*
*
*/
testInstantiateFrom(treeOfZodFunctionCalls: string): any;
/**
*
* @param caller the caller of the function
* @param calledFunction a function call that does not take any parameter
* @returns
*/
private instantiateLeafFunctionCallFrom;
/**
* This method returns <pre>z.calledFunction(<instantiated descendantCalledFunctionParam>)</pre>
* @param calledFunction the name of a zod function, which is assumed to be called directly by the named zod import <pre>this.nameOfTheZodImport</pre>
* @param calledFunctionParam the single parameter passed to the zod function call
*/
private instantiateFunctionCalledWithParamsFrom;
}