@kubb/plugin-faker 3.0.0-alpha.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.
@@ -0,0 +1,313 @@
1
+ import transformers from '@kubb/core/transformers'
2
+ import { SchemaGenerator, isKeyword, schemaKeywords } from '@kubb/plugin-oas'
3
+
4
+ import type { Schema, SchemaKeywordBase, SchemaKeywordMapper, SchemaMapper } from '@kubb/plugin-oas'
5
+ import type { Options } from '../types.ts'
6
+
7
+ export const fakerKeywordMapper = {
8
+ any: () => 'undefined',
9
+ unknown: () => 'unknown',
10
+ number: (min?: number, max?: number) => {
11
+ if (max !== undefined && min !== undefined) {
12
+ return `faker.number.float({ min: ${min}, max: ${max} })`
13
+ }
14
+
15
+ if (min !== undefined) {
16
+ return `faker.number.float({ min: ${min} })`
17
+ }
18
+
19
+ if (max !== undefined) {
20
+ return `faker.number.float({ max: ${max} })`
21
+ }
22
+
23
+ return 'faker.number.float()'
24
+ },
25
+ integer: (min?: number, max?: number) => {
26
+ if (max !== undefined && min !== undefined) {
27
+ return `faker.number.int({ min: ${min}, max: ${max} })`
28
+ }
29
+
30
+ if (min !== undefined) {
31
+ return `faker.number.int({ min: ${min} })`
32
+ }
33
+
34
+ if (max !== undefined) {
35
+ return `faker.number.int({ max: ${max} })`
36
+ }
37
+
38
+ return 'faker.number.int()'
39
+ },
40
+ string: (min?: number, max?: number) => {
41
+ if (max !== undefined && min !== undefined) {
42
+ return `faker.string.alpha({ length: { min: ${min}, max: ${max} } })`
43
+ }
44
+
45
+ if (min !== undefined) {
46
+ return `faker.string.alpha({ length: { min: ${min} } })`
47
+ }
48
+
49
+ if (max !== undefined) {
50
+ return `faker.string.alpha({ length: { max: ${max} } })`
51
+ }
52
+
53
+ return 'faker.string.alpha()'
54
+ },
55
+ boolean: () => 'faker.datatype.boolean()',
56
+ undefined: () => 'undefined',
57
+ null: () => 'null',
58
+ array: (items: string[] = []) => `faker.helpers.arrayElements([${items.join(', ')}]) as any`,
59
+ tuple: (items: string[] = []) => `faker.helpers.arrayElements([${items.join(', ')}]) as any`,
60
+ enum: (items: Array<string | number> = []) => `faker.helpers.arrayElement<any>([${items.join(', ')}])`,
61
+ union: (items: string[] = []) => `faker.helpers.arrayElement<any>([${items.join(', ')}])`,
62
+ /**
63
+ * ISO 8601
64
+ */
65
+ datetime: () => 'faker.date.anytime().toISOString()',
66
+ /**
67
+ * Type `'date'` Date
68
+ * Type `'string'` ISO date format (YYYY-MM-DD)
69
+ * @default ISO date format (YYYY-MM-DD)
70
+ */
71
+ date: (type: 'date' | 'string' = 'string', parser?: string) => {
72
+ if (type === 'string') {
73
+ if (parser) {
74
+ return `${parser}(faker.date.anytime()).format("YYYY-MM-DD")`
75
+ }
76
+ return 'faker.date.anytime().toString()'
77
+ }
78
+ return 'faker.date.anytime()'
79
+ },
80
+ /**
81
+ * Type `'date'` Date
82
+ * Type `'string'` ISO time format (HH:mm:ss[.SSSSSS])
83
+ * @default ISO time format (HH:mm:ss[.SSSSSS])
84
+ */
85
+ time: (type: 'date' | 'string' = 'string', parser?: string) => {
86
+ if (type === 'string') {
87
+ if (parser) {
88
+ return `${parser}(faker.date.anytime()).format("HH:mm:ss")`
89
+ }
90
+ return 'faker.date.anytime().toString()'
91
+ }
92
+ return 'faker.date.anytime()'
93
+ },
94
+ uuid: () => 'faker.string.uuid()',
95
+ url: () => 'faker.internet.url()',
96
+ and: (items: string[] = []) => `Object.assign({}, ${items.join(', ')})`,
97
+ object: () => 'object',
98
+ ref: () => 'ref',
99
+ matches: (value = '', regexGenerator: 'faker' | 'randexp' = 'faker') => {
100
+ if (regexGenerator === 'randexp') {
101
+ return `${transformers.toRegExpString(value, 'RandExp')}.gen()`
102
+ }
103
+ return `faker.helpers.fromRegExp(${transformers.toRegExpString(value)})`
104
+ },
105
+ email: () => 'faker.internet.email()',
106
+ firstName: () => 'faker.person.firstName()',
107
+ lastName: () => 'faker.person.lastName()',
108
+ password: () => 'faker.internet.password()',
109
+ phone: () => 'faker.phone.number()',
110
+ blob: () => 'faker.image.imageUrl() as unknown as Blob',
111
+ default: undefined,
112
+ describe: undefined,
113
+ const: (value?: string | number) => (value as string) ?? '',
114
+ max: undefined,
115
+ min: undefined,
116
+ nullable: undefined,
117
+ nullish: undefined,
118
+ optional: undefined,
119
+ readOnly: undefined,
120
+ strict: undefined,
121
+ deprecated: undefined,
122
+ example: undefined,
123
+ schema: undefined,
124
+ catchall: undefined,
125
+ name: undefined,
126
+ } satisfies SchemaMapper<string | null | undefined>
127
+
128
+ /**
129
+ * @link based on https://github.com/cellular/oazapfts/blob/7ba226ebb15374e8483cc53e7532f1663179a22c/src/codegen/generate.ts#L398
130
+ */
131
+
132
+ function schemaKeywordsorter(a: Schema, b: Schema) {
133
+ if (b.keyword === 'null') {
134
+ return -1
135
+ }
136
+
137
+ return 0
138
+ }
139
+
140
+ export function joinItems(items: string[]): string {
141
+ switch (items.length) {
142
+ case 0:
143
+ return 'undefined'
144
+ case 1:
145
+ return items[0]!
146
+ default:
147
+ return fakerKeywordMapper.union(items)
148
+ }
149
+ }
150
+
151
+ type ParserOptions = {
152
+ name: string
153
+ typeName?: string
154
+ description?: string
155
+
156
+ seed?: number | number[]
157
+ regexGenerator?: 'faker' | 'randexp'
158
+ withData?: boolean
159
+ dateParser?: Options['dateParser']
160
+ mapper?: Record<string, string>
161
+ }
162
+
163
+ export function parse(parent: Schema | undefined, current: Schema, options: ParserOptions): string | null | undefined {
164
+ const value = fakerKeywordMapper[current.keyword as keyof typeof fakerKeywordMapper]
165
+
166
+ if (!value) {
167
+ return undefined
168
+ }
169
+
170
+ if (isKeyword(current, schemaKeywords.union)) {
171
+ return fakerKeywordMapper.union(current.args.map((schema) => parse(current, schema, { ...options, withData: false })).filter(Boolean))
172
+ }
173
+
174
+ if (isKeyword(current, schemaKeywords.and)) {
175
+ return fakerKeywordMapper.and(current.args.map((schema) => parse(current, schema, { ...options, withData: false })).filter(Boolean))
176
+ }
177
+
178
+ if (isKeyword(current, schemaKeywords.array)) {
179
+ return fakerKeywordMapper.array(current.args.items.map((schema) => parse(current, schema, { ...options, withData: false })).filter(Boolean))
180
+ }
181
+
182
+ if (isKeyword(current, schemaKeywords.enum)) {
183
+ return fakerKeywordMapper.enum(
184
+ current.args.items.map((schema) => {
185
+ if (schema.format === 'number') {
186
+ return schema.name
187
+ }
188
+ return transformers.stringify(schema.name)
189
+ }),
190
+ )
191
+ }
192
+
193
+ if (isKeyword(current, schemaKeywords.ref)) {
194
+ if (!current.args?.name) {
195
+ throw new Error(`Name not defined for keyword ${current.keyword}`)
196
+ }
197
+
198
+ if (options.withData) {
199
+ return `${current.args.name}(data)`
200
+ }
201
+
202
+ return `${current.args.name}()`
203
+ }
204
+
205
+ if (isKeyword(current, schemaKeywords.object)) {
206
+ const argsObject = Object.entries(current.args?.properties || {})
207
+ .filter((item) => {
208
+ const schema = item[1]
209
+ return schema && typeof schema.map === 'function'
210
+ })
211
+ .map(([name, schemas]) => {
212
+ const nameSchema = schemas.find((schema) => schema.keyword === schemaKeywords.name) as SchemaKeywordMapper['name']
213
+ const mappedName = nameSchema?.args || name
214
+
215
+ // custom mapper(pluginOptions)
216
+ if (options.mapper?.[mappedName]) {
217
+ return `"${name}": ${options.mapper?.[mappedName]}`
218
+ }
219
+
220
+ return `"${name}": ${joinItems(
221
+ schemas
222
+ .sort(schemaKeywordsorter)
223
+ .map((schema) => parse(current, schema, { ...options, withData: false }))
224
+ .filter(Boolean),
225
+ )}`
226
+ })
227
+ .join(',')
228
+
229
+ return `{${argsObject}}`
230
+ }
231
+
232
+ if (isKeyword(current, schemaKeywords.tuple)) {
233
+ if (Array.isArray(current.args.items)) {
234
+ return fakerKeywordMapper.tuple(current.args.items.map((schema) => parse(current, schema, { ...options, withData: false })).filter(Boolean))
235
+ }
236
+
237
+ return parse(current, current.args.items, { ...options, withData: false })
238
+ }
239
+
240
+ if (isKeyword(current, schemaKeywords.const)) {
241
+ if (current.args.format === 'number' && current.args.name !== undefined) {
242
+ return fakerKeywordMapper.const(current.args.name?.toString())
243
+ }
244
+ return fakerKeywordMapper.const(transformers.stringify(current.args.value))
245
+ }
246
+
247
+ if (isKeyword(current, schemaKeywords.matches) && current.args) {
248
+ return fakerKeywordMapper.matches(current.args, options.regexGenerator)
249
+ }
250
+
251
+ if (isKeyword(current, schemaKeywords.null) || isKeyword(current, schemaKeywords.undefined) || isKeyword(current, schemaKeywords.any)) {
252
+ return value() || ''
253
+ }
254
+
255
+ if (isKeyword(current, schemaKeywords.string)) {
256
+ if (parent) {
257
+ const minSchema = SchemaGenerator.find([parent], schemaKeywords.min)
258
+ const maxSchema = SchemaGenerator.find([parent], schemaKeywords.max)
259
+
260
+ return fakerKeywordMapper.string(minSchema?.args, maxSchema?.args)
261
+ }
262
+
263
+ return fakerKeywordMapper.string()
264
+ }
265
+
266
+ if (isKeyword(current, schemaKeywords.number)) {
267
+ if (parent) {
268
+ const minSchema = SchemaGenerator.find([parent], schemaKeywords.min)
269
+ const maxSchema = SchemaGenerator.find([parent], schemaKeywords.max)
270
+
271
+ return fakerKeywordMapper.number(minSchema?.args, maxSchema?.args)
272
+ }
273
+
274
+ return fakerKeywordMapper.number()
275
+ }
276
+
277
+ if (isKeyword(current, schemaKeywords.integer)) {
278
+ if (parent) {
279
+ const minSchema = SchemaGenerator.find([parent], schemaKeywords.min)
280
+ const maxSchema = SchemaGenerator.find([parent], schemaKeywords.max)
281
+
282
+ return fakerKeywordMapper.integer(minSchema?.args, maxSchema?.args)
283
+ }
284
+
285
+ return fakerKeywordMapper.integer()
286
+ }
287
+
288
+ if (isKeyword(current, schemaKeywords.datetime)) {
289
+ return fakerKeywordMapper.datetime()
290
+ }
291
+
292
+ if (isKeyword(current, schemaKeywords.date)) {
293
+ return fakerKeywordMapper.date(current.args.type, options.dateParser)
294
+ }
295
+
296
+ if (isKeyword(current, schemaKeywords.time)) {
297
+ return fakerKeywordMapper.time(current.args.type, options.dateParser)
298
+ }
299
+
300
+ if (current.keyword in fakerKeywordMapper && 'args' in current) {
301
+ const value = fakerKeywordMapper[current.keyword as keyof typeof fakerKeywordMapper] as (typeof fakerKeywordMapper)['const']
302
+
303
+ const options = JSON.stringify((current as SchemaKeywordBase<unknown>).args)
304
+
305
+ return value(options)
306
+ }
307
+
308
+ if (current.keyword in fakerKeywordMapper) {
309
+ return value()
310
+ }
311
+
312
+ return undefined
313
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,153 @@
1
+ import path from 'node:path'
2
+
3
+ import { FileManager, PluginManager, createPlugin } from '@kubb/core'
4
+ import { camelCase } from '@kubb/core/transformers'
5
+ import { renderTemplate } from '@kubb/core/utils'
6
+ import { pluginOasName } from '@kubb/plugin-oas'
7
+ import { getGroupedByTagFiles } from '@kubb/plugin-oas/utils'
8
+ import { pluginTsName } from '@kubb/plugin-ts'
9
+
10
+ import { OperationGenerator } from './OperationGenerator.tsx'
11
+ import { SchemaGenerator } from './SchemaGenerator.tsx'
12
+
13
+ import type { Plugin } from '@kubb/core'
14
+ import type { PluginOas } from '@kubb/plugin-oas'
15
+ import type { PluginFaker } from './types.ts'
16
+
17
+ export const pluginFakerName = 'plugin-faker' satisfies PluginFaker['name']
18
+
19
+ export const pluginFaker = createPlugin<PluginFaker>((options) => {
20
+ const {
21
+ output = { path: 'mocks' },
22
+ seed,
23
+ group,
24
+ exclude = [],
25
+ include,
26
+ override = [],
27
+ transformers = {},
28
+ mapper = {},
29
+ dateType = 'string',
30
+ unknownType = 'any',
31
+ dateParser,
32
+ regexGenerator = 'faker',
33
+ } = options
34
+ const template = group?.output ? group.output : `${output.path}/{{tag}}Controller`
35
+
36
+ return {
37
+ name: pluginFakerName,
38
+ options: {
39
+ extName: output.extName,
40
+ transformers,
41
+ dateType,
42
+ seed,
43
+ unknownType,
44
+ dateParser,
45
+ mapper,
46
+ override,
47
+ regexGenerator,
48
+ },
49
+ pre: [pluginOasName, pluginTsName],
50
+ resolvePath(baseName, pathMode, options) {
51
+ const root = path.resolve(this.config.root, this.config.output.path)
52
+ const mode = pathMode ?? FileManager.getMode(path.resolve(root, output.path))
53
+
54
+ if (mode === 'single') {
55
+ /**
56
+ * when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
57
+ * Other plugins then need to call addOrAppend instead of just add from the fileManager class
58
+ */
59
+ return path.resolve(root, output.path)
60
+ }
61
+
62
+ if (options?.tag && group?.type === 'tag') {
63
+ const tag = camelCase(options.tag)
64
+
65
+ return path.resolve(root, renderTemplate(template, { tag }), baseName)
66
+ }
67
+
68
+ return path.resolve(root, output.path, baseName)
69
+ },
70
+ resolveName(name, type) {
71
+ const resolvedName = camelCase(name, {
72
+ prefix: type ? 'create' : undefined,
73
+ isFile: type === 'file',
74
+ })
75
+
76
+ if (type) {
77
+ return transformers?.name?.(resolvedName, type) || resolvedName
78
+ }
79
+
80
+ return resolvedName
81
+ },
82
+ async writeFile(path, source) {
83
+ if (!path.endsWith('.ts') || !source) {
84
+ return
85
+ }
86
+
87
+ return this.fileManager.write(path, source, { sanity: false })
88
+ },
89
+ async buildStart() {
90
+ const [swaggerPlugin]: [Plugin<PluginOas>] = PluginManager.getDependedPlugins<PluginOas>(this.plugins, [pluginOasName])
91
+
92
+ const oas = await swaggerPlugin.api.getOas()
93
+ const root = path.resolve(this.config.root, this.config.output.path)
94
+ const mode = FileManager.getMode(path.resolve(root, output.path))
95
+
96
+ const schemaGenerator = new SchemaGenerator(this.plugin.options, {
97
+ oas,
98
+ pluginManager: this.pluginManager,
99
+ plugin: this.plugin,
100
+ contentType: swaggerPlugin.api.contentType,
101
+ include: undefined,
102
+ override,
103
+ mode,
104
+ output: output.path,
105
+ })
106
+
107
+ const schemaFiles = await schemaGenerator.build()
108
+ await this.addFile(...schemaFiles)
109
+
110
+ const operationGenerator = new OperationGenerator(this.plugin.options, {
111
+ oas,
112
+ pluginManager: this.pluginManager,
113
+ plugin: this.plugin,
114
+ contentType: swaggerPlugin.api.contentType,
115
+ exclude,
116
+ include,
117
+ override,
118
+ mode,
119
+ })
120
+
121
+ const operationFiles = await operationGenerator.build()
122
+ await this.addFile(...operationFiles)
123
+ },
124
+ async buildEnd() {
125
+ if (this.config.output.write === false) {
126
+ return
127
+ }
128
+
129
+ const root = path.resolve(this.config.root, this.config.output.path)
130
+
131
+ if (group?.type === 'tag') {
132
+ const rootFiles = await getGroupedByTagFiles({
133
+ logger: this.logger,
134
+ files: this.fileManager.files,
135
+ plugin: this.plugin,
136
+ template,
137
+ exportAs: group.exportAs || '{{tag}}Mocks',
138
+ root,
139
+ output,
140
+ })
141
+
142
+ await this.addFile(...rootFiles)
143
+ }
144
+
145
+ await this.fileManager.addIndexes({
146
+ root,
147
+ output,
148
+ meta: { pluginKey: this.plugin.key },
149
+ logger: this.logger,
150
+ })
151
+ },
152
+ }
153
+ })
package/src/types.ts ADDED
@@ -0,0 +1,136 @@
1
+ import type { Plugin, PluginFactoryOptions, ResolveNameParams } from '@kubb/core'
2
+ import type * as KubbFile from '@kubb/fs/types'
3
+
4
+ import type { SchemaObject } from '@kubb/oas'
5
+ import type { Exclude, Include, Override, ResolvePathOptions, Schema } from '@kubb/plugin-oas'
6
+
7
+ export type Options = {
8
+ output?: {
9
+ /**
10
+ * Relative path to save the Faker mocks.
11
+ * When output is a file it will save all models inside that file else it will create a file per schema item.
12
+ * @default 'mocks'
13
+ */
14
+ path: string
15
+ /**
16
+ * Name to be used for the `export * as {{exportAs}} from './'`
17
+ */
18
+ exportAs?: string
19
+ /**
20
+ * Add an extension to the generated imports and exports, default it will not use an extension
21
+ */
22
+ extName?: KubbFile.Extname
23
+ /**
24
+ * Define what needs to exported, here you can also disable the export of barrel files
25
+ * @default `'barrel'`
26
+ */
27
+ exportType?: 'barrel' | 'barrelNamed' | false
28
+ }
29
+
30
+ /**
31
+ * Group the Faker mocks based on the provided name.
32
+ */
33
+ group?: {
34
+ /**
35
+ * Tag will group based on the operation tag inside the Swagger file
36
+ */
37
+ type: 'tag'
38
+ /**
39
+ * Relative path to save the grouped Faker mocks.
40
+ *
41
+ * `{{tag}}` will be replaced by the current tagName.
42
+ * @example `${output}/{{tag}}Controller` => `mocks/PetController`
43
+ * @default `${output}/{{tag}}Controller`
44
+ */
45
+ output?: string
46
+ /**
47
+ * Name to be used for the `export * as {{exportAs}} from './`
48
+ * @default `"{{tag}}Mocks"`
49
+ */
50
+ exportAs?: string
51
+ }
52
+ /**
53
+ * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
54
+ */
55
+ exclude?: Array<Exclude>
56
+ /**
57
+ * Array containing include parameters to include tags/operations/methods/paths.
58
+ */
59
+ include?: Array<Include>
60
+ /**
61
+ * Array containing override parameters to override `options` based on tags/operations/methods/paths.
62
+ */
63
+ override?: Array<Override<ResolvedOptions>>
64
+ /**
65
+ * Choose to use `date` or `datetime` as JavaScript `Date` instead of `string`.
66
+ * @default 'string'
67
+ */
68
+ dateType?: 'string' | 'date'
69
+ /**
70
+ * Which parser should be used when dateType is set to 'string'.
71
+ * - Schema with format 'date' will use ISO date format (YYYY-MM-DD)
72
+ * - `'dayjs'` will use `dayjs(faker.date.anytime()).format("YYYY-MM-DD")`.
73
+ * - `undefined` will use `faker.date.anytime().toString()`
74
+ * - Schema with format 'time' will use ISO time format (HH:mm:ss[.SSSSSS])
75
+ * - `'dayjs'` will use `dayjs(faker.date.anytime()).format("HH:mm:ss")`.
76
+ * - `undefined` will use `faker.date.anytime().toString()`
77
+ * * @default undefined
78
+ */
79
+ dateParser?: 'dayjs' | 'moment' | (string & {})
80
+ /**
81
+ * Which type to use when the Swagger/OpenAPI file is not providing more information
82
+ * @default 'any'
83
+ */
84
+ unknownType?: 'any' | 'unknown'
85
+ transformers?: {
86
+ /**
87
+ * Customize the names based on the type that is provided by the plugin.
88
+ */
89
+ name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
90
+ /**
91
+ * Receive schema and baseName(propertName) and return FakerMeta array
92
+ * TODO TODO add docs
93
+ * @beta
94
+ */
95
+ schema?: (props: { schema?: SchemaObject; name?: string; parentName?: string }, defaultSchemas: Schema[]) => Schema[] | undefined
96
+ }
97
+ /**
98
+ * Choose which generator to use when using Regexp.
99
+ *
100
+ * `'faker'` will use `faker.helpers.fromRegExp(new RegExp(/test/))`
101
+ * `'randexp'` will use `new RandExp(/test/).gen()`
102
+ * @default 'faker'
103
+ */
104
+ regexGenerator?: 'faker' | 'randexp'
105
+
106
+ mapper?: Record<string, string>
107
+ /**
108
+ * The use of Seed is intended to allow for consistent values in a test.
109
+ */
110
+ seed?: number | number[]
111
+ }
112
+
113
+ type ResolvedOptions = {
114
+ extName: KubbFile.Extname | undefined
115
+ dateType: NonNullable<Options['dateType']>
116
+ dateParser: Options['dateParser']
117
+ unknownType: NonNullable<Options['unknownType']>
118
+ transformers: NonNullable<Options['transformers']>
119
+ override: NonNullable<Options['override']>
120
+ seed: NonNullable<Options['seed']> | undefined
121
+ mapper: NonNullable<Options['mapper']>
122
+ regexGenerator: NonNullable<Options['regexGenerator']>
123
+ }
124
+
125
+ export type FileMeta = {
126
+ pluginKey?: Plugin['key']
127
+ tag?: string
128
+ }
129
+
130
+ export type PluginFaker = PluginFactoryOptions<'plugin-faker', Options, ResolvedOptions, never, ResolvePathOptions>
131
+
132
+ declare module '@kubb/core' {
133
+ export interface _Register {
134
+ ['@kubb/plugin-faker']: PluginFaker
135
+ }
136
+ }