@specbook/codegen 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/build.ts +7 -22
- package/src/schema.ts +1 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specbook/codegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Turns an OpenAPI or Swagger document into types, examples, schemas and metadata, in Node or in a browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"specbook",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"swagger2openapi": "7.0.8"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@specbook/core": "0.
|
|
43
|
+
"@specbook/core": "0.5.0",
|
|
44
44
|
"@types/node": "24.19.2",
|
|
45
45
|
"@types/swagger2openapi": "7.0.4",
|
|
46
46
|
"typescript": "7.0.2"
|
package/src/build.ts
CHANGED
|
@@ -397,17 +397,16 @@ export function generateFromDoc(
|
|
|
397
397
|
if (defaultType) responseTypes.push(`${st}: ${defaultType}`)
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
|
-
// ---- request examples: spec-defined first, then generated default
|
|
400
|
+
// ---- request examples: spec-defined first, then the generated default
|
|
401
401
|
const fileFieldPaths = body?.encoding === 'multipart' ? fileFields(body.schema, resolve) : []
|
|
402
402
|
const wholeBodyFile = body?.encoding === 'binary'
|
|
403
403
|
const sample = `./${SAMPLE_FILE}`
|
|
404
|
-
const paramsFor = (
|
|
404
|
+
const paramsFor = () => {
|
|
405
405
|
const pick = (where: string) => {
|
|
406
406
|
const out: Record<string, unknown> = {}
|
|
407
407
|
for (const p of group(where)) {
|
|
408
408
|
if (p.example !== undefined) out[p.name] = p.example
|
|
409
|
-
else if (p.required)
|
|
410
|
-
out[p.name] = synthesize(p.schema, resolve, 0, [], p.name, undefined, fake)
|
|
409
|
+
else if (p.required) out[p.name] = synthesize(p.schema, resolve, 0, [], p.name)
|
|
411
410
|
}
|
|
412
411
|
return Object.keys(out).length ? out : undefined
|
|
413
412
|
}
|
|
@@ -433,7 +432,7 @@ export function generateFromDoc(
|
|
|
433
432
|
title: ex.summary ?? key,
|
|
434
433
|
summary: ex.description,
|
|
435
434
|
source: 'spec',
|
|
436
|
-
...paramsFor(
|
|
435
|
+
...paramsFor(),
|
|
437
436
|
body: bodyFor(sanitizeExample(ex.value)),
|
|
438
437
|
})
|
|
439
438
|
}
|
|
@@ -442,30 +441,16 @@ export function generateFromDoc(
|
|
|
442
441
|
name: take('example'),
|
|
443
442
|
title: 'Example',
|
|
444
443
|
source: 'spec',
|
|
445
|
-
...paramsFor(
|
|
444
|
+
...paramsFor(),
|
|
446
445
|
body: bodyFor(sanitizeExample(body.media.example)),
|
|
447
446
|
})
|
|
448
447
|
const plain = {
|
|
449
|
-
...paramsFor(
|
|
448
|
+
...paramsFor(),
|
|
450
449
|
body: body?.schema
|
|
451
|
-
? bodyFor(synthesize(body.schema, resolve, 0, [], '', undefined,
|
|
452
|
-
: undefined,
|
|
453
|
-
}
|
|
454
|
-
const dynamic = {
|
|
455
|
-
...paramsFor(true),
|
|
456
|
-
body: body?.schema
|
|
457
|
-
? bodyFor(synthesize(body.schema, resolve, 0, [], '', undefined, true, true))
|
|
450
|
+
? bodyFor(synthesize(body.schema, resolve, 0, [], '', undefined, true))
|
|
458
451
|
: undefined,
|
|
459
452
|
}
|
|
460
453
|
docs.push({ name: take('default'), title: 'Default', source: 'generated', ...plain })
|
|
461
|
-
if (JSON.stringify(dynamic) !== JSON.stringify(plain))
|
|
462
|
-
docs.push({
|
|
463
|
-
name: take('dynamic'),
|
|
464
|
-
title: 'Dynamic',
|
|
465
|
-
summary: 'Generated values, fresh on every play.',
|
|
466
|
-
source: 'generated',
|
|
467
|
-
...dynamic,
|
|
468
|
-
})
|
|
469
454
|
for (const d of docs)
|
|
470
455
|
exampleFiles.push({
|
|
471
456
|
path: `examples/${id}/${d.name}.example.json`,
|
package/src/schema.ts
CHANGED
|
@@ -247,41 +247,6 @@ function exampleBoolean(key: string): boolean {
|
|
|
247
247
|
)
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
const FAKE_BY_FORMAT: Record<string, string> = {
|
|
251
|
-
email: 'internet.email',
|
|
252
|
-
uuid: 'string.uuid',
|
|
253
|
-
uri: 'internet.url',
|
|
254
|
-
url: 'internet.url',
|
|
255
|
-
hostname: 'internet.domainName',
|
|
256
|
-
ipv4: 'internet.ipv4',
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const FAKE_BY_NAME: [RegExp, string][] = [
|
|
260
|
-
[/^(first_?name|given_?name)$/, 'person.firstName'],
|
|
261
|
-
[/^(last_?name|family_?name|surname)$/, 'person.lastName'],
|
|
262
|
-
[/^(full_?name|display_?name)$/, 'person.fullName'],
|
|
263
|
-
[/^name$/, 'word.noun'],
|
|
264
|
-
[/^(user_?name|login|handle)$/, 'internet.username'],
|
|
265
|
-
[/^e?-?mail(_?address)?$/, 'internet.email'],
|
|
266
|
-
[/^(phone|phone_?number|mobile)$/, 'phone.number'],
|
|
267
|
-
[/^(city|town)$/, 'location.city'],
|
|
268
|
-
[/^country$/, 'location.country'],
|
|
269
|
-
[/^(postal_?code|zip|zip_?code)$/, 'location.zipCode'],
|
|
270
|
-
[/^(line1|address|street|address_?line_?1)$/, 'location.streetAddress'],
|
|
271
|
-
[/^(company|organization|org)$/, 'company.name'],
|
|
272
|
-
[/^(title|subject)$/, 'lorem.sentence'],
|
|
273
|
-
[/^(description|summary|bio|about|body|comment|note|notes|message)$/, 'lorem.sentence'],
|
|
274
|
-
[/^(url|website|homepage)$/, 'internet.url'],
|
|
275
|
-
]
|
|
276
|
-
|
|
277
|
-
/** `{{faker.*}}` placeholder for a string leaf, chosen by format first and then by field name. */
|
|
278
|
-
export function fakerFor(key: string, format?: string): string | undefined {
|
|
279
|
-
const path =
|
|
280
|
-
(format && FAKE_BY_FORMAT[format]) ||
|
|
281
|
-
FAKE_BY_NAME.find(([re]) => re.test(key.toLowerCase()))?.[1]
|
|
282
|
-
return path ? `{{faker.${path}}}` : undefined
|
|
283
|
-
}
|
|
284
|
-
|
|
285
250
|
/** Deepest nesting of objects and arrays an example goes into before it settles for an empty one. */
|
|
286
251
|
const MAX_DEPTH = 6
|
|
287
252
|
|
|
@@ -351,22 +316,17 @@ export function synthesize(
|
|
|
351
316
|
seen: string[] = [],
|
|
352
317
|
key = '',
|
|
353
318
|
status?: number,
|
|
354
|
-
fake = false,
|
|
355
319
|
sparse = false,
|
|
356
320
|
): unknown {
|
|
357
321
|
if (!schema) return null
|
|
358
322
|
const again = (s: Schema | undefined, k = key, d = depth, sn = seen) =>
|
|
359
|
-
synthesize(s, resolve, d, sn, k, status,
|
|
323
|
+
synthesize(s, resolve, d, sn, k, status, sparse)
|
|
360
324
|
if (schema.$ref) {
|
|
361
325
|
const target = resolve(schema.$ref)
|
|
362
326
|
// a reference back into itself carries on as "required fields only", which the depth rules then end
|
|
363
327
|
if (seen.includes(schema.$ref)) return again(target, key, Math.max(depth, MAX_DEPTH), [])
|
|
364
328
|
return again(target, key, depth, [...seen, schema.$ref])
|
|
365
329
|
}
|
|
366
|
-
if (fake && schema.type === 'string' && !schema.enum) {
|
|
367
|
-
const generated = fakerFor(key, schema.format)
|
|
368
|
-
if (generated) return generated
|
|
369
|
-
}
|
|
370
330
|
// a spec example that contradicts its own schema (a string field whose example is an array) is ignored
|
|
371
331
|
if (schema.example !== undefined && fits(schema, schema.example))
|
|
372
332
|
return schema.enum
|