@kubb/plugin-faker 5.0.0-beta.4 → 5.0.0-beta.56
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 +39 -22
- package/dist/{Faker-CdyPfOPg.d.ts → Faker-A5UuxwJj.d.ts} +3 -3
- package/dist/{Faker-fcQEB9i5.js → Faker-CHh0JtBG.js} +41 -145
- package/dist/Faker-CHh0JtBG.js.map +1 -0
- package/dist/{Faker-BgleOzVN.cjs → Faker-CcGjn5ZM.cjs} +40 -174
- package/dist/Faker-CcGjn5ZM.cjs.map +1 -0
- package/dist/components.cjs +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +1 -1
- package/dist/{fakerGenerator-D7daHCh6.js → fakerGenerator-DDNsdbH2.js} +237 -94
- package/dist/fakerGenerator-DDNsdbH2.js.map +1 -0
- package/dist/{fakerGenerator-VJEVzLjc.cjs → fakerGenerator-DrwGWYwv.cjs} +240 -97
- package/dist/fakerGenerator-DrwGWYwv.cjs.map +1 -0
- package/dist/fakerGenerator-KKVr-CA2.d.ts +14 -0
- package/dist/generators.cjs +1 -1
- package/dist/generators.d.ts +1 -1
- package/dist/generators.js +1 -1
- package/dist/index.cjs +240 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +35 -15
- package/dist/index.js +241 -70
- package/dist/index.js.map +1 -1
- package/dist/{printerFaker-CJiwzoto.d.ts → printerFaker-CMCJT3FB.d.ts} +68 -35
- package/package.json +12 -22
- package/src/components/Faker.tsx +51 -65
- package/src/generators/fakerGenerator.tsx +108 -72
- package/src/plugin.ts +27 -23
- package/src/printers/printerFaker.ts +102 -40
- package/src/resolvers/resolverFaker.ts +31 -39
- package/src/types.ts +40 -31
- package/src/utils.ts +7 -106
- package/dist/Faker-BgleOzVN.cjs.map +0 -1
- package/dist/Faker-fcQEB9i5.js.map +0 -1
- package/dist/fakerGenerator-C3Ho3BaI.d.ts +0 -9
- package/dist/fakerGenerator-D7daHCh6.js.map +0 -1
- package/dist/fakerGenerator-VJEVzLjc.cjs.map +0 -1
- package/extension.yaml +0 -364
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
|
@@ -1,21 +1,19 @@
|
|
|
1
|
+
import { getPerContentTypeName, resolveContentTypeVariants } from '@internals/shared'
|
|
2
|
+
import { aliasConflictingImports, filterUsedImports, rewriteAliasedImports } from '@internals/utils'
|
|
1
3
|
import { ast, defineGenerator } from '@kubb/core'
|
|
2
4
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
3
5
|
import { File, jsxRenderer } from '@kubb/renderer-jsx'
|
|
4
6
|
import { Faker } from '../components/Faker.tsx'
|
|
5
7
|
import { printerFaker } from '../printers/printerFaker.ts'
|
|
6
8
|
import type { PluginFaker } from '../types.ts'
|
|
7
|
-
import {
|
|
8
|
-
aliasConflictingImports,
|
|
9
|
-
buildResponseUnionSchema,
|
|
10
|
-
canOverrideSchema,
|
|
11
|
-
filterUsedImports,
|
|
12
|
-
localeToFakerImport,
|
|
13
|
-
resolveParamNameByLocation,
|
|
14
|
-
resolveSchemaRef,
|
|
15
|
-
resolveTypeReference,
|
|
16
|
-
rewriteAliasedImports,
|
|
17
|
-
} from '../utils.ts'
|
|
9
|
+
import { buildResponseUnionSchema, canOverrideSchema, localeToFakerImport, resolveParamNameByLocation, resolveTypeReference } from '../utils.ts'
|
|
18
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Built-in generator for `@kubb/plugin-faker`. Emits one `createX` factory
|
|
13
|
+
* per schema in the spec plus per-operation request/response factories. Each
|
|
14
|
+
* factory returns a value matching the corresponding TypeScript type from
|
|
15
|
+
* `@kubb/plugin-ts`.
|
|
16
|
+
*/
|
|
19
17
|
export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
20
18
|
name: 'faker',
|
|
21
19
|
renderer: jsxRenderer,
|
|
@@ -24,26 +22,29 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
24
22
|
const { output, group, dateParser, regexGenerator, mapper, seed, locale, printer } = ctx.options
|
|
25
23
|
const pluginTs = ctx.driver.getPlugin(pluginTsName)
|
|
26
24
|
|
|
27
|
-
if (!node.name || !pluginTs
|
|
25
|
+
if (!node.name || !pluginTs) {
|
|
28
26
|
return
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
const tsResolver = ctx.driver.getResolver(pluginTsName)
|
|
32
30
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
31
|
+
const schemaName = node.name
|
|
32
|
+
const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum)
|
|
33
|
+
const tsEnumType = pluginTs.options?.enum?.type
|
|
34
|
+
const tsEnumTypeSuffix = pluginTs.options?.enum?.typeSuffix ?? 'Key'
|
|
35
|
+
const schemaTypeName =
|
|
36
|
+
isEnumSchema && tsEnumType === 'asConst' ? tsResolver.resolveEnumKeyName({ name: schemaName }, tsEnumTypeSuffix) : tsResolver.resolveTypeName(schemaName)
|
|
36
37
|
const meta = {
|
|
37
38
|
name: resolver.resolveName(schemaName),
|
|
38
|
-
file: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }),
|
|
39
|
-
typeName:
|
|
39
|
+
file: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
40
|
+
typeName: schemaTypeName,
|
|
40
41
|
typeFile: tsResolver.resolveFile(
|
|
41
42
|
{ name: schemaName, extname: '.ts' },
|
|
42
|
-
{ root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group },
|
|
43
|
+
{ root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group ?? undefined },
|
|
43
44
|
),
|
|
44
45
|
} as const
|
|
45
|
-
const canOverride = canOverrideSchema(
|
|
46
|
-
const cyclicSchemas =
|
|
46
|
+
const canOverride = canOverrideSchema(node)
|
|
47
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
47
48
|
const printerInstance = printerFaker({
|
|
48
49
|
resolver,
|
|
49
50
|
schemaName,
|
|
@@ -54,9 +55,9 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
54
55
|
nodes: printer?.nodes,
|
|
55
56
|
cyclicSchemas,
|
|
56
57
|
})
|
|
57
|
-
const fakerText = printerInstance.print(
|
|
58
|
+
const fakerText = printerInstance.print(node) ?? 'undefined'
|
|
58
59
|
const typeReference = resolveTypeReference({
|
|
59
|
-
node
|
|
60
|
+
node,
|
|
60
61
|
canOverride,
|
|
61
62
|
name: meta.name,
|
|
62
63
|
typeName: meta.typeName,
|
|
@@ -64,12 +65,10 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
64
65
|
typeFilePath: meta.typeFile.path,
|
|
65
66
|
})
|
|
66
67
|
|
|
67
|
-
const imports = adapter
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}))
|
|
72
|
-
.filter((entry) => entry.path !== meta.file.path)
|
|
68
|
+
const imports = adapter.getImports(node, (schemaName) => ({
|
|
69
|
+
name: resolver.resolveName(schemaName),
|
|
70
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
71
|
+
}))
|
|
73
72
|
const usedImports = filterUsedImports(imports, fakerText)
|
|
74
73
|
|
|
75
74
|
return (
|
|
@@ -77,20 +76,21 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
77
76
|
baseName={meta.file.baseName}
|
|
78
77
|
path={meta.file.path}
|
|
79
78
|
meta={meta.file.meta}
|
|
80
|
-
banner={resolver.resolveBanner(
|
|
81
|
-
footer={resolver.resolveFooter(
|
|
79
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
80
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
82
81
|
>
|
|
83
82
|
<File.Import name={locale ? [{ propertyName: localeToFakerImport(locale), name: 'faker' }] : ['faker']} path="@faker-js/faker" />
|
|
84
83
|
{regexGenerator === 'randexp' && <File.Import name={'RandExp'} path={'randexp'} />}
|
|
85
84
|
{dateParser !== 'faker' && <File.Import path={dateParser} name={dateParser} />}
|
|
86
85
|
{typeReference.importPath && <File.Import isTypeOnly root={meta.file.path} path={typeReference.importPath} name={[meta.typeName]} />}
|
|
87
|
-
{
|
|
88
|
-
|
|
86
|
+
{usedImports.map((imp) => (
|
|
87
|
+
<File.Import key={[schemaName, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />
|
|
88
|
+
))}
|
|
89
89
|
<Faker
|
|
90
90
|
name={meta.name}
|
|
91
91
|
typeName={typeReference.typeName}
|
|
92
|
-
description={
|
|
93
|
-
node={
|
|
92
|
+
description={node.description}
|
|
93
|
+
node={node}
|
|
94
94
|
printer={printerInstance}
|
|
95
95
|
seed={seed}
|
|
96
96
|
canOverride={canOverride}
|
|
@@ -115,31 +115,66 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
115
115
|
name: resolveParamNameByLocation(resolver, node, param),
|
|
116
116
|
typeName: resolveParamNameByLocation(tsResolver, node, param),
|
|
117
117
|
}))
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
type RenderUnit = { schema: ast.SchemaNode | null; name: string; typeName: string; description?: string; skipImportNames: Array<string> }
|
|
119
|
+
|
|
120
|
+
// Expands a content array into render units: one faker per content type plus a union faker
|
|
121
|
+
// (named `baseName`) when more than one content type carries a schema, else a single faker.
|
|
122
|
+
function expandContentUnits(
|
|
123
|
+
entries: Array<{ contentType: string; schema?: ast.SchemaNode | null }>,
|
|
124
|
+
baseName: string,
|
|
125
|
+
tsBaseName: string,
|
|
126
|
+
description: string | undefined,
|
|
127
|
+
decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
|
|
128
|
+
): Array<RenderUnit> {
|
|
129
|
+
const withSchema = entries.filter((entry) => entry.schema)
|
|
130
|
+
if (withSchema.length <= 1) {
|
|
131
|
+
const primary = withSchema[0] ?? entries[0]
|
|
132
|
+
if (!primary?.schema) return []
|
|
133
|
+
return [{ schema: decorate ? decorate(primary.schema) : primary.schema, name: baseName, typeName: tsBaseName, description, skipImportNames: [] }]
|
|
134
|
+
}
|
|
135
|
+
const variants = resolveContentTypeVariants(entries, baseName)
|
|
136
|
+
const unionSchema = ast.createSchema({ type: 'union', members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })) })
|
|
137
|
+
return [
|
|
138
|
+
...variants.map((variant) => ({
|
|
139
|
+
schema: decorate ? decorate(variant.schema) : variant.schema,
|
|
140
|
+
name: variant.name,
|
|
141
|
+
typeName: getPerContentTypeName(tsBaseName, variant.suffix),
|
|
142
|
+
description,
|
|
143
|
+
skipImportNames: [],
|
|
144
|
+
})),
|
|
145
|
+
{ schema: unionSchema, name: baseName, typeName: tsBaseName, description, skipImportNames: variants.map((variant) => variant.name) },
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const responseUnits = node.responses.flatMap((response) =>
|
|
150
|
+
expandContentUnits(
|
|
151
|
+
response.content ?? [],
|
|
152
|
+
resolver.resolveResponseStatusName(node, response.statusCode),
|
|
153
|
+
tsResolver.resolveResponseStatusName(node, response.statusCode),
|
|
154
|
+
response.description,
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
const dataUnits = expandContentUnits(
|
|
158
|
+
node.requestBody?.content ?? [],
|
|
159
|
+
resolver.resolveDataName(node),
|
|
160
|
+
tsResolver.resolveDataName(node),
|
|
161
|
+
node.requestBody?.description,
|
|
162
|
+
(schema) => ({ ...schema, description: node.requestBody?.description ?? schema.description }),
|
|
163
|
+
)
|
|
134
164
|
const responseName = resolver.resolveResponseName(node)
|
|
135
165
|
const localHelperNames = new Set([
|
|
136
166
|
...paramEntries.map((entry) => entry.name),
|
|
137
|
-
...
|
|
138
|
-
...(
|
|
167
|
+
...responseUnits.map((unit) => unit.name),
|
|
168
|
+
...dataUnits.map((unit) => unit.name),
|
|
139
169
|
responseName,
|
|
140
170
|
])
|
|
171
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
172
|
+
|
|
141
173
|
const meta = {
|
|
142
|
-
file: resolver.resolveFile(
|
|
174
|
+
file: resolver.resolveFile(
|
|
175
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
176
|
+
{ root, output, group: group ?? undefined },
|
|
177
|
+
),
|
|
143
178
|
typeFile: tsResolver.resolveFile(
|
|
144
179
|
{
|
|
145
180
|
name: node.operationId,
|
|
@@ -150,7 +185,7 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
150
185
|
{
|
|
151
186
|
root,
|
|
152
187
|
output: pluginTs.options?.output ?? output,
|
|
153
|
-
group: pluginTs.options?.group,
|
|
188
|
+
group: pluginTs.options?.group ?? undefined,
|
|
154
189
|
},
|
|
155
190
|
),
|
|
156
191
|
} as const
|
|
@@ -159,7 +194,7 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
159
194
|
return adapter
|
|
160
195
|
.getImports(schema, (schemaName) => ({
|
|
161
196
|
name: resolver.resolveName(schemaName),
|
|
162
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
197
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
163
198
|
}))
|
|
164
199
|
.filter((entry) => entry.path !== meta.file.path)
|
|
165
200
|
}
|
|
@@ -182,7 +217,6 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
182
217
|
}
|
|
183
218
|
|
|
184
219
|
const canOverride = canOverrideSchema(schema)
|
|
185
|
-
const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
|
|
186
220
|
const printerInstance = printerFaker({
|
|
187
221
|
resolver,
|
|
188
222
|
schemaName: name,
|
|
@@ -230,8 +264,8 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
230
264
|
baseName={meta.file.baseName}
|
|
231
265
|
path={meta.file.path}
|
|
232
266
|
meta={meta.file.meta}
|
|
233
|
-
banner={resolver.resolveBanner(
|
|
234
|
-
footer={resolver.resolveFooter(
|
|
267
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
268
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
235
269
|
>
|
|
236
270
|
<File.Import name={locale ? [{ propertyName: localeToFakerImport(locale), name: 'faker' }] : ['faker']} path="@faker-js/faker" />
|
|
237
271
|
{regexGenerator === 'randexp' && <File.Import name={'RandExp'} path={'randexp'} />}
|
|
@@ -243,27 +277,29 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
|
|
|
243
277
|
typeName,
|
|
244
278
|
}),
|
|
245
279
|
)}
|
|
246
|
-
{
|
|
280
|
+
{responseUnits.map((unit) =>
|
|
247
281
|
renderEntry({
|
|
248
|
-
schema:
|
|
249
|
-
name,
|
|
250
|
-
typeName,
|
|
251
|
-
description:
|
|
282
|
+
schema: unit.schema,
|
|
283
|
+
name: unit.name,
|
|
284
|
+
typeName: unit.typeName,
|
|
285
|
+
description: unit.description,
|
|
286
|
+
skipImportNames: unit.skipImportNames,
|
|
287
|
+
}),
|
|
288
|
+
)}
|
|
289
|
+
{dataUnits.map((unit) =>
|
|
290
|
+
renderEntry({
|
|
291
|
+
schema: unit.schema,
|
|
292
|
+
name: unit.name,
|
|
293
|
+
typeName: unit.typeName,
|
|
294
|
+
description: unit.description,
|
|
295
|
+
skipImportNames: unit.skipImportNames,
|
|
252
296
|
}),
|
|
253
297
|
)}
|
|
254
|
-
{dataEntry
|
|
255
|
-
? renderEntry({
|
|
256
|
-
schema: dataEntry.schema,
|
|
257
|
-
name: dataEntry.name,
|
|
258
|
-
typeName: dataEntry.typeName,
|
|
259
|
-
description: dataEntry.description,
|
|
260
|
-
})
|
|
261
|
-
: null}
|
|
262
298
|
{renderEntry({
|
|
263
299
|
schema: buildResponseUnionSchema(node, resolver),
|
|
264
300
|
name: responseName,
|
|
265
301
|
typeName: tsResolver.resolveResponseName(node),
|
|
266
|
-
skipImportNames:
|
|
302
|
+
skipImportNames: responseUnits.map((unit) => unit.name),
|
|
267
303
|
})}
|
|
268
304
|
</File>
|
|
269
305
|
)
|
package/src/plugin.ts
CHANGED
|
@@ -1,28 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { definePlugin
|
|
1
|
+
import { createGroupConfig } from '@internals/shared'
|
|
2
|
+
import { definePlugin } from '@kubb/core'
|
|
3
3
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
4
4
|
import { fakerGenerator } from './generators/fakerGenerator.tsx'
|
|
5
5
|
import { resolverFaker } from './resolvers/resolverFaker.ts'
|
|
6
6
|
import type { PluginFaker } from './types.ts'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Canonical plugin name for `@kubb/plugin-faker
|
|
9
|
+
* Canonical plugin name for `@kubb/plugin-faker`. Used for driver lookups and
|
|
10
|
+
* cross-plugin dependency references.
|
|
10
11
|
*/
|
|
11
12
|
export const pluginFakerName = 'plugin-faker' satisfies PluginFaker['name']
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Generates
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Generates one mock-data factory per OpenAPI schema using Faker.js. Call
|
|
16
|
+
* `createPet()` to get a realistic `Pet` object. Useful for tests, Storybook,
|
|
17
|
+
* and local development without a running backend.
|
|
17
18
|
*
|
|
18
19
|
* @example
|
|
19
|
-
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { defineConfig } from 'kubb'
|
|
22
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
23
|
+
* import { pluginFaker } from '@kubb/plugin-faker'
|
|
24
|
+
*
|
|
25
|
+
* export default defineConfig({
|
|
26
|
+
* input: { path: './petStore.yaml' },
|
|
27
|
+
* output: { path: './src/gen' },
|
|
28
|
+
* plugins: [
|
|
29
|
+
* pluginTs(),
|
|
30
|
+
* pluginFaker({
|
|
31
|
+
* output: { path: './mocks' },
|
|
32
|
+
* seed: [100],
|
|
33
|
+
* }),
|
|
34
|
+
* ],
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
20
37
|
*/
|
|
21
38
|
export const pluginFaker = definePlugin<PluginFaker>((options) => {
|
|
22
39
|
const {
|
|
23
|
-
output = { path: 'mocks',
|
|
40
|
+
output = { path: 'mocks', barrel: { type: 'named' } },
|
|
24
41
|
seed,
|
|
25
|
-
locale,
|
|
42
|
+
locale = 'en',
|
|
26
43
|
group,
|
|
27
44
|
exclude = [],
|
|
28
45
|
include,
|
|
@@ -37,20 +54,7 @@ export const pluginFaker = definePlugin<PluginFaker>((options) => {
|
|
|
37
54
|
transformer: userTransformer,
|
|
38
55
|
} = options
|
|
39
56
|
|
|
40
|
-
const groupConfig = group
|
|
41
|
-
? ({
|
|
42
|
-
...group,
|
|
43
|
-
name: group.name
|
|
44
|
-
? group.name
|
|
45
|
-
: (ctx: { group: string }) => {
|
|
46
|
-
if (group.type === 'path') {
|
|
47
|
-
return `${ctx.group.split('/')[1]}`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return `${camelCase(ctx.group)}Controller`
|
|
51
|
-
},
|
|
52
|
-
} satisfies Group)
|
|
53
|
-
: undefined
|
|
57
|
+
const groupConfig = createGroupConfig(group)
|
|
54
58
|
|
|
55
59
|
return {
|
|
56
60
|
name: pluginFakerName,
|
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import { stringify, toRegExpString } from '@
|
|
1
|
+
import { buildObject, extractRefName, objectKey, stringify, toRegExpString } from '@kubb/ast/utils'
|
|
2
2
|
import { ast } from '@kubb/core'
|
|
3
3
|
import type { PluginFaker, ResolverFaker } from '../types.ts'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Partial
|
|
6
|
+
* Partial map of node-type overrides for the Faker printer. Each key is a
|
|
7
|
+
* `SchemaType` (`'string'`, `'date'`, ...) and each handler returns the
|
|
8
|
+
* Faker expression for that schema as a string. Use `this.transform` to
|
|
9
|
+
* recurse into nested schema nodes and `this.options` to read printer options.
|
|
10
|
+
*
|
|
11
|
+
* @example Override the integer handler
|
|
12
|
+
* ```ts
|
|
13
|
+
* pluginFaker({
|
|
14
|
+
* printer: {
|
|
15
|
+
* nodes: {
|
|
16
|
+
* integer() {
|
|
17
|
+
* return 'faker.number.float()'
|
|
18
|
+
* },
|
|
19
|
+
* },
|
|
20
|
+
* },
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
7
23
|
*/
|
|
8
24
|
export type PrinterFakerNodes = ast.PrinterPartial<string, PrinterFakerOptions>
|
|
9
25
|
|
|
10
26
|
/**
|
|
11
|
-
*
|
|
27
|
+
* Options passed to the Faker printer at instantiation: the parser library
|
|
28
|
+
* for date strings, the regex generator, the user-supplied schema-name
|
|
29
|
+
* mapper, and the resolver used to compute identifiers.
|
|
12
30
|
*/
|
|
13
31
|
export type PrinterFakerOptions = {
|
|
14
32
|
dateParser?: PluginFaker['resolvedOptions']['dateParser']
|
|
@@ -18,6 +36,12 @@ export type PrinterFakerOptions = {
|
|
|
18
36
|
typeName?: string
|
|
19
37
|
schemaName?: string
|
|
20
38
|
nestedInObject?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Set while printing the members of a union (`oneOf`). Object properties then index their
|
|
41
|
+
* type as `(NonNullable<T> & Record<K, unknown>)[K]` instead of `NonNullable<T>[K]`, so a key
|
|
42
|
+
* carried by only some branches stays valid (a plain index would be a TS2339).
|
|
43
|
+
*/
|
|
44
|
+
nestedInUnion?: boolean
|
|
21
45
|
nodes?: PrinterFakerNodes
|
|
22
46
|
/**
|
|
23
47
|
* Names of schemas that participate in a circular dependency chain.
|
|
@@ -85,7 +109,7 @@ const fakerKeywordMapper = {
|
|
|
85
109
|
},
|
|
86
110
|
boolean: () => 'faker.datatype.boolean()',
|
|
87
111
|
null: () => 'null',
|
|
88
|
-
array: (items: string
|
|
112
|
+
array: (items: Array<string> = [], min?: number, max?: number) => {
|
|
89
113
|
if (items.length > 1) {
|
|
90
114
|
return `faker.helpers.arrayElements([${items.join(', ')}])`
|
|
91
115
|
}
|
|
@@ -106,9 +130,9 @@ const fakerKeywordMapper = {
|
|
|
106
130
|
|
|
107
131
|
return `faker.helpers.multiple(() => (${item}))`
|
|
108
132
|
},
|
|
109
|
-
tuple: (items: string
|
|
133
|
+
tuple: (items: Array<string> = []) => `[${items.join(', ')}]`,
|
|
110
134
|
enum: (items: Array<string | number | boolean | undefined> = [], type = 'any') => `faker.helpers.arrayElement<${type}>([${items.join(', ')}])`,
|
|
111
|
-
union: (items: string
|
|
135
|
+
union: (items: Array<string> = []) => `faker.helpers.arrayElement<any>([${items.join(', ')}])`,
|
|
112
136
|
datetime: () => 'faker.date.anytime().toISOString()',
|
|
113
137
|
date: (representation: 'date' | 'string' = 'string', parser: PluginFaker['resolvedOptions']['dateParser'] = 'faker') => {
|
|
114
138
|
if (representation === 'string') {
|
|
@@ -142,7 +166,7 @@ const fakerKeywordMapper = {
|
|
|
142
166
|
},
|
|
143
167
|
uuid: () => 'faker.string.uuid()',
|
|
144
168
|
url: () => 'faker.internet.url()',
|
|
145
|
-
and: (items: string
|
|
169
|
+
and: (items: Array<string> = []) => {
|
|
146
170
|
if (items.length === 0) {
|
|
147
171
|
return '{}'
|
|
148
172
|
}
|
|
@@ -180,6 +204,32 @@ function parseEnumValue(value: string | number | boolean | undefined) {
|
|
|
180
204
|
return value
|
|
181
205
|
}
|
|
182
206
|
|
|
207
|
+
/** Reads the discriminator literal off a variant, or `undefined` when it can't be determined. */
|
|
208
|
+
function getDiscriminatorValue(member: ast.SchemaNode, discriminatorPropertyName: string) {
|
|
209
|
+
const prop = ast.narrowSchema(member, 'object')?.properties?.find((p) => p.name === discriminatorPropertyName)
|
|
210
|
+
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
|
|
211
|
+
|
|
212
|
+
return enumNode ? getEnumValues(enumNode)[0] : undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Type expression for an object property's value, indexed off the parent `typeName`.
|
|
217
|
+
*
|
|
218
|
+
* In a union (`oneOf`), a key that only some branches declare turns a plain `NonNullable<T>[K]`
|
|
219
|
+
* into a TS2339 error, so union members guard the access. The breakdown is below.
|
|
220
|
+
*/
|
|
221
|
+
function indexedTypeName(typeName: string, propertyName: string, nestedInUnion?: boolean): string {
|
|
222
|
+
const key = JSON.stringify(propertyName)
|
|
223
|
+
|
|
224
|
+
// `(NonNullable<T> & Record<K, unknown>)[K]`, read inside-out:
|
|
225
|
+
// NonNullable<T> strips null and undefined from the parent type T.
|
|
226
|
+
// & Record<K, unknown> forces every branch to have key K. A branch that already declares K
|
|
227
|
+
// keeps it (`T[K] & unknown` is `T[K]`); a branch missing K gains it as `unknown`.
|
|
228
|
+
// [K] reads the key, which is now always present, so it never hits TS2339.
|
|
229
|
+
// For a single object T the intersection does nothing, leaving `T[K]`.
|
|
230
|
+
return nestedInUnion ? `(NonNullable<${typeName}> & Record<${key}, unknown>)[${key}]` : `NonNullable<${typeName}>[${key}]`
|
|
231
|
+
}
|
|
232
|
+
|
|
183
233
|
/**
|
|
184
234
|
* Creates a Faker printer that generates mock data generation code from schema nodes.
|
|
185
235
|
* Handles circular references gracefully by emitting memoizing getters for cyclic properties.
|
|
@@ -234,7 +284,7 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
|
|
|
234
284
|
// Use the canonical name from the $ref path — node.name may have been overridden
|
|
235
285
|
// (e.g. by single-member allOf flatten using the property-derived child name).
|
|
236
286
|
// Inline refs (without $ref) from faker utils already carry resolved helper names.
|
|
237
|
-
const refName = node.ref ? (
|
|
287
|
+
const refName = node.ref ? (extractRefName(node.ref) ?? node.name ?? node.schema?.name) : (node.name ?? node.schema?.name)
|
|
238
288
|
|
|
239
289
|
if (!refName) {
|
|
240
290
|
throw new Error('Name not defined for ref node')
|
|
@@ -258,18 +308,32 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
|
|
|
258
308
|
return fakerKeywordMapper.enum(getEnumValues(node).map(parseEnumValue), this.options.typeName)
|
|
259
309
|
},
|
|
260
310
|
union(node): string {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
const { discriminatorPropertyName } = node
|
|
312
|
+
const baseTypeName = this.options.typeName
|
|
313
|
+
|
|
314
|
+
const items: Array<string> = (node.members ?? [])
|
|
315
|
+
.map((member) => {
|
|
316
|
+
// For a discriminated union, narrow each variant to its own branch so nested
|
|
317
|
+
// `NonNullable<T>[K]` indexes resolve against that branch instead of the whole union.
|
|
318
|
+
const value = discriminatorPropertyName ? getDiscriminatorValue(member, discriminatorPropertyName) : undefined
|
|
319
|
+
|
|
320
|
+
if (baseTypeName && value !== undefined) {
|
|
321
|
+
const typeName = `Extract<NonNullable<${baseTypeName}>, { ${JSON.stringify(discriminatorPropertyName)}: ${parseEnumValue(value)} }>`
|
|
322
|
+
|
|
323
|
+
return printNested(member, { typeName, nestedInObject: true })
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Without a discriminator, keep the union type but guard each indexed access (see
|
|
327
|
+
// `indexedTypeName`) so a key carried by only some branches resolves to `unknown`
|
|
328
|
+
// rather than erroring with TS2339.
|
|
329
|
+
return printNested(member, { typeName: baseTypeName, nestedInObject: true, nestedInUnion: true })
|
|
330
|
+
})
|
|
267
331
|
.filter((item): item is string => Boolean(item))
|
|
268
332
|
|
|
269
333
|
return fakerKeywordMapper.union(items)
|
|
270
334
|
},
|
|
271
335
|
intersection(node): string {
|
|
272
|
-
const items: string
|
|
336
|
+
const items: Array<string> = (node.members ?? [])
|
|
273
337
|
.map((member) =>
|
|
274
338
|
printNested(member, {
|
|
275
339
|
nestedInObject: true,
|
|
@@ -280,7 +344,7 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
|
|
|
280
344
|
return fakerKeywordMapper.and(items)
|
|
281
345
|
},
|
|
282
346
|
array(node): string {
|
|
283
|
-
const items: string
|
|
347
|
+
const items: Array<string> = (node.items ?? [])
|
|
284
348
|
.map((member) =>
|
|
285
349
|
printNested(member, {
|
|
286
350
|
typeName: this.options.typeName ? `NonNullable<${this.options.typeName}>[number]` : undefined,
|
|
@@ -292,7 +356,7 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
|
|
|
292
356
|
return fakerKeywordMapper.array(items, node.min, node.max)
|
|
293
357
|
},
|
|
294
358
|
tuple(node): string {
|
|
295
|
-
const items: string
|
|
359
|
+
const items: Array<string> = (node.items ?? [])
|
|
296
360
|
.map((member, index) =>
|
|
297
361
|
printNested(member, {
|
|
298
362
|
typeName: this.options.typeName ? `NonNullable<${this.options.typeName}>[${index}]` : undefined,
|
|
@@ -305,32 +369,30 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
|
|
|
305
369
|
},
|
|
306
370
|
object(node): string {
|
|
307
371
|
const cyclicSchemas = this.options.cyclicSchemas
|
|
308
|
-
const
|
|
309
|
-
.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
372
|
+
const entries = (node.properties ?? []).map((property): string => {
|
|
373
|
+
if (this.options.mapper && Object.hasOwn(this.options.mapper, property.name)) {
|
|
374
|
+
return `${objectKey(property.name)}: ${this.options.mapper[property.name]}`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const value: string =
|
|
378
|
+
printNested(property.schema, {
|
|
379
|
+
typeName: this.options.typeName ? indexedTypeName(this.options.typeName, property.name, this.options.nestedInUnion) : undefined,
|
|
380
|
+
nestedInObject: true,
|
|
381
|
+
}) ?? 'undefined'
|
|
313
382
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// emit a memoizing lazy getter. On first access it computes the value,
|
|
323
|
-
// replaces itself with a plain data property via Object.defineProperty,
|
|
324
|
-
// and returns the cached value – so every subsequent read is stable.
|
|
325
|
-
if (cyclicSchemas && ast.containsCircularRef(property.schema, { circularSchemas: cyclicSchemas, excludeName: this.options.schemaName })) {
|
|
326
|
-
return `get ${property.name}() { const _value = ${value}; Object.defineProperty(this, ${JSON.stringify(property.name)}, { value: _value, configurable: true, writable: true, enumerable: true }); return _value }`
|
|
327
|
-
}
|
|
383
|
+
// When the property's schema transitively references a schema that is
|
|
384
|
+
// part of a circular dependency (other than the current schema itself),
|
|
385
|
+
// emit a memoizing lazy getter. On first access it computes the value,
|
|
386
|
+
// replaces itself with a plain data property via Object.defineProperty,
|
|
387
|
+
// and returns the cached value – so every subsequent read is stable.
|
|
388
|
+
if (cyclicSchemas && ast.containsCircularRef(property.schema, { circularSchemas: cyclicSchemas, excludeName: this.options.schemaName })) {
|
|
389
|
+
return `get ${objectKey(property.name)}() { const _value = ${value}; Object.defineProperty(this, ${JSON.stringify(property.name)}, { value: _value, configurable: true, writable: true, enumerable: true }); return _value }`
|
|
390
|
+
}
|
|
328
391
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
.join(',')
|
|
392
|
+
return `${objectKey(property.name)}: ${value}`
|
|
393
|
+
})
|
|
332
394
|
|
|
333
|
-
return
|
|
395
|
+
return buildObject(entries)
|
|
334
396
|
},
|
|
335
397
|
...options.nodes,
|
|
336
398
|
},
|