@figulus/schema 0.5.0-alpha-dev-2 → 0.5.0-alpha-dev-4

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.
Files changed (141) hide show
  1. package/.openapi-meta/generator/generation-mode.ts +41 -0
  2. package/.openapi-meta/generator/output/cleaned-src-dir.ts +40 -0
  3. package/{scripts/fix-openapi-3.1.ts → .openapi-meta/generator/output/lib/fix-generated-openapi.ts} +3 -84
  4. package/.openapi-meta/generator/output/openapi-file.ts +141 -0
  5. package/.openapi-meta/generator/output/openapi-meta-dir.ts +214 -0
  6. package/.openapi-meta/generator/output/output-dir.ts +16 -0
  7. package/.openapi-meta/generator/output/output-file.ts +33 -0
  8. package/.openapi-meta/generator/src-file.ts +239 -0
  9. package/.openapi-meta/index.ts +3 -3
  10. package/dist/core/entities/figspec.d.ts.map +1 -1
  11. package/dist/core/entities/figspec.js +9 -11
  12. package/dist/core/entities/figspec.js.map +1 -1
  13. package/dist/core/entities/figstack.d.ts.map +1 -1
  14. package/dist/core/entities/figstack.js +12 -14
  15. package/dist/core/entities/figstack.js.map +1 -1
  16. package/dist/core/execute.d.ts.map +1 -1
  17. package/dist/core/execute.js +10 -12
  18. package/dist/core/execute.js.map +1 -1
  19. package/dist/core/generic.d.ts.map +1 -1
  20. package/dist/core/generic.js +11 -13
  21. package/dist/core/generic.js.map +1 -1
  22. package/dist/core/primitives.d.ts.map +1 -1
  23. package/dist/core/primitives.js +1 -7
  24. package/dist/core/primitives.js.map +1 -1
  25. package/dist/core/volumes.d.ts.map +1 -1
  26. package/dist/core/volumes.js +21 -23
  27. package/dist/core/volumes.js.map +1 -1
  28. package/dist/engine/deprecated/run-request.d.ts.map +1 -1
  29. package/dist/engine/deprecated/run-request.js +14 -16
  30. package/dist/engine/deprecated/run-request.js.map +1 -1
  31. package/dist/engine/dry-run.d.ts.map +1 -1
  32. package/dist/engine/dry-run.js +1 -3
  33. package/dist/engine/dry-run.js.map +1 -1
  34. package/dist/engine/health.d.ts.map +1 -1
  35. package/dist/engine/health.js +3 -5
  36. package/dist/engine/health.js.map +1 -1
  37. package/dist/engine/images.d.ts.map +1 -1
  38. package/dist/engine/images.js +17 -19
  39. package/dist/engine/images.js.map +1 -1
  40. package/dist/engine/paths/containers.d.ts +410 -7
  41. package/dist/engine/paths/containers.d.ts.map +1 -1
  42. package/dist/engine/paths/containers.js +0 -2
  43. package/dist/engine/paths/containers.js.map +1 -1
  44. package/dist/engine/paths/dry-run.d.ts +86 -2
  45. package/dist/engine/paths/dry-run.d.ts.map +1 -1
  46. package/dist/engine/paths/dry-run.js.map +1 -1
  47. package/dist/engine/paths/health.d.ts +48 -2
  48. package/dist/engine/paths/health.d.ts.map +1 -1
  49. package/dist/engine/paths/health.js.map +1 -1
  50. package/dist/engine/paths/images.d.ts +170 -4
  51. package/dist/engine/paths/images.d.ts.map +1 -1
  52. package/dist/engine/paths/images.js +0 -2
  53. package/dist/engine/paths/images.js.map +1 -1
  54. package/dist/engine/paths/sessions.d.ts +728 -9
  55. package/dist/engine/paths/sessions.d.ts.map +1 -1
  56. package/dist/engine/paths/sessions.js +0 -2
  57. package/dist/engine/paths/sessions.js.map +1 -1
  58. package/dist/engine/paths/stacks.d.ts +275 -5
  59. package/dist/engine/paths/stacks.d.ts.map +1 -1
  60. package/dist/engine/paths/stacks.js.map +1 -1
  61. package/dist/engine/paths/volumes.d.ts +203 -5
  62. package/dist/engine/paths/volumes.d.ts.map +1 -1
  63. package/dist/engine/paths/volumes.js.map +1 -1
  64. package/dist/engine/response.d.ts.map +1 -1
  65. package/dist/engine/response.js +4 -6
  66. package/dist/engine/response.js.map +1 -1
  67. package/dist/engine/sessions.d.ts.map +1 -1
  68. package/dist/engine/sessions.js +41 -43
  69. package/dist/engine/sessions.js.map +1 -1
  70. package/dist/engine/volumes.d.ts.map +1 -1
  71. package/dist/engine/volumes.js +11 -13
  72. package/dist/engine/volumes.js.map +1 -1
  73. package/dist/registry/json-schema.d.ts +3 -3
  74. package/dist/registry/json-schema.d.ts.map +1 -1
  75. package/dist/registry/json-schema.js.map +1 -1
  76. package/dist/registry/metadata.d.ts.map +1 -1
  77. package/dist/registry/metadata.js +10 -13
  78. package/dist/registry/metadata.js.map +1 -1
  79. package/package.json +10 -6
  80. package/scripts/generator.ts +92 -0
  81. package/{src → src-raw}/core/primitives.ts +1 -4
  82. package/{src → src-raw}/registry/metadata.ts +1 -2
  83. package/tsconfig.build.json +1 -1
  84. package/tsconfig.json +1 -1
  85. package/dist/core/entities/index.d.ts +0 -3
  86. package/dist/core/entities/index.d.ts.map +0 -1
  87. package/dist/core/entities/index.js +0 -3
  88. package/dist/core/entities/index.js.map +0 -1
  89. package/dist/core/index.d.ts +0 -6
  90. package/dist/core/index.d.ts.map +0 -1
  91. package/dist/core/index.js +0 -6
  92. package/dist/core/index.js.map +0 -1
  93. package/dist/engine/deprecated/index.d.ts +0 -2
  94. package/dist/engine/deprecated/index.d.ts.map +0 -1
  95. package/dist/engine/deprecated/index.js +0 -2
  96. package/dist/engine/deprecated/index.js.map +0 -1
  97. package/dist/engine/index.d.ts +0 -9
  98. package/dist/engine/index.d.ts.map +0 -1
  99. package/dist/engine/index.js +0 -9
  100. package/dist/engine/index.js.map +0 -1
  101. package/dist/engine/paths/index.d.ts +0 -8
  102. package/dist/engine/paths/index.d.ts.map +0 -1
  103. package/dist/engine/paths/index.js +0 -8
  104. package/dist/engine/paths/index.js.map +0 -1
  105. package/dist/index.d.ts +0 -3
  106. package/dist/index.d.ts.map +0 -1
  107. package/dist/index.js +0 -3
  108. package/dist/index.js.map +0 -1
  109. package/dist/registry/index.d.ts +0 -3
  110. package/dist/registry/index.d.ts.map +0 -1
  111. package/dist/registry/index.js +0 -3
  112. package/dist/registry/index.js.map +0 -1
  113. package/scripts/generate-openapi-data.ts +0 -51
  114. package/scripts/generate-openapi-meta.ts +0 -540
  115. /package/{src → src-raw}/core/entities/figspec.ts +0 -0
  116. /package/{src → src-raw}/core/entities/figstack.ts +0 -0
  117. /package/{src → src-raw}/core/entities/index.ts +0 -0
  118. /package/{src → src-raw}/core/execute.ts +0 -0
  119. /package/{src → src-raw}/core/generic.ts +0 -0
  120. /package/{src → src-raw}/core/index.ts +0 -0
  121. /package/{src → src-raw}/core/volumes.ts +0 -0
  122. /package/{src → src-raw}/engine/deprecated/index.ts +0 -0
  123. /package/{src → src-raw}/engine/deprecated/run-request.ts +0 -0
  124. /package/{src → src-raw}/engine/dry-run.ts +0 -0
  125. /package/{src → src-raw}/engine/health.ts +0 -0
  126. /package/{src → src-raw}/engine/images.ts +0 -0
  127. /package/{src → src-raw}/engine/index.ts +0 -0
  128. /package/{src → src-raw}/engine/paths/containers.ts +0 -0
  129. /package/{src → src-raw}/engine/paths/dry-run.ts +0 -0
  130. /package/{src → src-raw}/engine/paths/health.ts +0 -0
  131. /package/{src → src-raw}/engine/paths/images.ts +0 -0
  132. /package/{src → src-raw}/engine/paths/index.ts +0 -0
  133. /package/{src → src-raw}/engine/paths/sessions.ts +0 -0
  134. /package/{src → src-raw}/engine/paths/stacks.ts +0 -0
  135. /package/{src → src-raw}/engine/paths/volumes.ts +0 -0
  136. /package/{src → src-raw}/engine/response.ts +0 -0
  137. /package/{src → src-raw}/engine/sessions.ts +0 -0
  138. /package/{src → src-raw}/engine/volumes.ts +0 -0
  139. /package/{src → src-raw}/index.ts +0 -0
  140. /package/{src → src-raw}/registry/index.ts +0 -0
  141. /package/{src → src-raw}/registry/json-schema.ts +0 -0
@@ -0,0 +1,41 @@
1
+ import z from "zod";
2
+
3
+ export const generationModeIdSchema = z.enum(["openapi-meta", "clean-src"]);
4
+ export type GenerationModeId = z.infer<typeof generationModeIdSchema>;
5
+
6
+ interface GenerationModeData {
7
+ topLevelOutputDir: string;
8
+ successMsg: string;
9
+ failureMsg: string;
10
+ }
11
+
12
+ const generationModeMappings: { [Id in GenerationModeId]: GenerationModeData } = {
13
+ "openapi-meta": {
14
+ topLevelOutputDir: ".openapi-meta/generated/",
15
+ successMsg: "✅ Generated OpenAPI spec at",
16
+ failureMsg: "❌ Failed to generate OpenAPI spec",
17
+ },
18
+ "clean-src": {
19
+ topLevelOutputDir: "src-clean/",
20
+ successMsg: "✅ Generated clean src",
21
+ failureMsg: "❌ Failed to generate clean src",
22
+ },
23
+ }
24
+
25
+ export class GenerationMode {
26
+ private data: GenerationModeData;
27
+
28
+ constructor(public id: GenerationModeId) {
29
+ this.data = generationModeMappings[this.id];
30
+ }
31
+
32
+ public getTopLevelOutputDir() {
33
+ return this.data.topLevelOutputDir;
34
+ }
35
+
36
+ public getCompletionMsg(success: boolean, args?: any[]) {
37
+ const msg = success ? this.data.successMsg : this.data.failureMsg;
38
+ const argsStr = args ? ": " + args.join(" ") : "";
39
+ return msg + argsStr;
40
+ }
41
+ }
@@ -0,0 +1,40 @@
1
+ import { OutputFile } from "./output-file.js";
2
+ import { GenerationMode } from "../generation-mode.js";
3
+ import { OutputDir } from "./output-dir.js";
4
+ import { allSrcFiles, SrcFile } from "../src-file.js";
5
+
6
+ export class CleanedSrcDir extends OutputDir {
7
+ mode: GenerationMode;
8
+ private srcFiles: SrcFile[];
9
+
10
+ constructor() {
11
+ super();
12
+ this.mode = new GenerationMode("clean-src");
13
+ this.srcFiles = allSrcFiles;
14
+ }
15
+
16
+ private generateCleanedSrcFile(file: SrcFile) {
17
+ const noOAPIImports = file.getInputFileLines().filter((l) =>
18
+ !l.includes("extendZodWithOpenApi(z)") &&
19
+ !l.includes(`from "@asteasolutions/zod-to-openapi"`)
20
+ );
21
+ const noGaps = noOAPIImports.filter((l, i) => l.length > 0 || noOAPIImports[i-1].length > 0);
22
+ const noRefs = noGaps.map((l) => l.replace(/\.openapi\(.*\)/, ""));
23
+ const noTypes = noRefs.map((l) => l.replace(": RouteConfig", ""));
24
+ return noTypes.join("\n");
25
+ }
26
+
27
+ public generateFiles(dryRun?: boolean) {
28
+ this.recreateDir();
29
+
30
+ for(const file of this.srcFiles) {
31
+ const outputFile = new OutputFile(file, this.mode);
32
+ const content = this.generateCleanedSrcFile(file);
33
+
34
+ if(!dryRun) {
35
+ outputFile.write(content);
36
+ console.log(`Wrote ${file.id} to ${outputFile.getPath().fullPath}`);
37
+ } else console.log(content);
38
+ }
39
+ }
40
+ }
@@ -1,43 +1,6 @@
1
- import { parse, stringify } from "yaml";
2
- import { readFileSync, writeFileSync } from "fs";
3
-
4
- const inputPath = process.argv[2];
5
- const outputPath = process.argv[3] ?? inputPath;
6
-
7
- if (!inputPath) {
8
- console.error("Usage: npx tsx fix-nullable.ts <input.yaml> [output.yaml]");
9
- process.exit(1);
10
- }
11
-
12
- /**
13
- * Recursively walks a parsed YAML/JSON object and fixes OpenAPI 3.1 schema issues
14
- * produced by @asteasolutions/zod-to-openapi when generating from Zod schemas.
15
- *
16
- * Fixes:
17
- *
18
- * 1. Standalone nullable member in anyOf/oneOf/allOf:
19
- * { anyOf: [..., { nullable: true }] }
20
- * → { anyOf: [..., { type: "null" }] }
21
- *
22
- * 2. Sibling nullable on a schema object:
23
- * { type: "string", nullable: true }
24
- * → { anyOf: [{ type: "string" }, { type: "null" }] }
25
- *
26
- * 3. OpenAPI 3.0-style exclusiveMinimum as boolean:
27
- * { minimum: 0, exclusiveMinimum: true }
28
- * → { exclusiveMinimum: 0 }
29
- *
30
- * 4. Zod unions generated as anyOf when they should be oneOf:
31
- * { anyOf: [{ $ref: ... }, { $ref: ... }, { type: "string" }] }
32
- * → { oneOf: [{ $ref: ... }, { $ref: ... }, { type: "string" }] }
33
- * (when items are mutually exclusive: all have different types, or all are refs, etc)
34
- *
35
- * All are produced by @asteasolutions/zod-to-openapi when handling z.null(),
36
- * z.nullable(), z.union(), or numeric constraints in Zod schemas.
37
- */
38
- function fixNullable(node: unknown): unknown {
1
+ export function fixGeneratedOpenAPI(node: unknown): unknown {
39
2
  if (Array.isArray(node)) {
40
- const fixed = node.map(fixNullable);
3
+ const fixed = node.map(fixGeneratedOpenAPI);
41
4
 
42
5
  // Pattern 1: replace { nullable: true } entries in anyOf/oneOf/allOf arrays
43
6
  // with { type: "null" }
@@ -61,7 +24,7 @@ function fixNullable(node: unknown): unknown {
61
24
  // Recurse into all child values first
62
25
  const result: Record<string, unknown> = {};
63
26
  for (const [key, value] of Object.entries(obj)) {
64
- result[key] = fixNullable(value);
27
+ result[key] = fixGeneratedOpenAPI(value);
65
28
  }
66
29
 
67
30
  // Pattern 2: sibling nullable: true on a schema with a type field
@@ -137,47 +100,3 @@ function fixNullable(node: unknown): unknown {
137
100
 
138
101
  return node;
139
102
  }
140
-
141
- const raw = readFileSync(inputPath, "utf-8");
142
- const parsed = parse(raw);
143
- const fixed = fixNullable(parsed);
144
- const output = stringify(fixed, { lineWidth: 0 });
145
-
146
- writeFileSync(outputPath, output, "utf-8");
147
-
148
- const inputLines = raw.split("\n").length;
149
- const outputLines = output.split("\n").length;
150
-
151
- // Count how many replacements were made by diffing nullable occurrences
152
- const nullableCountBefore = (raw.match(/nullable:\s*true/g) ?? []).length;
153
- const nullableCountAfter = (output.match(/nullable:\s*true/g) ?? []).length;
154
- const fixed_count = nullableCountBefore - nullableCountAfter;
155
-
156
- const exclusiveMinBefore = (raw.match(/exclusiveMinimum:\s*true/g) ?? []).length;
157
- const exclusiveMinAfter = (output.match(/exclusiveMinimum:\s*true/g) ?? []).length;
158
- const exclusiveMinFixed = exclusiveMinBefore - exclusiveMinAfter;
159
-
160
- const anyOfBefore = (raw.match(/anyOf:/g) ?? []).length;
161
- const anyOfAfter = (output.match(/anyOf:/g) ?? []).length;
162
- const anyOfToOneOfFixed = anyOfBefore - anyOfAfter;
163
-
164
- console.log(`Input: ${inputPath} (${inputLines} lines)`);
165
- console.log(`Output: ${outputPath} (${outputLines} lines)`);
166
- console.log(`Fixed: ${fixed_count} nullable: true → type: "null" replacements`);
167
- console.log(`Fixed: ${exclusiveMinFixed} exclusiveMinimum: true → numeric replacements`);
168
- console.log(`Fixed: ${anyOfToOneOfFixed} anyOf → oneOf conversions`);
169
- if (nullableCountAfter > 0) {
170
- console.warn(
171
- `Warning: ${nullableCountAfter} nullable: true occurrences remain — manual review needed`
172
- );
173
- }
174
- if (exclusiveMinAfter > 0) {
175
- console.warn(
176
- `Warning: ${exclusiveMinAfter} exclusiveMinimum: true occurrences remain — manual review needed`
177
- );
178
- }
179
- if (anyOfAfter > 0) {
180
- console.log(
181
- `ℹ️ ${anyOfAfter} anyOf schemas remain (may be intentional for overlapping unions)`
182
- );
183
- }
@@ -0,0 +1,141 @@
1
+ import { OpenApiGeneratorV3, OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
2
+ import { OpenAPIObjectConfig } from "@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator";
3
+ import { readFileSync, writeFileSync } from "fs";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import YAML from "yaml";
7
+ import z from "zod";
8
+ import { registerAll } from "../../index.js";
9
+ import { OpenAPIMetaDir } from "./openapi-meta-dir.js";
10
+ import { fixGeneratedOpenAPI } from "./lib/fix-generated-openapi.js";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const outputDir = path.join(__dirname, "..", "..", "..", "..", "..", "api");
15
+
16
+ export const openApiFileIdSchema = z.enum(["core", "registry"]);
17
+ export type OpenAPIFileId = z.infer<typeof openApiFileIdSchema>;
18
+
19
+ function getPackageVersion() {
20
+ const packageJsonPath = path.resolve(
21
+ path.dirname(fileURLToPath(import.meta.url)),
22
+ "..", "..", "..",
23
+ "package.json"
24
+ );
25
+ const packageJsonData = readFileSync(packageJsonPath);
26
+ const parsed = z.object({ version: z.string() }).parse(
27
+ JSON.parse(packageJsonData.toString())
28
+ );
29
+ return parsed.version;
30
+ }
31
+
32
+ interface OpenAPIFileData {
33
+ fileName: string;
34
+ document: OpenAPIObjectConfig;
35
+ }
36
+
37
+ const openApiFileMappings: {
38
+ [Id in OpenAPIFileId]: OpenAPIFileData
39
+ } = {
40
+ core: {
41
+ fileName: "openapi-new",
42
+ document: {
43
+ openapi: "3.1.0",
44
+ info: {
45
+ title: "Figulus Schema",
46
+ version: getPackageVersion(),
47
+ description:
48
+ "Schemas and types for the Figulus ecosystem",
49
+ contact: {
50
+ name: "Figulus Project",
51
+ url: "https://github.com/figulusproject/figulus",
52
+ },
53
+ },
54
+ servers: [
55
+ {
56
+ url: "http://localhost",
57
+ description: "Local Unix socket (connect via /run/figulus/figulus.sock)",
58
+ },
59
+ ],
60
+ tags: [
61
+ {
62
+ name: "health",
63
+ description: "Engine and Docker health checks",
64
+ },
65
+ {
66
+ name: "containers",
67
+ description: "Container job execution and lifecycle management",
68
+ },
69
+ {
70
+ name: "images",
71
+ description: "Docker image build and garbage collection",
72
+ },
73
+ {
74
+ name: "volumes",
75
+ description: "Docker volume management",
76
+ },
77
+ {
78
+ name: "sessions",
79
+ description: "Persistent session container management",
80
+ },
81
+ {
82
+ name: "stacks",
83
+ description: "FigStack multi-container orchestration",
84
+ },
85
+ ]
86
+ }
87
+ },
88
+ registry: {
89
+ fileName: "openapi-registry",
90
+ document: {
91
+ openapi: "3.1.0",
92
+ info: {
93
+ title: "Figulus Registry Schema",
94
+ version: getPackageVersion(),
95
+ description:
96
+ "Schemas and types for the Figulus Registry",
97
+ contact: {
98
+ name: "Figulus Project",
99
+ url: "https://github.com/figulusproject/figulus",
100
+ },
101
+ },
102
+ servers: [
103
+ {
104
+ url: "https://github.com/figulusproject/registry",
105
+ description: "Official GitHub repository for the registry",
106
+ },
107
+ ],
108
+ }
109
+ }
110
+ }
111
+
112
+ export class OpenAPIFile {
113
+ private data: OpenAPIFileData;
114
+
115
+ constructor(public id: OpenAPIFileId) {
116
+ this.data = openApiFileMappings[this.id];
117
+ }
118
+
119
+ public getPath() {
120
+ return path.join(outputDir, `${this.data.fileName}.yaml`);
121
+ }
122
+
123
+ public async generate(dryRun?: boolean) {
124
+ const dir = new OpenAPIMetaDir(this);
125
+ dir.generateFiles();
126
+
127
+ const registry = await registerAll(new OpenAPIRegistry());
128
+ const generator = new OpenApiGeneratorV3(registry.definitions);
129
+ const openApiDoc = generator.generateDocument(this.data.document);
130
+
131
+ if (openApiDoc.components?.schemas) {
132
+ openApiDoc.components.schemas = Object.fromEntries(
133
+ Object.entries(openApiDoc.components.schemas).sort(([a], [b]) => a.localeCompare(b))
134
+ );
135
+ }
136
+
137
+ const yamlContent = YAML.stringify(fixGeneratedOpenAPI(openApiDoc), { lineWidth: 0 });
138
+ if(!dryRun) writeFileSync(this.getPath(), yamlContent);
139
+ else console.log(yamlContent);
140
+ }
141
+ }
@@ -0,0 +1,214 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { OpenAPIMappedSchemaIterable } from "../../index.js";
4
+ import { OutputFile } from "./output-file.js";
5
+ import { GenerationMode } from "../generation-mode.js";
6
+ import { OpenAPIFile } from "./openapi-file.js";
7
+ import { OutputDir } from "./output-dir.js";
8
+ import { allSrcFiles, srcDir, SrcFile } from "../src-file.js";
9
+
10
+ type IterableBase = OpenAPIMappedSchemaIterable<string>;
11
+
12
+ class OpenAPIMetaDirFile {
13
+ private srcLines: string[];
14
+ private refs: IterableBase;
15
+ private isSchema: boolean;
16
+ private iterableValueType: string;
17
+ public names: {
18
+ keyConst: string;
19
+ keyType: string;
20
+ mapping: string;
21
+ iterable: string;
22
+ };
23
+
24
+ constructor(public srcFile: SrcFile) {
25
+ this.srcLines = this.srcFile.getInputFileLines();
26
+
27
+ this.isSchema = this.srcFile.getType() === "schema";
28
+ this.iterableValueType = this.isSchema ? "OpenAPIMappedSchemaValue" : "RouteConfig";
29
+
30
+ const camel = this.srcFile.getName("camel");
31
+ const pascal = this.srcFile.getName("pascal");
32
+ const oapi = "OpenAPI";
33
+ this.names = {
34
+ keyConst: camel + oapi + "KeySchema",
35
+ keyType: pascal + oapi + "Key",
36
+ mapping: camel + oapi + "Mappings",
37
+ iterable: camel + oapi + "Iterable",
38
+ };
39
+
40
+ this.refs = this.getRefs();
41
+ }
42
+
43
+ private getIterableSchemas(): IterableBase {
44
+ const output: IterableBase = [];
45
+ let currentConst: string = "";
46
+
47
+ for (const l of this.srcLines) {
48
+ const isConst = l.includes("export const");
49
+ const isOpenAPIRef = /\.openapi\("[^"]+"\)/.test(l);
50
+ if (!isConst && !isOpenAPIRef) continue;
51
+
52
+ const matchAndSlice = (matchPhraseStart: string, matchPhraseEnd: string) => {
53
+ const start = l.slice(l.indexOf(matchPhraseStart) + matchPhraseStart.length);
54
+ const end = start.slice(0, start.indexOf(matchPhraseEnd) + matchPhraseEnd.length - 1);
55
+ return end;
56
+ }
57
+
58
+ if (isConst) {
59
+ const start = l.slice(l.indexOf("export const ") + "export const ".length);
60
+ const match = start.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
61
+ currentConst = match ? match[1] : "";
62
+ continue;
63
+ }
64
+
65
+ output.push({ key: matchAndSlice(`openapi("`, `"`), obj: currentConst });
66
+ }
67
+
68
+ return output;
69
+ }
70
+
71
+ private getIterablePaths(): IterableBase {
72
+ const output: IterableBase = [];
73
+
74
+ const matchPhraseStart = "export const";
75
+ const matchPhraseEnd = ": RouteConfig";
76
+
77
+ const constNames = this.srcLines.filter((l) => l.startsWith(matchPhraseStart)).map((l) => {
78
+ const start = l.slice(l.indexOf(matchPhraseStart) + matchPhraseStart.length + 1);
79
+ const end = start.slice(0, start.indexOf(matchPhraseEnd));
80
+ return end;
81
+ });
82
+
83
+ constNames.forEach((name) => {
84
+ output.push({ key: name, obj: name });
85
+ });
86
+
87
+ return output;
88
+ }
89
+
90
+ private getRefs(): IterableBase {
91
+ if (this.isSchema)
92
+ return this.getIterableSchemas();
93
+ return this.getIterablePaths();
94
+ }
95
+
96
+ private generateImports() {
97
+ const dir = this.srcFile.getInputDir();
98
+ const dirPath = dir ? `${dir}/` : "";
99
+ const depth = dir ? (dir.split("/").length + 2) : 2;
100
+ const relativePrefix = "../".repeat(depth);
101
+
102
+ return [
103
+ `import z from "zod";`,
104
+ `import { OpenAPIMappedSchemaIterable, OpenAPIMappedSchemaValue } from "${"../".repeat(depth - 1)}index.js";`,
105
+ this.isSchema ? `` : `import { RouteConfig } from "@asteasolutions/zod-to-openapi";`,
106
+ `import { ${this.refs.map((v) => v.obj).join(", ")} } from "${relativePrefix + srcDir + dirPath + this.srcFile.getInputFileName()}.js";`,
107
+ ].filter(Boolean).join("\n");
108
+ }
109
+
110
+ private generateKeySchema() {
111
+ const prefix = `export const ${this.names.keyConst} = z.enum([\n`;
112
+ const suffix = `\n]);`;
113
+ return prefix + this.refs.map((r) => ` "${r.key}",`).join("\n") + suffix;
114
+ }
115
+
116
+ private generateKeyType() {
117
+ return `type ${this.names.keyType} = z.infer<typeof ${this.names.keyConst}>;`;
118
+ }
119
+
120
+ private generateMappings() {
121
+ const prefix = `const ${this.names.mapping}: { [key in ${this.names.keyType}]: ${this.iterableValueType} } = {\n`;
122
+ const suffix = `\n};`;
123
+ return prefix + this.refs.map((r) => ` ${r.key}: ${r.obj},`).join("\n") + suffix;
124
+ }
125
+
126
+ private generateIterable() {
127
+ return `export const ${this.names.iterable}: OpenAPIMappedSchemaIterable<${this.iterableValueType}> = ${this.names.keyConst}.options.map((key) => ({\n key,\n obj: ${this.names.mapping}[key],\n}));`
128
+ }
129
+
130
+ public generateContent() {
131
+ const imports = this.generateImports();
132
+ const keySchema = this.generateKeySchema();
133
+ const keyType = this.generateKeyType();
134
+ const mappings = this.generateMappings();
135
+ const iterable = this.generateIterable();
136
+
137
+ return imports + "\n\n" +
138
+ keySchema + "\n" +
139
+ keyType + "\n\n" +
140
+ mappings + "\n\n" +
141
+ iterable + "\n";
142
+ }
143
+ }
144
+
145
+ export class OpenAPIMetaDir extends OutputDir {
146
+ mode: GenerationMode;
147
+
148
+ private srcFiles: SrcFile[];
149
+ private generatedFiles: {
150
+ metaDirFile: OpenAPIMetaDirFile,
151
+ outputFile: OutputFile
152
+ }[];
153
+
154
+ constructor(private openAPIFile: OpenAPIFile) {
155
+ super();
156
+ this.mode = new GenerationMode("openapi-meta");
157
+ this.srcFiles = allSrcFiles.filter((f) => f.isInOpenAPIFile(this.openAPIFile.id));
158
+ this.generatedFiles = this.srcFiles.map((f) => ({
159
+ metaDirFile: new OpenAPIMetaDirFile(f),
160
+ outputFile: new OutputFile(f, this.mode),
161
+ }));
162
+ }
163
+
164
+ private generateIndex() {
165
+ const files = this.generatedFiles;
166
+
167
+ const imports = [
168
+ `import { OpenAPIMappedSchemaIterable, OpenAPIMappedSchemaValue } from "..";\nimport { RouteConfig } from "@asteasolutions/zod-to-openapi";`,
169
+ ...files.map((f) => {
170
+ const importName = f.metaDirFile.names.iterable;
171
+ const dirName = f.outputFile.getPath().innerDir;
172
+ const fileName = f.metaDirFile.srcFile.getInputFileName() + ".js";
173
+ return `import { ${importName} } from "./${dirName + fileName}";`;
174
+ }),
175
+ "\n"
176
+ ].join("\n");
177
+
178
+ const schemaIterables = () => {
179
+ const schemaFiles = files.filter((f) => f.metaDirFile.srcFile.getType() === "schema");
180
+
181
+ const prefix = `export const openApiSchemaIterables: OpenAPIMappedSchemaIterable<OpenAPIMappedSchemaValue> = [\n`;
182
+ const iterableNames = schemaFiles.map((f) => f.metaDirFile.names.iterable);
183
+ const suffix = `\n];`;
184
+
185
+ return prefix + iterableNames.map((n) => ` ...${n},`).join("\n") + suffix;
186
+ }
187
+
188
+ const pathIterables = () => {
189
+ const pathFiles = files.filter((f) => f.metaDirFile.srcFile.getType() === "path");
190
+
191
+ const prefix = `export const openApiPathIterables: OpenAPIMappedSchemaIterable<RouteConfig> = [\n`;
192
+ const iterableNames = pathFiles.map((f) => f.metaDirFile.names.iterable);
193
+ const suffix = `\n];`;
194
+
195
+ return prefix + iterableNames.map((n) => ` ...${n},`).join("\n") + suffix;
196
+ }
197
+
198
+ return imports + schemaIterables() + "\n\n" + pathIterables() + "\n";
199
+ }
200
+
201
+ public generateFiles() {
202
+ this.recreateDir();
203
+
204
+ for(const { metaDirFile, outputFile } of this.generatedFiles) {
205
+ outputFile.write(metaDirFile.generateContent());
206
+ console.log(`Wrote ${metaDirFile.srcFile.id} to ${outputFile.getPath().fullPath}`);
207
+ }
208
+
209
+ writeFileSync(
210
+ path.resolve(this.getTopLevelDir() + "index.ts"),
211
+ this.generateIndex()
212
+ );
213
+ }
214
+ }
@@ -0,0 +1,16 @@
1
+ import { mkdirSync, rmSync } from "fs";
2
+ import { GenerationMode } from "../generation-mode.js";
3
+
4
+ export abstract class OutputDir {
5
+ abstract mode: GenerationMode;
6
+
7
+ public getTopLevelDir() {
8
+ return this.mode.getTopLevelOutputDir();
9
+ }
10
+
11
+ public recreateDir() {
12
+ const path = this.getTopLevelDir();
13
+ rmSync(path, { recursive: true, force: true });
14
+ mkdirSync(path, { recursive: true });
15
+ }
16
+ }
@@ -0,0 +1,33 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { GenerationMode } from "../generation-mode.js";
3
+ import { SrcFile } from "../src-file.js";
4
+ import path from "node:path";
5
+
6
+ export class OutputFile {
7
+ constructor(private src: SrcFile, private mode: GenerationMode) {}
8
+
9
+ private isInSubDir() {
10
+ return this.src.getInputDir() !== undefined;
11
+ }
12
+
13
+ public getPath() {
14
+ const inputDir = this.src.getInputDir();
15
+ const innerDir = inputDir ? `${inputDir}/` : "";
16
+ const topLevelDir = this.mode.getTopLevelOutputDir();
17
+ const outputDir = topLevelDir + innerDir;
18
+ const fileName = this.src.getInputFileName() + ".ts";
19
+ const fullPath = outputDir + fileName;
20
+ return {
21
+ innerDir,
22
+ outputDir: path.resolve(outputDir),
23
+ fullPath: path.resolve(fullPath),
24
+ };
25
+ }
26
+
27
+ public write(content: string) {
28
+ const { outputDir, fullPath } = this.getPath();
29
+ if(this.isInSubDir())
30
+ mkdirSync(outputDir, { recursive: true });
31
+ writeFileSync(fullPath, content);
32
+ }
33
+ }