@goast/core 0.6.0-beta.1 → 0.6.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.
Files changed (88) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +67 -67
  3. package/esm/src/codegen/config.d.ts +11 -2
  4. package/esm/src/codegen/config.d.ts.map +1 -1
  5. package/esm/src/parse/openapi-types.d.ts +4 -0
  6. package/esm/src/parse/openapi-types.d.ts.map +1 -1
  7. package/esm/src/transform/api-types.d.ts +14 -0
  8. package/esm/src/transform/api-types.d.ts.map +1 -1
  9. package/esm/src/transform/helpers.d.ts +6 -0
  10. package/esm/src/transform/helpers.d.ts.map +1 -1
  11. package/esm/src/transform/helpers.js +34 -5
  12. package/esm/src/transform/transform-endpoint.js +1 -0
  13. package/esm/src/transform/transform-schema.d.ts.map +1 -1
  14. package/esm/src/transform/transform-schema.js +32 -21
  15. package/esm/src/utils/file-system.utils.d.ts.map +1 -1
  16. package/esm/src/utils/file-system.utils.js +36 -4
  17. package/esm/src/utils/schema.utils.d.ts.map +1 -1
  18. package/esm/src/utils/schema.utils.js +91 -11
  19. package/esm/src/utils/string.utils.d.ts +19 -0
  20. package/esm/src/utils/string.utils.d.ts.map +1 -1
  21. package/esm/src/utils/string.utils.js +25 -0
  22. package/package.json +1 -1
  23. package/script/src/codegen/config.d.ts +11 -2
  24. package/script/src/codegen/config.d.ts.map +1 -1
  25. package/script/src/parse/openapi-types.d.ts +4 -0
  26. package/script/src/parse/openapi-types.d.ts.map +1 -1
  27. package/script/src/transform/api-types.d.ts +14 -0
  28. package/script/src/transform/api-types.d.ts.map +1 -1
  29. package/script/src/transform/helpers.d.ts +6 -0
  30. package/script/src/transform/helpers.d.ts.map +1 -1
  31. package/script/src/transform/helpers.js +37 -5
  32. package/script/src/transform/transform-endpoint.js +1 -0
  33. package/script/src/transform/transform-schema.d.ts.map +1 -1
  34. package/script/src/transform/transform-schema.js +31 -20
  35. package/script/src/utils/file-system.utils.d.ts.map +1 -1
  36. package/script/src/utils/file-system.utils.js +35 -3
  37. package/script/src/utils/schema.utils.d.ts.map +1 -1
  38. package/script/src/utils/schema.utils.js +91 -11
  39. package/script/src/utils/string.utils.d.ts +19 -0
  40. package/script/src/utils/string.utils.d.ts.map +1 -1
  41. package/script/src/utils/string.utils.js +26 -0
  42. package/src/_dnt.shims.ts +69 -0
  43. package/src/mod.ts +35 -0
  44. package/src/src/ast/node.ts +46 -0
  45. package/src/src/codegen/config.ts +50 -0
  46. package/src/src/codegen/endpoints-generator.ts +72 -0
  47. package/src/src/codegen/generator.ts +192 -0
  48. package/src/src/codegen/internal-utils.ts +6 -0
  49. package/src/src/codegen/schemas-generator.ts +87 -0
  50. package/src/src/codegen/services-generator.ts +77 -0
  51. package/src/src/codegen/types.ts +37 -0
  52. package/src/src/collect/collector.ts +142 -0
  53. package/src/src/collect/helpers.ts +36 -0
  54. package/src/src/collect/types.ts +21 -0
  55. package/src/src/internal-utils.ts +5 -0
  56. package/src/src/parse/deref-proxy.ts +70 -0
  57. package/src/src/parse/openapi-types.ts +953 -0
  58. package/src/src/parse/parser.ts +206 -0
  59. package/src/src/parse/types.ts +28 -0
  60. package/src/src/transform/api-types.ts +259 -0
  61. package/src/src/transform/helpers.ts +251 -0
  62. package/src/src/transform/transform-document.ts +35 -0
  63. package/src/src/transform/transform-endpoint.ts +332 -0
  64. package/src/src/transform/transform-schema.ts +232 -0
  65. package/src/src/transform/transformer.ts +53 -0
  66. package/src/src/transform/types.ts +52 -0
  67. package/src/src/utils/action-provider.ts +58 -0
  68. package/src/src/utils/api-data.utils.ts +48 -0
  69. package/src/src/utils/array.utils.ts +7 -0
  70. package/src/src/utils/asset-manager.ts +51 -0
  71. package/src/src/utils/class.utils.ts +40 -0
  72. package/src/src/utils/common.utils.ts +86 -0
  73. package/src/src/utils/condition.ts +7 -0
  74. package/src/src/utils/endpoint.utils.ts +10 -0
  75. package/src/src/utils/error.utils.ts +3 -0
  76. package/src/src/utils/factory.ts +42 -0
  77. package/src/src/utils/file-system.utils.ts +97 -0
  78. package/src/src/utils/generator.utils.ts +6 -0
  79. package/src/src/utils/object.utils.ts +32 -0
  80. package/src/src/utils/schema.utils.ts +240 -0
  81. package/src/src/utils/source-builder.ts +597 -0
  82. package/src/src/utils/string-builder/options.ts +19 -0
  83. package/src/src/utils/string-builder/string-builder.ts +165 -0
  84. package/src/src/utils/string-builder/utils.ts +69 -0
  85. package/src/src/utils/string.utils.ts +394 -0
  86. package/src/src/utils/string.utils.types.ts +179 -0
  87. package/src/src/utils/type.utils.ts +93 -0
  88. package/src/src/utils/yaml-info.ts +48 -0
@@ -0,0 +1,42 @@
1
+ import type { Constructor } from './type.utils.js';
2
+
3
+ type FactoryProvider<T, TArgs extends unknown[]> =
4
+ | {
5
+ kind: 'ctor';
6
+ factory: Constructor<T, TArgs>;
7
+ }
8
+ | {
9
+ kind: 'fn';
10
+ factory: (...args: TArgs) => T;
11
+ }
12
+ | {
13
+ kind: 'value';
14
+ factory: T;
15
+ };
16
+
17
+ export class Factory<T, TArgs extends unknown[]> {
18
+ constructor(private readonly _provider: FactoryProvider<T, TArgs>) {}
19
+
20
+ public create(...args: TArgs): T {
21
+ switch (this._provider.kind) {
22
+ case 'ctor':
23
+ return new this._provider.factory(...args);
24
+ case 'fn':
25
+ return this._provider.factory(...args);
26
+ case 'value':
27
+ return this._provider.factory;
28
+ }
29
+ }
30
+
31
+ public static fromType<T, TArgs extends unknown[]>(type: Constructor<T, TArgs>): Factory<T, TArgs> {
32
+ return new Factory({ kind: 'ctor', factory: type });
33
+ }
34
+
35
+ public static fromFn<T, TArgs extends unknown[]>(fn: (...args: TArgs) => T): Factory<T, TArgs> {
36
+ return new Factory({ kind: 'fn', factory: fn });
37
+ }
38
+
39
+ public static fromValue<T>(value: T): Factory<T, []> {
40
+ return new Factory({ kind: 'value', factory: value });
41
+ }
42
+ }
@@ -0,0 +1,97 @@
1
+ import { basename, dirname, join, resolve } from 'node:path';
2
+
3
+
4
+ import fs from 'fs-extra';
5
+
6
+ import type { OpenApiGeneratorConfig } from '../codegen/config.js';
7
+ import type { Nullable } from './type.utils.js';
8
+
9
+ export type DirectoryScanOptions = {
10
+ recursive: boolean;
11
+ maxDepth: Nullable<number>;
12
+ filter?: (file: string) => boolean;
13
+ };
14
+
15
+ export const defaultDirectoryScanOptions: DirectoryScanOptions = {
16
+ recursive: false,
17
+ maxDepth: 5,
18
+ };
19
+
20
+ export async function getFiles(dir: string, options?: Partial<DirectoryScanOptions>): Promise<string[]> {
21
+ const opts = { ...defaultDirectoryScanOptions, ...options };
22
+ const files: string[] = [];
23
+ await getFilesImpl(dir, opts, files, 0);
24
+ return files;
25
+ }
26
+
27
+ async function getFilesImpl(
28
+ dir: string,
29
+ options: DirectoryScanOptions,
30
+ files: string[],
31
+ currentDepth: number,
32
+ ): Promise<void> {
33
+ const dirFiles = await fs.readdir(dir);
34
+ for (const file of dirFiles) {
35
+ const filePath = resolve(dir, file);
36
+ const stat = await fs.lstat(filePath);
37
+ if (stat.isDirectory()) {
38
+ if (options.recursive) {
39
+ if (options.maxDepth && options.maxDepth > currentDepth) {
40
+ await getFilesImpl(filePath, options, files, currentDepth + 1);
41
+ }
42
+ }
43
+ } else {
44
+ if (!options.filter || options.filter(file)) {
45
+ files.push(filePath);
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Splits a file name into its base and extension, treating the extension as everything from the last dot onward
53
+ * (including the dot). A name with no dot has an empty extension. A name that is nothing but an extension (e.g.
54
+ * `.kt`) has an empty base.
55
+ */
56
+ function splitFileName(fileName: string): { base: string; ext: string } {
57
+ const dotIndex = fileName.lastIndexOf('.');
58
+ if (dotIndex === -1) {
59
+ return { base: fileName, ext: '' };
60
+ }
61
+ return { base: fileName.slice(0, dotIndex), ext: fileName.slice(dotIndex) };
62
+ }
63
+
64
+ /**
65
+ * Finds an unused path derived from `filePath` by inserting `_1` before the extension, then `_2`, and so on, until
66
+ * a path that does not yet exist is found.
67
+ */
68
+ function getCountedFilePath(filePath: string): string {
69
+ const dir = dirname(filePath);
70
+ const { base, ext } = splitFileName(basename(filePath));
71
+
72
+ let counter = 1;
73
+ let candidate = join(dir, `${base}_${counter}${ext}`);
74
+ while (fs.existsSync(candidate)) {
75
+ counter++;
76
+ candidate = join(dir, `${base}_${counter}${ext}`);
77
+ }
78
+ return candidate;
79
+ }
80
+
81
+ export function writeGeneratedFile(config: OpenApiGeneratorConfig, filePath: string, content: string): void {
82
+ let targetPath = filePath;
83
+
84
+ if (config.existingFileBehavior !== 'override' && fs.existsSync(filePath)) {
85
+ if (config.existingFileBehavior === 'error') {
86
+ throw new Error(`File already exists: ${filePath}`);
87
+ }
88
+ if (config.existingFileBehavior === 'skip') {
89
+ return;
90
+ }
91
+ // config.existingFileBehavior === 'count'
92
+ targetPath = getCountedFilePath(filePath);
93
+ }
94
+
95
+ fs.ensureDirSync(dirname(targetPath));
96
+ fs.writeFileSync(targetPath, content);
97
+ }
@@ -0,0 +1,6 @@
1
+ export function getInitializedValue<T>(value: T | undefined): T {
2
+ if (value === undefined) {
3
+ throw new Error('Generator not initialized.');
4
+ }
5
+ return value;
6
+ }
@@ -0,0 +1,32 @@
1
+ export function createOverwriteProxy<T extends object>(target: T, values?: Partial<T>): T {
2
+ const proxy = new Proxy(target, new OverwriteProxyHandler());
3
+ if (values) {
4
+ Object.assign(proxy, values);
5
+ }
6
+ return proxy;
7
+ }
8
+
9
+ export function getDeepProperty(obj: object, path: string[]): unknown {
10
+ let current = obj;
11
+ for (const key of path) {
12
+ if (!current || typeof current !== 'object') {
13
+ return undefined;
14
+ }
15
+ // deno-lint-ignore no-explicit-any
16
+ current = (current as any)[key];
17
+ }
18
+ return current;
19
+ }
20
+
21
+ class OverwriteProxyHandler<T extends object> implements ProxyHandler<T> {
22
+ private readonly _overwrittenValues: Record<PropertyKey, unknown> = {};
23
+
24
+ public get(target: T, prop: PropertyKey): unknown {
25
+ return this._overwrittenValues[prop] ?? (target as Record<PropertyKey, unknown>)[prop];
26
+ }
27
+
28
+ public set(_target: T, prop: PropertyKey, value: unknown): boolean {
29
+ this._overwrittenValues[prop] = value;
30
+ return true;
31
+ }
32
+ }
@@ -0,0 +1,240 @@
1
+ import { isOpenApiObjectProperty } from '../internal-utils.js';
2
+ import type { OpenApiSchema } from '../parse/openapi-types.js';
3
+ import type { ApiSchema, ApiSchemaProperty, ObjectLikeApiSchema } from '../transform/api-types.js';
4
+
5
+ export function resolveAnyOfAndAllOf(
6
+ schema: ApiSchema<'combined' | 'object'>,
7
+ ignoreNonObjectParts: boolean,
8
+ ): ApiSchema<'object'> | undefined {
9
+ if (!ignoreNonObjectParts && (hasInvalidSubSchema(schema.anyOf) || hasInvalidSubSchema(schema.allOf))) {
10
+ return undefined;
11
+ }
12
+
13
+ const required = new Set(schema.required);
14
+ const properties = new Map<string, ApiSchemaProperty>();
15
+ if ('properties' in schema) {
16
+ schema.properties.forEach((prop) => properties.set(prop.name, prop));
17
+ }
18
+ collectSubSchemaProperties(schema.allOf, properties, required, false);
19
+ collectSubSchemaProperties(schema.anyOf, properties, required, true);
20
+
21
+ const initialAdditionalProperties = schema.kind === 'object' ? schema.additionalProperties : undefined;
22
+ let additionalProperties = initialAdditionalProperties;
23
+ additionalProperties = combineAdditionalProperties(schema.allOf, additionalProperties);
24
+ additionalProperties = combineAdditionalProperties(schema.anyOf, additionalProperties);
25
+
26
+ if (properties.size === 0 && initialAdditionalProperties === additionalProperties) {
27
+ return undefined;
28
+ }
29
+
30
+ return {
31
+ ...schema,
32
+ kind: 'object',
33
+ type: 'object',
34
+ anyOf: [],
35
+ allOf: [],
36
+ properties: properties,
37
+ required: required,
38
+ additionalProperties,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * The sub-schemas already collected, split by the `optional` they were collected with, so that a branch
44
+ * composing back to one of its own ancestors terminates instead of overflowing the stack.
45
+ *
46
+ * Collecting the same (sub-schema, `optional`) pair twice cannot contribute anything new: the property map
47
+ * is first-wins, `required` is a set, and a pair's descendants are always traversed with an `optional`
48
+ * derived only from that pair's own, so the second pass visits exactly the same schemas and reaches exactly
49
+ * the same conclusions. Skipping it is therefore equivalent, not merely cheaper.
50
+ */
51
+ type CollectedSubSchemas = { required: Set<ApiSchema>; optional: Set<ApiSchema> };
52
+
53
+ function collectSubSchemaProperties(
54
+ subSchemas: ApiSchema[],
55
+ properties: Map<string, ApiSchemaProperty>,
56
+ required: Set<string>,
57
+ optional: boolean,
58
+ collected: CollectedSubSchemas = { required: new Set(), optional: new Set() },
59
+ ) {
60
+ const seen = optional ? collected.optional : collected.required;
61
+ for (const subSchema of subSchemas) {
62
+ if (seen.has(subSchema)) continue;
63
+ seen.add(subSchema);
64
+
65
+ if (subSchema.kind === 'object') {
66
+ for (const prop of subSchema.properties.values()) {
67
+ const existing = properties.get(prop.name);
68
+ if (!existing) {
69
+ properties.set(prop.name, prop);
70
+ } else if (hasConflictingType(existing.schema, prop.schema)) {
71
+ // Two branches declare the same property with types that cannot both hold. Picking one and
72
+ // discarding the other would make the merged schema assert something the source contradicts, so
73
+ // widen to a schema that admits both instead.
74
+ properties.set(prop.name, { name: prop.name, schema: widenSchemas(existing.schema, prop.schema) });
75
+ }
76
+ }
77
+ }
78
+
79
+ // A branch does not have to contribute properties to contribute requiredness: `allOf: [{ required: [x] }]`
80
+ // only tightens a property inherited from a sibling branch and has no type or properties of its own.
81
+ if (!optional) {
82
+ for (const prop of subSchema.required) {
83
+ required.add(prop);
84
+ }
85
+ }
86
+
87
+ if (subSchema.kind === 'object' || subSchema.kind === 'combined') {
88
+ collectSubSchemaProperties(subSchema.allOf, properties, required, optional, collected);
89
+ collectSubSchemaProperties(subSchema.anyOf, properties, required, true, collected);
90
+ } else {
91
+ // Exactly one branch of a composed `oneOf` applies to any given instance, so its properties are
92
+ // collected but none of its requiredness is — the same treatment an `anyOf` branch gets.
93
+ const oneOf = composedOneOf(subSchema);
94
+ if (oneOf) collectSubSchemaProperties(oneOf, properties, required, true, collected);
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * The branches of a `oneOf` sub-schema that describe the same instance as the schema composing it, or
101
+ * `undefined` if it has none.
102
+ *
103
+ * An undiscriminated `oneOf` inside a composition constrains the very instance being composed:
104
+ * `allOf: [{ oneOf: [A, B] }, C]` is "A or B, *and* C", so whichever branch applies contributes its
105
+ * properties to the whole. A `oneOf` that declares a `discriminator` is not a composition but a list of
106
+ * subtypes, and `allOf: [Base]` against such a schema means "is a Base" — the sibling subtypes' properties
107
+ * belong to those siblings, not to the schema inheriting from the base.
108
+ */
109
+ function composedOneOf(schema: ApiSchema): ApiSchema[] | undefined {
110
+ return schema.kind === 'oneOf' && !schema.discriminator ? schema.oneOf : undefined;
111
+ }
112
+
113
+ /**
114
+ * Whether two declarations of one property name state types that cannot both hold. A branch that says
115
+ * nothing about the type (`unknown`) constrains nothing and so never conflicts, and two declarations of the
116
+ * same kind are treated as a refinement of one another rather than a conflict, so the first still wins.
117
+ */
118
+ function hasConflictingType(a: ApiSchema, b: ApiSchema): boolean {
119
+ return a.kind !== b.kind && a.kind !== 'unknown' && b.kind !== 'unknown';
120
+ }
121
+
122
+ /** The branches a schema contributes to a widened `oneOf`, flattened so widening stays a single level. */
123
+ function conflictBranches(schema: ApiSchema): ApiSchema[] {
124
+ return schema.kind === 'oneOf' ? schema.oneOf : [schema];
125
+ }
126
+
127
+ /** A `oneOf` admitting either side of a conflicting merge, so neither side is silently discarded. */
128
+ function widenSchemas(a: ApiSchema, b: ApiSchema): ApiSchema<'oneOf'> {
129
+ const oneOf: ApiSchema[] = [];
130
+ for (const branch of [...conflictBranches(a), ...conflictBranches(b)]) {
131
+ if (!oneOf.some((x) => x.id === branch.id)) oneOf.push(branch);
132
+ }
133
+
134
+ // Derived from the branch ids rather than generated, so the same conflict always yields the same schema.
135
+ const id = `conflict-${oneOf.map((x) => x.id).join('+')}`;
136
+ return {
137
+ ...a,
138
+ $ref: undefined,
139
+ id,
140
+ name: id,
141
+ isNameGenerated: true,
142
+ kind: 'oneOf',
143
+ enum: undefined,
144
+ const: undefined,
145
+ default: undefined,
146
+ discriminator: undefined,
147
+ inheritedSchemas: [],
148
+ nullable: a.nullable || b.nullable,
149
+ oneOf,
150
+ };
151
+ }
152
+
153
+ function combineAdditionalProperties(
154
+ subSchemas: ApiSchema[],
155
+ current: ObjectLikeApiSchema['additionalProperties'],
156
+ /** Guards the same composition cycle `collectSubSchemaProperties` guards; see `CollectedSubSchemas`. */
157
+ visited: Set<ApiSchema> = new Set(),
158
+ ): ObjectLikeApiSchema['additionalProperties'] {
159
+ if (current === true) return true;
160
+
161
+ for (const subSchema of subSchemas) {
162
+ if (visited.has(subSchema)) continue;
163
+ visited.add(subSchema);
164
+
165
+ if (subSchema.kind === 'object' && subSchema.additionalProperties) {
166
+ if (subSchema.additionalProperties === true) return true;
167
+ if (!current) {
168
+ current = subSchema.additionalProperties;
169
+ } else if (current && current.id !== subSchema.additionalProperties.id) {
170
+ if (current.kind === 'oneOf') {
171
+ current.oneOf.push(subSchema.additionalProperties);
172
+ } else {
173
+ const id = `schema-${Math.random().toString(36).substring(2)}`;
174
+ current = {
175
+ $ref: undefined,
176
+ $src: current.$src,
177
+ id,
178
+ kind: 'oneOf',
179
+ name: id,
180
+ isNameGenerated: true,
181
+ description: undefined,
182
+ deprecated: false,
183
+ accessibility: 'all',
184
+ enum: undefined,
185
+ const: undefined,
186
+ default: undefined,
187
+ example: undefined,
188
+ nullable: false,
189
+ required: new Set(),
190
+ custom: {},
191
+ not: undefined,
192
+ discriminator: undefined,
193
+ inheritedSchemas: [],
194
+ oneOf: [current, subSchema.additionalProperties],
195
+ };
196
+ }
197
+ }
198
+ }
199
+
200
+ if (subSchema.kind === 'object' || subSchema.kind === 'combined') {
201
+ current = combineAdditionalProperties(subSchema.allOf, current, visited);
202
+ current = combineAdditionalProperties(subSchema.anyOf, current, visited);
203
+ } else {
204
+ const oneOf = composedOneOf(subSchema);
205
+ if (oneOf) current = combineAdditionalProperties(oneOf, current, visited);
206
+ }
207
+
208
+ if (current === true) return true;
209
+ }
210
+
211
+ return current;
212
+ }
213
+
214
+ function hasInvalidSubSchema(subSchemas: ApiSchema[]): boolean {
215
+ return subSchemas.some(
216
+ (x) =>
217
+ x.kind !== 'object' &&
218
+ (x.kind !== 'combined' || (!hasInvalidSubSchema(x.allOf) && !hasInvalidSubSchema(x.anyOf))),
219
+ );
220
+ }
221
+
222
+ export const DEFAULT_IGNORED_SCHEMA_PROPERTIES: (keyof OpenApiSchema)[] = [
223
+ 'description',
224
+ 'example',
225
+ 'deprecated',
226
+ ];
227
+
228
+ /**
229
+ * Retrieves the first schema reference and the reference chain of a given schema that
230
+ * defines some schema properties. (e.g. schemas with just "$ref" are skipped)
231
+ * @param schema The schema to retrieve the reference from.
232
+ * @param propertiesToIgnore The properties to ignore when checking for a schema definition.
233
+ */
234
+ export function getSchemaReference(schema: ApiSchema, propertiesToIgnore: (keyof OpenApiSchema)[]): ApiSchema {
235
+ if (!schema.$ref) return schema;
236
+ const hasPropertiesDefined = Object.keys(schema.$src.originalComponent).some(
237
+ (key) => isOpenApiObjectProperty(key) && !propertiesToIgnore.includes(key as keyof OpenApiSchema),
238
+ );
239
+ return hasPropertiesDefined ? schema : getSchemaReference(schema.$ref, propertiesToIgnore);
240
+ }