zod-to-x 2.1.0-dev.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -5
- package/dist/core/ast_node.d.ts +6 -4
- package/dist/core/ast_node.js +22 -10
- package/dist/layered-modeling/model.d.ts +1 -0
- package/dist/transpilers/index.d.ts +1 -0
- package/dist/transpilers/index.js +3 -1
- package/dist/transpilers/python/libs.d.ts +23 -0
- package/dist/transpilers/python/libs.js +28 -0
- package/dist/transpilers/python/options.d.ts +9 -0
- package/dist/transpilers/python/options.js +8 -0
- package/dist/transpilers/python/runner.d.ts +116 -0
- package/dist/transpilers/python/runner.js +397 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
<em style="font-size: smaller;">Image generated using Canvas AI.</em>
|
|
4
4
|
</p>
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://
|
|
6
|
+
<a href="https://www.npmjs.com/package/zod-to-x" target="_blank">
|
|
7
|
+
<img src="https://img.shields.io/badge/npm%20-red?style=for-the-badge&logo=npm" alt="npm">
|
|
8
|
+
</a>
|
|
9
|
+
<a href="https://github.com/rroumenov/zod-to-x/releases" target="_blank" style="margin-left: 10px;">
|
|
7
10
|
<img src="https://img.shields.io/badge/View%20Changelog-brightgreen?style=for-the-badge" alt="View Changelog">
|
|
8
11
|
</a>
|
|
9
12
|
<a href="https://playcode.io/2277071" target="_blank" style="margin-left: 10px;">
|
|
@@ -20,6 +23,14 @@
|
|
|
20
23
|
|
|
21
24
|
<span style="color: red;">**Important Announcement:**</span> `zod-to-x@2.0.0` has been released, introducing migration to Zod V4. At this stage, only the existent behavior has been migrated, while new features like Literal Templates are still under analysis. Only the complete Zod V4 version will be supported, **not v4-mini**. Additionally, `zod-to-x@1.X.Y` will continue to be maintained for Zod V3, and any new transpilation languages will also be supported in version 1.
|
|
22
25
|
|
|
26
|
+
<p align="center">
|
|
27
|
+
<strong>Supported Languages:</strong><br><br>
|
|
28
|
+
<img src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript">
|
|
29
|
+
<img src="https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python">
|
|
30
|
+
<img src="https://img.shields.io/badge/C++-00599C?style=for-the-badge&logo=cplusplus&logoColor=white" alt="C++">
|
|
31
|
+
<img src="https://img.shields.io/badge/Protobuf-4285F4?style=for-the-badge&logo=google&logoColor=white" alt="Protobuf">
|
|
32
|
+
</p>
|
|
33
|
+
|
|
23
34
|
|
|
24
35
|
|
|
25
36
|
## Table of contents
|
|
@@ -32,10 +43,11 @@
|
|
|
32
43
|
- [Layered modeling](#layered-modeling)
|
|
33
44
|
- [Usage example](#usage-example)
|
|
34
45
|
- [Custom layers](#custom-layers)
|
|
35
|
-
- [Generic types](#generic-types)
|
|
46
|
+
- [Generic types](#generic-types)
|
|
36
47
|
- [Currently supported output languages](#currently-supported-output-languages)
|
|
37
48
|
- [Typescript](#1-typescript)
|
|
38
|
-
- [
|
|
49
|
+
- [Python](#2-python) <sup>*(new)*</sup>
|
|
50
|
+
- [C++](#3-c)
|
|
39
51
|
- [Additional utils](#additional-utils)
|
|
40
52
|
- [Protobuf V3 generation](#2-zod2protov3)
|
|
41
53
|
- [Mapping of supported Zod Types by Language](#mapping-of-supported-zod-types-by-langauge)
|
|
@@ -50,7 +62,7 @@ Managing data consistency across multiple layers and languages is a common chall
|
|
|
50
62
|
Define your data structures in one place using the powerful [`@zod`](https://github.com/colinhacks/zod) library. This eliminates redundancy, reduces inconsistencies, and simplifies maintenance across your entire codebase, all while allowing you to continue leveraging any npm package in the [`@zod`](https://github.com/colinhacks/zod) ecosystem.
|
|
51
63
|
|
|
52
64
|
2. **Multi-Language Compatibility**
|
|
53
|
-
Generate data models for TypeScript,
|
|
65
|
+
Generate data models for TypeScript, Python (Pydantic), C++, and Protobuf V3 (with languages like Golang on the roadmap). No more manually rewriting models for different platforms.
|
|
54
66
|
|
|
55
67
|
3. **Enhanced Productivity**
|
|
56
68
|
Automate the transpilation of data models to save time, reduce errors, and let your team focus on business logic instead of boilerplate code.
|
|
@@ -600,7 +612,13 @@ Common options:
|
|
|
600
612
|
- **keepKeys**: Specifies whether property names should follow the TypeScript naming convention (false) or remain as originally defined (true). The default is `false`.
|
|
601
613
|
- [Examples](https://github.com/rroumenov/zod-to-x/blob/main/test/test_zod2ts)
|
|
602
614
|
|
|
603
|
-
### 2)
|
|
615
|
+
### 2) Python
|
|
616
|
+
`Pydantic` is used for data validation and serialization/deserialization. Generates Pydantic BaseModel classes with full type hints.
|
|
617
|
+
- Options:
|
|
618
|
+
- **keepKeys**: Specifies whether property names should follow the Python naming convention (false) or remain as originally defined (true). The default is `false`.
|
|
619
|
+
- [Examples](https://github.com/rroumenov/zod-to-x/blob/main/test/test_zod2py)
|
|
620
|
+
|
|
621
|
+
### 3) C++
|
|
604
622
|
`Nlohmann` dependency is used for data serialization/deserialization. For *C++11*, `Boost` dependency is used. For *C++17* or newer, standard libraries are used.
|
|
605
623
|
- Options:
|
|
606
624
|
- **includeNulls**: When serializing, include all values even if `null`. Defaults to `false`.
|
package/dist/core/ast_node.d.ts
CHANGED
|
@@ -64,11 +64,13 @@ export declare class Zod2Ast {
|
|
|
64
64
|
*/
|
|
65
65
|
private _getEnumValues;
|
|
66
66
|
/**
|
|
67
|
-
* Intersects the properties of two
|
|
67
|
+
* Intersects the properties of two Zod object schemas and returns the combined properties.
|
|
68
|
+
* Processes the Zod shapes directly instead of looking up cached AST nodes, ensuring that
|
|
69
|
+
* instantiated generic types (from useGenericType) are correctly resolved.
|
|
68
70
|
*
|
|
69
|
-
* @param left - The left
|
|
70
|
-
* @param right - The right
|
|
71
|
-
* @returns An object containing the combined properties of the left and right
|
|
71
|
+
* @param left - The left Zod object schema.
|
|
72
|
+
* @param right - The right Zod object schema.
|
|
73
|
+
* @returns An object containing the combined properties of the left and right schemas.
|
|
72
74
|
*/
|
|
73
75
|
private _intersectAstNodes;
|
|
74
76
|
/**
|
package/dist/core/ast_node.js
CHANGED
|
@@ -138,18 +138,30 @@ class Zod2Ast {
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
|
-
* Intersects the properties of two
|
|
141
|
+
* Intersects the properties of two Zod object schemas and returns the combined properties.
|
|
142
|
+
* Processes the Zod shapes directly instead of looking up cached AST nodes, ensuring that
|
|
143
|
+
* instantiated generic types (from useGenericType) are correctly resolved.
|
|
142
144
|
*
|
|
143
|
-
* @param left - The left
|
|
144
|
-
* @param right - The right
|
|
145
|
-
* @returns An object containing the combined properties of the left and right
|
|
145
|
+
* @param left - The left Zod object schema.
|
|
146
|
+
* @param right - The right Zod object schema.
|
|
147
|
+
* @returns An object containing the combined properties of the left and right schemas.
|
|
146
148
|
*/
|
|
147
149
|
_intersectAstNodes(left, right) {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
const properties = {};
|
|
151
|
+
for (const schema of [left, right]) {
|
|
152
|
+
const shape = schema.def.shape;
|
|
153
|
+
for (const key in shape) {
|
|
154
|
+
if (zod_helpers_1.ZodHelpers.isZodPromise(shape[key]) &&
|
|
155
|
+
zod_helpers_1.ZodHelpers.isZod2XGeneric(shape[key])) {
|
|
156
|
+
const templateKey = shape[key].unwrap().def.values[0];
|
|
157
|
+
properties[key] = new core_1.ASTGenericType(templateKey);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
properties[key] = this._zodToAST(shape[key]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { properties };
|
|
153
165
|
}
|
|
154
166
|
/**
|
|
155
167
|
* Merges multiple AST definitions into a single AST object containing combined properties.
|
|
@@ -425,7 +437,7 @@ class Zod2Ast {
|
|
|
425
437
|
}
|
|
426
438
|
}
|
|
427
439
|
else {
|
|
428
|
-
const intersectedProperties = this._intersectAstNodes(
|
|
440
|
+
const intersectedProperties = this._intersectAstNodes(def.left, def.right);
|
|
429
441
|
item.newObject = new core_1.ASTObject({
|
|
430
442
|
name,
|
|
431
443
|
properties: intersectedProperties.properties,
|
|
@@ -43,5 +43,6 @@ export declare class Zod2XModel {
|
|
|
43
43
|
transpile(target: Target<"Zod2Cpp">, opt?: TargetOpt<"Zod2Cpp">, astOpt?: AstOpt): string;
|
|
44
44
|
transpile(target: Target<"Zod2Cpp17">, opt?: TargetOpt<"Zod2Cpp17">, astOpt?: AstOpt): string;
|
|
45
45
|
transpile(target: Target<"Zod2Ts">, opt?: TargetOpt<"Zod2Ts">, astOpt?: AstOpt): string;
|
|
46
|
+
transpile(target: Target<"Zod2Py">, opt?: TargetOpt<"Zod2Py">, astOpt?: AstOpt): string;
|
|
46
47
|
}
|
|
47
48
|
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Zod2Cpp17 = exports.Zod2Cpp = exports.Zod2Ts = void 0;
|
|
3
|
+
exports.Zod2Py = exports.Zod2Cpp17 = exports.Zod2Cpp = exports.Zod2Ts = void 0;
|
|
4
4
|
var runner_1 = require("./typescript/runner");
|
|
5
5
|
Object.defineProperty(exports, "Zod2Ts", { enumerable: true, get: function () { return runner_1.Zod2Ts; } });
|
|
6
6
|
var runner_2 = require("./cpp/runner");
|
|
7
7
|
Object.defineProperty(exports, "Zod2Cpp", { enumerable: true, get: function () { return runner_2.Zod2Cpp; } });
|
|
8
8
|
Object.defineProperty(exports, "Zod2Cpp17", { enumerable: true, get: function () { return runner_2.Zod2Cpp17; } });
|
|
9
|
+
var runner_3 = require("./python/runner");
|
|
10
|
+
Object.defineProperty(exports, "Zod2Py", { enumerable: true, get: function () { return runner_3.Zod2Py; } });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Python library imports for Pydantic
|
|
3
|
+
* @returns
|
|
4
|
+
*/
|
|
5
|
+
export declare function getLibs(): {
|
|
6
|
+
baseModel: string;
|
|
7
|
+
fieldImport: string;
|
|
8
|
+
aliasGenerator: string;
|
|
9
|
+
annotatedType: string;
|
|
10
|
+
typeAliasType: string;
|
|
11
|
+
genericType: string;
|
|
12
|
+
typeVarType: string;
|
|
13
|
+
enumType: string;
|
|
14
|
+
anyType: string;
|
|
15
|
+
listType: string;
|
|
16
|
+
dictType: string;
|
|
17
|
+
setType: string;
|
|
18
|
+
tupleType: string;
|
|
19
|
+
unionType: string;
|
|
20
|
+
optionalType: string;
|
|
21
|
+
literalType: string;
|
|
22
|
+
datetimeType: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLibs = getLibs;
|
|
4
|
+
/**
|
|
5
|
+
* @description Python library imports for Pydantic
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
function getLibs() {
|
|
9
|
+
return {
|
|
10
|
+
baseModel: "from pydantic import BaseModel, ConfigDict",
|
|
11
|
+
fieldImport: "from pydantic import Field",
|
|
12
|
+
aliasGenerator: "from pydantic.alias_generators import to_camel",
|
|
13
|
+
annotatedType: "from typing import Annotated",
|
|
14
|
+
typeAliasType: "from typing import TypeAlias",
|
|
15
|
+
genericType: "from typing import Generic",
|
|
16
|
+
typeVarType: "from typing import TypeVar",
|
|
17
|
+
enumType: "from enum import Enum",
|
|
18
|
+
anyType: "from typing import Any",
|
|
19
|
+
listType: "from typing import List",
|
|
20
|
+
dictType: "from typing import Dict",
|
|
21
|
+
setType: "from typing import Set",
|
|
22
|
+
tupleType: "from typing import Tuple",
|
|
23
|
+
unionType: "from typing import Union",
|
|
24
|
+
optionalType: "from typing import Optional",
|
|
25
|
+
literalType: "from typing import Literal",
|
|
26
|
+
datetimeType: "from datetime import datetime",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IZodToXOpt } from "../../core";
|
|
2
|
+
export interface IZod2PyOpt extends Omit<IZodToXOpt, "indent"> {
|
|
3
|
+
/**
|
|
4
|
+
* By default (false), structure/class property names are converted according to the target
|
|
5
|
+
* language's naming conventions. If set to true, the original property names are preserved.
|
|
6
|
+
*/
|
|
7
|
+
keepKeys?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const defaultOpts: IZod2PyOpt;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ASTAliasedTypes, ASTEnum, ASTIntersection, ASTNode, ASTObject, ASTUnion, Zod2X } from "../../core";
|
|
2
|
+
import { IZod2PyOpt } from "./options";
|
|
3
|
+
export declare class Zod2Py extends Zod2X<IZod2PyOpt> {
|
|
4
|
+
protected readonly commentKey = "#";
|
|
5
|
+
protected lib: {
|
|
6
|
+
baseModel: string;
|
|
7
|
+
fieldImport: string;
|
|
8
|
+
aliasGenerator: string;
|
|
9
|
+
annotatedType: string;
|
|
10
|
+
typeAliasType: string;
|
|
11
|
+
genericType: string;
|
|
12
|
+
typeVarType: string;
|
|
13
|
+
enumType: string;
|
|
14
|
+
anyType: string;
|
|
15
|
+
listType: string;
|
|
16
|
+
dictType: string;
|
|
17
|
+
setType: string;
|
|
18
|
+
tupleType: string;
|
|
19
|
+
unionType: string;
|
|
20
|
+
optionalType: string;
|
|
21
|
+
literalType: string;
|
|
22
|
+
datetimeType: string;
|
|
23
|
+
};
|
|
24
|
+
private baseSchemaAdded;
|
|
25
|
+
private typeVars;
|
|
26
|
+
constructor(opt?: IZod2PyOpt);
|
|
27
|
+
protected runAfter(): void;
|
|
28
|
+
protected runBefore(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Adds BaseSchema class definition if not already added.
|
|
31
|
+
* This is the base class for all Pydantic models with shared configuration.
|
|
32
|
+
*/
|
|
33
|
+
private _addBaseSchema;
|
|
34
|
+
/**
|
|
35
|
+
* Declares TypeVars that haven't been declared yet.
|
|
36
|
+
* Adds them right before their first usage.
|
|
37
|
+
* Ex: T = TypeVar('T')
|
|
38
|
+
*/
|
|
39
|
+
private _declareNewTypeVars;
|
|
40
|
+
/**
|
|
41
|
+
* Consolidates multiline imports from the same module and sorts them alphabetically.
|
|
42
|
+
* Modifies this.imports Set to contain consolidated import statements.
|
|
43
|
+
*/
|
|
44
|
+
private _consolidateImports;
|
|
45
|
+
protected addImportFromFile(filename: string, namespace: string): string;
|
|
46
|
+
protected getTypeFromExternalNamespace(namespace: string, typeName: string): string;
|
|
47
|
+
protected addExtendedType(name: string, parentNamespace: string, aliasOf: string, opt?: {
|
|
48
|
+
type?: "union" | "d-union" | "alias";
|
|
49
|
+
isInternal?: boolean;
|
|
50
|
+
templates?: string;
|
|
51
|
+
isClass?: boolean;
|
|
52
|
+
}): void;
|
|
53
|
+
protected getGenericTemplatesTranslation(data: ASTNode): string | undefined;
|
|
54
|
+
protected checkExtendedTypeInclusion(data: ASTNode, type?: "alias" | "union" | "d-union"): boolean;
|
|
55
|
+
protected getAnyType: () => string;
|
|
56
|
+
protected getBooleanType: () => string;
|
|
57
|
+
protected getDateType: () => string;
|
|
58
|
+
/** Ex: Set[TypeA] */
|
|
59
|
+
protected getSetType: (itemType: string) => string;
|
|
60
|
+
protected getStringType: () => string;
|
|
61
|
+
/** Ex: Tuple[TypeA, TypeB] */
|
|
62
|
+
protected getTupleType: (itemsType: string[]) => string;
|
|
63
|
+
/** Ex: Union[TypeA, TypeB] */
|
|
64
|
+
protected getUnionType: (itemsType: string[]) => string;
|
|
65
|
+
/** Ex: TypeA & TypeB -> intersection handling */
|
|
66
|
+
protected getIntersectionType: (itemsType: string[]) => string;
|
|
67
|
+
/** Ex: int or float depending on isInt flag */
|
|
68
|
+
protected getNumberType: (isInt: boolean, range: {
|
|
69
|
+
min?: number;
|
|
70
|
+
max?: number;
|
|
71
|
+
}) => string;
|
|
72
|
+
/** Ex: List[List[TypeA]] */
|
|
73
|
+
protected getArrayType(arrayType: string, arrayDeep: number): string;
|
|
74
|
+
/** Ex: Literal["value"] or Literal[true] or EnumName.ENUM_VALUE */
|
|
75
|
+
protected getLiteralStringType(value: string | number | boolean, parentEnumNameKey?: [string, string]): string | number;
|
|
76
|
+
/** Ex: Dict[TypeA, TypeB] */
|
|
77
|
+
protected getMapType(keyType: string, valueType: string): string;
|
|
78
|
+
/** Ex: Dict[TypeA, TypeB] */
|
|
79
|
+
protected getRecordType(keyType: string, valueType: string): string;
|
|
80
|
+
protected transpileAliasedType(data: ASTAliasedTypes): void;
|
|
81
|
+
/** Ex:
|
|
82
|
+
* class MyEnum(str, Enum):
|
|
83
|
+
* ITEM_KEY1 = "ItemValue1"
|
|
84
|
+
* ITEM_KEY2 = "ItemValue2"
|
|
85
|
+
*
|
|
86
|
+
* # Or for mixed types:
|
|
87
|
+
* class MyEnum(Enum):
|
|
88
|
+
* ITEM_KEY1 = 1
|
|
89
|
+
* ITEM_KEY2 = "ItemValue2"
|
|
90
|
+
*/
|
|
91
|
+
protected transpileEnum(data: ASTEnum): void;
|
|
92
|
+
protected transpileIntersection(data: ASTIntersection): void;
|
|
93
|
+
protected transpileStruct(data: ASTObject): void;
|
|
94
|
+
protected transpileUnion(data: ASTUnion): void;
|
|
95
|
+
/**
|
|
96
|
+
* Creates a wrapper class for a union type.
|
|
97
|
+
* Python/Pydantic needs this for proper serialization/deserialization of unions.
|
|
98
|
+
* Ex:
|
|
99
|
+
* class UnionItemWrapper(BaseSchema):
|
|
100
|
+
* data: UnionItem
|
|
101
|
+
*/
|
|
102
|
+
private _createUnionWrapper;
|
|
103
|
+
/** Ex:
|
|
104
|
+
* class MyStruct(BaseSchema):
|
|
105
|
+
* att1: TypeA
|
|
106
|
+
* att2: Optional[TypeB] = None
|
|
107
|
+
*
|
|
108
|
+
* # Or with generics:
|
|
109
|
+
* class MyGenericStruct(BaseSchema, Generic[T]):
|
|
110
|
+
* data: T
|
|
111
|
+
* */
|
|
112
|
+
private _transpileStructAsClass;
|
|
113
|
+
/** For Class attributes.
|
|
114
|
+
* Ex: attribute1: Optional[TypeA] = None */
|
|
115
|
+
private _transpileMember;
|
|
116
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Zod2Py = void 0;
|
|
7
|
+
const case_1 = __importDefault(require("case"));
|
|
8
|
+
const core_1 = require("../../core");
|
|
9
|
+
const options_1 = require("./options");
|
|
10
|
+
const libs_1 = require("./libs");
|
|
11
|
+
class Zod2Py extends core_1.Zod2X {
|
|
12
|
+
constructor(opt = {}) {
|
|
13
|
+
super(Object.assign(Object.assign({}, options_1.defaultOpts), opt));
|
|
14
|
+
this.commentKey = "#";
|
|
15
|
+
this.baseSchemaAdded = false;
|
|
16
|
+
this.typeVars = new Set();
|
|
17
|
+
this.getAnyType = () => {
|
|
18
|
+
this.imports.add(this.lib.anyType);
|
|
19
|
+
return "Any";
|
|
20
|
+
};
|
|
21
|
+
this.getBooleanType = () => "bool";
|
|
22
|
+
this.getDateType = () => {
|
|
23
|
+
this.imports.add(this.lib.datetimeType);
|
|
24
|
+
return "datetime";
|
|
25
|
+
};
|
|
26
|
+
/** Ex: Set[TypeA] */
|
|
27
|
+
this.getSetType = (itemType) => {
|
|
28
|
+
this.imports.add(this.lib.setType);
|
|
29
|
+
return `Set[${itemType}]`;
|
|
30
|
+
};
|
|
31
|
+
this.getStringType = () => "str";
|
|
32
|
+
/** Ex: Tuple[TypeA, TypeB] */
|
|
33
|
+
this.getTupleType = (itemsType) => {
|
|
34
|
+
this.imports.add(this.lib.tupleType);
|
|
35
|
+
return `Tuple[${itemsType.join(", ")}]`;
|
|
36
|
+
};
|
|
37
|
+
/** Ex: Union[TypeA, TypeB] */
|
|
38
|
+
this.getUnionType = (itemsType) => {
|
|
39
|
+
this.imports.add(this.lib.unionType);
|
|
40
|
+
return `Union[${itemsType.join(", ")}]`;
|
|
41
|
+
};
|
|
42
|
+
/** Ex: TypeA & TypeB -> intersection handling */
|
|
43
|
+
this.getIntersectionType = (itemsType) => {
|
|
44
|
+
// Python doesn't have intersection types, we'll create a new class
|
|
45
|
+
return itemsType.join(" & "); // This will be handled in transpileIntersection
|
|
46
|
+
};
|
|
47
|
+
/** Ex: int or float depending on isInt flag */
|
|
48
|
+
this.getNumberType = (isInt, range) => {
|
|
49
|
+
return isInt ? "int" : "float";
|
|
50
|
+
};
|
|
51
|
+
this.lib = (0, libs_1.getLibs)();
|
|
52
|
+
}
|
|
53
|
+
runAfter() {
|
|
54
|
+
this._consolidateImports();
|
|
55
|
+
}
|
|
56
|
+
runBefore() { }
|
|
57
|
+
/**
|
|
58
|
+
* Adds BaseSchema class definition if not already added.
|
|
59
|
+
* This is the base class for all Pydantic models with shared configuration.
|
|
60
|
+
*/
|
|
61
|
+
_addBaseSchema() {
|
|
62
|
+
if (this.baseSchemaAdded)
|
|
63
|
+
return;
|
|
64
|
+
this.baseSchemaAdded = true;
|
|
65
|
+
this.imports.add(this.lib.baseModel);
|
|
66
|
+
if (this.opt.keepKeys !== true) {
|
|
67
|
+
this.imports.add(this.lib.aliasGenerator);
|
|
68
|
+
}
|
|
69
|
+
this.push0("class BaseSchema(BaseModel):");
|
|
70
|
+
this.push1("model_config = ConfigDict(");
|
|
71
|
+
if (this.opt.keepKeys !== true) {
|
|
72
|
+
this.push2("alias_generator=to_camel,");
|
|
73
|
+
this.push2("serialize_by_alias=True,");
|
|
74
|
+
this.push2("populate_by_name=True,");
|
|
75
|
+
}
|
|
76
|
+
this.push2("use_enum_values=True");
|
|
77
|
+
this.push1(")");
|
|
78
|
+
this.push0("");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Declares TypeVars that haven't been declared yet.
|
|
82
|
+
* Adds them right before their first usage.
|
|
83
|
+
* Ex: T = TypeVar('T')
|
|
84
|
+
*/
|
|
85
|
+
_declareNewTypeVars(templates) {
|
|
86
|
+
const newTypeVars = Array.from(templates).filter((t) => !this.typeVars.has(t));
|
|
87
|
+
if (newTypeVars.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
this.imports.add(this.lib.typeVarType);
|
|
90
|
+
newTypeVars.forEach((typeVar) => {
|
|
91
|
+
this.push0(`${typeVar} = TypeVar('${typeVar}')`);
|
|
92
|
+
this.typeVars.add(typeVar);
|
|
93
|
+
});
|
|
94
|
+
this.push0("");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Consolidates multiline imports from the same module and sorts them alphabetically.
|
|
98
|
+
* Modifies this.imports Set to contain consolidated import statements.
|
|
99
|
+
*/
|
|
100
|
+
_consolidateImports() {
|
|
101
|
+
const importGroups = new Map();
|
|
102
|
+
const simpleImports = new Set();
|
|
103
|
+
// Group imports by module
|
|
104
|
+
this.imports.forEach((importStatement) => {
|
|
105
|
+
if (importStatement.startsWith("from ") && importStatement.includes(" import ")) {
|
|
106
|
+
// Parse "from module import item" format
|
|
107
|
+
const match = importStatement.match(/^from\s+(\S+)\s+import\s+(.+)$/);
|
|
108
|
+
if (match) {
|
|
109
|
+
const module = match[1];
|
|
110
|
+
const items = match[2].split(",").map((item) => item.trim());
|
|
111
|
+
if (!importGroups.has(module)) {
|
|
112
|
+
importGroups.set(module, new Set());
|
|
113
|
+
}
|
|
114
|
+
items.forEach((item) => {
|
|
115
|
+
if (item) {
|
|
116
|
+
importGroups.get(module).add(item);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (importStatement.startsWith("import ")) {
|
|
122
|
+
// Simple import statement
|
|
123
|
+
simpleImports.add(importStatement);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// Clear existing imports
|
|
127
|
+
this.imports.clear();
|
|
128
|
+
// Add consolidated "from" imports back to this.imports (sorted by module)
|
|
129
|
+
const sortedModules = Array.from(importGroups.keys()).sort();
|
|
130
|
+
sortedModules.forEach((module) => {
|
|
131
|
+
const items = Array.from(importGroups.get(module)).sort();
|
|
132
|
+
if (items.length === 1) {
|
|
133
|
+
this.imports.add(`from ${module} import ${items[0]}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.imports.add(`from ${module} import ${items.join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// Add simple imports back to this.imports (sorted)
|
|
140
|
+
const sortedSimpleImports = Array.from(simpleImports).sort();
|
|
141
|
+
sortedSimpleImports.forEach((imp) => {
|
|
142
|
+
this.imports.add(imp);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
addImportFromFile(filename, namespace) {
|
|
146
|
+
const moduleName = filename.endsWith(".py") ? filename.slice(0, -3) : filename;
|
|
147
|
+
return `import ${moduleName} as ${namespace}`;
|
|
148
|
+
}
|
|
149
|
+
getTypeFromExternalNamespace(namespace, typeName) {
|
|
150
|
+
return `${namespace}.${typeName}`;
|
|
151
|
+
}
|
|
152
|
+
addExtendedType(name, parentNamespace, aliasOf, opt) {
|
|
153
|
+
var _a;
|
|
154
|
+
const extendedType = (opt === null || opt === void 0 ? void 0 : opt.isInternal)
|
|
155
|
+
? aliasOf
|
|
156
|
+
: this.getTypeFromExternalNamespace(parentNamespace, aliasOf);
|
|
157
|
+
const templates = (_a = opt === null || opt === void 0 ? void 0 : opt.templates) !== null && _a !== void 0 ? _a : "";
|
|
158
|
+
if (opt === null || opt === void 0 ? void 0 : opt.isClass) {
|
|
159
|
+
// For classes (ASTObject, ASTIntersection), use inheritance with templates
|
|
160
|
+
this.push0(`class ${name}(${extendedType}${templates}): ...\n`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// For type aliases (primitives, unions, etc.), use TypeAlias
|
|
164
|
+
this.imports.add(this.lib.typeAliasType);
|
|
165
|
+
this.push0(`${name}: TypeAlias = ${extendedType}${templates}\n`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
getGenericTemplatesTranslation(data) {
|
|
169
|
+
if ((data instanceof core_1.ASTObject || data instanceof core_1.ASTDefinition) &&
|
|
170
|
+
data.templatesTranslation.length > 0) {
|
|
171
|
+
return ("[" +
|
|
172
|
+
data.templatesTranslation
|
|
173
|
+
.map((t) => {
|
|
174
|
+
if (this.isExternalTypeImport(t)) {
|
|
175
|
+
this.addExternalTypeImport(t);
|
|
176
|
+
return this.getTypeFromExternalNamespace(t.parentNamespace, t.aliasOf);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
return t.aliasOf;
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
.join(", ") +
|
|
183
|
+
"]");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
checkExtendedTypeInclusion(data, type) {
|
|
187
|
+
// Determine if the aliased type is a class (ASTObject or ASTIntersection with newObject)
|
|
188
|
+
const isClass = data instanceof core_1.ASTObject ||
|
|
189
|
+
(data instanceof core_1.ASTIntersection && data.newObject !== undefined);
|
|
190
|
+
if (this.isExternalTypeImport(data)) {
|
|
191
|
+
if (data.aliasOf) {
|
|
192
|
+
this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
|
|
193
|
+
type,
|
|
194
|
+
templates: this.getGenericTemplatesTranslation(data),
|
|
195
|
+
isClass,
|
|
196
|
+
});
|
|
197
|
+
this.addExternalTypeImport(data);
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
else if (data.aliasOf) {
|
|
202
|
+
this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
|
|
203
|
+
type,
|
|
204
|
+
isInternal: true,
|
|
205
|
+
templates: this.getGenericTemplatesTranslation(data),
|
|
206
|
+
isClass,
|
|
207
|
+
});
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
/** Ex: List[List[TypeA]] */
|
|
213
|
+
getArrayType(arrayType, arrayDeep) {
|
|
214
|
+
this.imports.add(this.lib.listType);
|
|
215
|
+
let output = `List[${arrayType}]`;
|
|
216
|
+
for (let i = 0; i < arrayDeep - 1; i++) {
|
|
217
|
+
output = `List[${output}]`;
|
|
218
|
+
}
|
|
219
|
+
return output;
|
|
220
|
+
}
|
|
221
|
+
/** Ex: Literal["value"] or Literal[true] or EnumName.ENUM_VALUE */
|
|
222
|
+
getLiteralStringType(value, parentEnumNameKey) {
|
|
223
|
+
if (!parentEnumNameKey)
|
|
224
|
+
this.imports.add(this.lib.literalType);
|
|
225
|
+
return ("Literal[" +
|
|
226
|
+
(parentEnumNameKey
|
|
227
|
+
? `${parentEnumNameKey[0]}.${case_1.default.constant(case_1.default.snake(parentEnumNameKey[1]))}`
|
|
228
|
+
: typeof value === "boolean"
|
|
229
|
+
? case_1.default.capital(value.toString())
|
|
230
|
+
: `${isNaN(Number(value)) ? `"${value}"` : value}`) +
|
|
231
|
+
"]");
|
|
232
|
+
}
|
|
233
|
+
/** Ex: Dict[TypeA, TypeB] */
|
|
234
|
+
getMapType(keyType, valueType) {
|
|
235
|
+
this.imports.add(this.lib.dictType);
|
|
236
|
+
return `Dict[${keyType}, ${valueType}]`;
|
|
237
|
+
}
|
|
238
|
+
/** Ex: Dict[TypeA, TypeB] */
|
|
239
|
+
getRecordType(keyType, valueType) {
|
|
240
|
+
this.imports.add(this.lib.dictType);
|
|
241
|
+
return `Dict[${keyType}, ${valueType}]`;
|
|
242
|
+
}
|
|
243
|
+
transpileAliasedType(data) {
|
|
244
|
+
if (this.checkExtendedTypeInclusion(data, "alias")) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let extendedType = undefined;
|
|
248
|
+
this.addComment(data.description);
|
|
249
|
+
if (data instanceof core_1.ASTArray) {
|
|
250
|
+
extendedType = this.getAttributeType(data.item);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
extendedType = this.getAttributeType(data);
|
|
254
|
+
}
|
|
255
|
+
if (extendedType !== undefined) {
|
|
256
|
+
this.imports.add(this.lib.typeAliasType);
|
|
257
|
+
this.push0(`${data.name}: TypeAlias = ${extendedType}\n`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Ex:
|
|
261
|
+
* class MyEnum(str, Enum):
|
|
262
|
+
* ITEM_KEY1 = "ItemValue1"
|
|
263
|
+
* ITEM_KEY2 = "ItemValue2"
|
|
264
|
+
*
|
|
265
|
+
* # Or for mixed types:
|
|
266
|
+
* class MyEnum(Enum):
|
|
267
|
+
* ITEM_KEY1 = 1
|
|
268
|
+
* ITEM_KEY2 = "ItemValue2"
|
|
269
|
+
*/
|
|
270
|
+
transpileEnum(data) {
|
|
271
|
+
if (this.checkExtendedTypeInclusion(data, "alias")) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.imports.add(this.lib.enumType);
|
|
275
|
+
this.addComment(data.description);
|
|
276
|
+
// Check if all values are strings
|
|
277
|
+
const allStrings = data.values.every(([, value]) => typeof value === "string");
|
|
278
|
+
const enumParent = allStrings ? "(str, Enum)" : "(Enum)";
|
|
279
|
+
this.push0(`class ${data.name}${enumParent}:`);
|
|
280
|
+
data.values.forEach(([key, value]) => {
|
|
281
|
+
const keyName = case_1.default.constant(case_1.default.snake(key));
|
|
282
|
+
const enumValue = typeof value === "string" ? `"${value}"` : `${value}`;
|
|
283
|
+
this.push1(`${keyName} = ${enumValue}`);
|
|
284
|
+
});
|
|
285
|
+
this.push0("");
|
|
286
|
+
}
|
|
287
|
+
transpileIntersection(data) {
|
|
288
|
+
if (this.checkExtendedTypeInclusion(data)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this._addBaseSchema();
|
|
292
|
+
this.addComment(data.description);
|
|
293
|
+
// Use multiple inheritance like C++ for intersections
|
|
294
|
+
const leftType = this.getAttributeType(data.left);
|
|
295
|
+
const rightType = this.getAttributeType(data.right);
|
|
296
|
+
this.push0(`class ${data.name}(${leftType}, ${rightType}): ...\n`);
|
|
297
|
+
}
|
|
298
|
+
transpileStruct(data) {
|
|
299
|
+
if (this.checkExtendedTypeInclusion(data)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this._addBaseSchema();
|
|
303
|
+
if (data.templates.size > 0) {
|
|
304
|
+
this._declareNewTypeVars(data.templates);
|
|
305
|
+
}
|
|
306
|
+
this.addComment(data.description);
|
|
307
|
+
this._transpileStructAsClass(data);
|
|
308
|
+
}
|
|
309
|
+
transpileUnion(data) {
|
|
310
|
+
if (this.checkExtendedTypeInclusion(data, data.discriminantKey === undefined ? "union" : "d-union")) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Python uses Union type aliases (like C++), not merged classes
|
|
314
|
+
this._addBaseSchema();
|
|
315
|
+
this.addComment(data.description);
|
|
316
|
+
const attributesTypes = data.options.map(this.getAttributeType.bind(this));
|
|
317
|
+
// For discriminated unions, use Annotated with Field discriminator
|
|
318
|
+
if (data.discriminantKey) {
|
|
319
|
+
this.imports.add(this.lib.annotatedType);
|
|
320
|
+
this.imports.add(this.lib.fieldImport);
|
|
321
|
+
this.push0(`${data.name} = Annotated[`);
|
|
322
|
+
this.push1(`${this.getUnionType(attributesTypes)},`);
|
|
323
|
+
this.push1(`Field(discriminator='${data.discriminantKey}')`);
|
|
324
|
+
this.push0(`]\n`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
this.push0(`${data.name} = ${this.getUnionType(attributesTypes)}\n`);
|
|
328
|
+
}
|
|
329
|
+
this._createUnionWrapper(data.name);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Creates a wrapper class for a union type.
|
|
333
|
+
* Python/Pydantic needs this for proper serialization/deserialization of unions.
|
|
334
|
+
* Ex:
|
|
335
|
+
* class UnionItemWrapper(BaseSchema):
|
|
336
|
+
* data: UnionItem
|
|
337
|
+
*/
|
|
338
|
+
_createUnionWrapper(unionName) {
|
|
339
|
+
const wrapperName = `${unionName}Wrapper`;
|
|
340
|
+
this.push0(`class ${wrapperName}(BaseSchema):`);
|
|
341
|
+
this.push1(`data: ${unionName}`);
|
|
342
|
+
this.push0("");
|
|
343
|
+
}
|
|
344
|
+
/** Ex:
|
|
345
|
+
* class MyStruct(BaseSchema):
|
|
346
|
+
* att1: TypeA
|
|
347
|
+
* att2: Optional[TypeB] = None
|
|
348
|
+
*
|
|
349
|
+
* # Or with generics:
|
|
350
|
+
* class MyGenericStruct(BaseSchema, Generic[T]):
|
|
351
|
+
* data: T
|
|
352
|
+
* */
|
|
353
|
+
_transpileStructAsClass(data) {
|
|
354
|
+
// Handle generic templates
|
|
355
|
+
let baseClasses = "BaseSchema";
|
|
356
|
+
if (data.templates.size > 0) {
|
|
357
|
+
const templates = Array.from(data.templates);
|
|
358
|
+
this.imports.add(this.lib.genericType);
|
|
359
|
+
baseClasses += `, Generic[${templates.join(", ")}]`;
|
|
360
|
+
}
|
|
361
|
+
this.push0(`class ${data.name}(${baseClasses}):`);
|
|
362
|
+
const hasProperties = Object.keys(data.properties).length > 0;
|
|
363
|
+
if (!hasProperties) {
|
|
364
|
+
this.push1("pass");
|
|
365
|
+
this.push0("");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Generate fields
|
|
369
|
+
for (const [key, value] of Object.entries(data.properties)) {
|
|
370
|
+
const keyName = this.opt.keepKeys === true ? key : case_1.default.snake(key);
|
|
371
|
+
this._transpileMember(keyName, value);
|
|
372
|
+
}
|
|
373
|
+
this.push0("");
|
|
374
|
+
}
|
|
375
|
+
/** For Class attributes.
|
|
376
|
+
* Ex: attribute1: Optional[TypeA] = None */
|
|
377
|
+
_transpileMember(memberName, memberNode) {
|
|
378
|
+
const pythonType = this.getAttributeType(memberNode);
|
|
379
|
+
const isOptional = memberNode.isOptional || memberNode.isNullable;
|
|
380
|
+
let typeAnnotation;
|
|
381
|
+
if (isOptional) {
|
|
382
|
+
this.imports.add(this.lib.optionalType);
|
|
383
|
+
typeAnnotation = pythonType.startsWith("Optional[")
|
|
384
|
+
? pythonType
|
|
385
|
+
: `Optional[${pythonType}]`;
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
typeAnnotation = pythonType;
|
|
389
|
+
}
|
|
390
|
+
if (memberNode.description && !memberNode.name && !this.isTranspilerable(memberNode)) {
|
|
391
|
+
this.addComment(memberNode.description, `\n${this.indent[1]}`);
|
|
392
|
+
}
|
|
393
|
+
const defaultValue = isOptional ? " = None" : "";
|
|
394
|
+
this.push1(`${memberName}: ${typeAnnotation}${defaultValue}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
exports.Zod2Py = Zod2Py;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zod-to-x",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Multi language types generation from Zod schemas.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"format:check": "prettier --check .",
|
|
14
14
|
"test": "find ./test -name \"err-*\" -delete && vitest --run",
|
|
15
15
|
"test:cpp": "bash ./test/test_zod2cpp/test_cpp.sh",
|
|
16
|
+
"test:py": "bash ./test/test_zod2py/test_py.sh",
|
|
17
|
+
"test:all": "npm run test:cpp && npm run test:py",
|
|
16
18
|
"ts-run": "ts-node -r tsconfig-paths/register",
|
|
17
19
|
"prepare": "husky"
|
|
18
20
|
},
|