@amritk/generate-parsers 0.5.0 → 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.
@@ -34,9 +34,9 @@ describe('build-schema', () => {
34
34
  expect(filenames).toContain('contact.ts')
35
35
  expect(filenames).toContain('schema.ts')
36
36
  const schemaFile = result.find((file) => file.filename === 'schema.ts')
37
- // The generated schema.ts should declare SchemaObject and parseSchemaObject from the user's $defs/schema
38
- expect(schemaFile?.content).toContain('export type SchemaObject')
39
- expect(schemaFile?.content).toContain('parseSchemaObject')
37
+ // The generated schema.ts should declare Schema and parseSchema from the user's $defs/schema
38
+ expect(schemaFile?.content).toContain('export type Schema')
39
+ expect(schemaFile?.content).toContain('parseSchema')
40
40
  })
41
41
 
42
42
  it('applies extensions to matching definitions during build', async () => {
@@ -193,8 +193,8 @@ describe('build-schema', () => {
193
193
  const result = await buildSchema(schema, 'Document', undefined, true)
194
194
  const documentFile = result.find((file) => file.filename === 'document.ts')
195
195
 
196
- expect(documentFile?.content).toContain("import type { ContactObject } from './contact';")
197
- expect(documentFile?.content).not.toContain('parseContactObject')
196
+ expect(documentFile?.content).toContain("import type { Contact } from './contact';")
197
+ expect(documentFile?.content).not.toContain('parseContact')
198
198
  })
199
199
 
200
200
  it('still generates files for all $ref definitions in types-only mode', async () => {
@@ -246,8 +246,8 @@ describe('build-schema', () => {
246
246
  const result = await buildSchema(schema, 'Document', undefined, true)
247
247
  const contactFile = result.find((file) => file.filename === 'contact.ts')
248
248
 
249
- expect(contactFile?.content).toContain('export type ContactObject')
250
- expect(contactFile?.content).not.toContain('export const parseContactObject')
249
+ expect(contactFile?.content).toContain('export type Contact')
250
+ expect(contactFile?.content).not.toContain('export const parseContact')
251
251
  })
252
252
 
253
253
  it('applies extensions correctly in types-only mode', async () => {
@@ -273,7 +273,7 @@ describe('build-schema', () => {
273
273
  // Extension property should still appear in the type
274
274
  expect(parameterFile?.content).toContain("'x-enabled'")
275
275
  // But no parser should be generated
276
- expect(parameterFile?.content).not.toContain('parseParameterObject')
276
+ expect(parameterFile?.content).not.toContain('parseParameter')
277
277
  })
278
278
 
279
279
  it('warns and skips when a $ref cannot be resolved', async () => {
@@ -322,7 +322,7 @@ describe('build-schema', () => {
322
322
  // Own property should be present in the object literal
323
323
  expect(documentFile?.content).toContain('name?:')
324
324
  // allOf ref generates an intersection type with the referenced type
325
- expect(documentFile?.content).toContain('TaggableObject')
325
+ expect(documentFile?.content).toContain('Taggable')
326
326
  // The taggable definition should generate its own file
327
327
  const filenames = result.map((f) => f.filename)
328
328
  expect(filenames).toContain('taggable.ts')
@@ -356,11 +356,11 @@ describe('build-schema', () => {
356
356
  const documentFile = result.find((file) => file.filename === 'document.ts')
357
357
  const filenames = result.map((f) => f.filename)
358
358
 
359
- // ExamplesObject IS imported since allOf generates an intersection type
360
- expect(documentFile?.content).toContain('ExamplesObject')
359
+ // Examples IS imported since allOf generates an intersection type
360
+ expect(documentFile?.content).toContain('Examples')
361
361
  // The examples definition generates its own file
362
362
  expect(filenames).toContain('examples.ts')
363
- // ExampleItemObject is also resolved from additionalProperties
363
+ // ExampleItem is also resolved from additionalProperties
364
364
  expect(filenames).toContain('example-item.ts')
365
365
  })
366
366
 
@@ -435,7 +435,7 @@ describe('build-schema', () => {
435
435
  const filenames = result.map((f) => f.filename)
436
436
 
437
437
  // specification-extensions is now treated like any other allOf ref
438
- expect(documentFile?.content).toContain('SpecificationExtensionsObject')
438
+ expect(documentFile?.content).toContain('SpecificationExtensions')
439
439
  // The specification-extensions definition generates its own file
440
440
  expect(filenames).toContain('specification-extensions.ts')
441
441
  // x-custom property is NOT inlined into document.ts — it's in specification-extensions.ts
@@ -546,7 +546,7 @@ describe('build-schema', () => {
546
546
  // Both files are generated independently — they map to different filenames now
547
547
  expect(filenames).toContain('parameter.ts')
548
548
  expect(filenames).toContain('parameter-or-reference.ts')
549
- // The parameter.ts should contain the real ParameterObject type
549
+ // The parameter.ts should contain the real Parameter type
550
550
  const parameterFile = result.find((f) => f.filename === 'parameter.ts')
551
551
  expect(parameterFile?.content).toContain('name')
552
552
  expect(parameterFile?.content).toContain('in')
@@ -572,7 +572,7 @@ describe('build-schema', () => {
572
572
  expect(indexFile).toBeDefined()
573
573
  // Named type + parser + shape-validator exports for each file
574
574
  expect(indexFile?.content).toContain(
575
- "export { type ContactObject, validateContactObjectShape, parseContactObject } from './contact';",
575
+ "export { type Contact, validateContactShape, parseContact } from './contact';",
576
576
  )
577
577
  expect(indexFile?.content).toContain(
578
578
  "export { type Document, validateDocumentShape, parseDocument } from './document';",
@@ -601,9 +601,9 @@ describe('build-schema', () => {
601
601
 
602
602
  expect(indexFile).toBeDefined()
603
603
  // Types-only: export type { ... } syntax, no parsers
604
- expect(indexFile?.content).toContain("export type { ContactObject } from './contact';")
604
+ expect(indexFile?.content).toContain("export type { Contact } from './contact';")
605
605
  expect(indexFile?.content).toContain("export type { Document } from './document';")
606
- expect(indexFile?.content).not.toContain('parseContactObject')
606
+ expect(indexFile?.content).not.toContain('parseContact')
607
607
  expect(indexFile?.content).not.toContain('parseDocument')
608
608
  // No wildcard exports
609
609
  expect(indexFile?.content).not.toContain('export *')
@@ -96,6 +96,9 @@ const extractDynamicAnchorDefs = (schema: JSONSchema): string[] => {
96
96
  * directory while the helper sources are emitted once at the output root.
97
97
  * @param readonly - When true, every property, array, and record in the generated type
98
98
  * definitions is emitted as `readonly`.
99
+ * @param typeSuffix - Suffix appended to every type/parser name derived from a `$ref`
100
+ * (e.g. `'Object'` → `ContactObject`). Defaults to `''` (no suffix). The root type name
101
+ * is used verbatim and is not affected by this suffix.
99
102
  * @returns An array of generated TypeScript files
100
103
  *
101
104
  * @example
@@ -138,6 +141,7 @@ export const buildSchema = async (
138
141
  helpersMode: HelpersMode = 'package',
139
142
  helpersImportPrefix = './',
140
143
  readonly = false,
144
+ typeSuffix = '',
141
145
  ): Promise<GeneratedFile[]> => {
142
146
  // Upgrade draft-07 schemas to 2020-12 conventions before processing.
143
147
  // This renames `definitions` → `$defs` recursively so the rest of the
@@ -165,6 +169,7 @@ export const buildSchema = async (
165
169
  helpersMode,
166
170
  helpersImportPrefix,
167
171
  readonly,
172
+ typeSuffix,
168
173
  ...(logWarnings !== undefined ? { logWarnings } : {}),
169
174
  ...(strict !== undefined ? { strict } : {}),
170
175
  })
@@ -206,7 +211,7 @@ export const buildSchema = async (
206
211
 
207
212
  // Generate file for this ref, resolving any $dynamicRef to $ref first
208
213
  // and applying any matching extensions for this definition
209
- const typeName = refToName(ref)
214
+ const typeName = refToName(ref, typeSuffix)
210
215
  const filename = refToFilename(ref)
211
216
  const processedSchema = resolveDynamicRefs(resolvedSchema as JSONSchema, dynamicRefMap)
212
217
  const extendedSchema = extensions ? applySchemaExtensions(processedSchema, filename, extensions) : processedSchema
@@ -217,6 +222,7 @@ export const buildSchema = async (
217
222
  helpersMode,
218
223
  helpersImportPrefix,
219
224
  readonly,
225
+ typeSuffix,
220
226
  ...(logWarnings !== undefined ? { logWarnings } : {}),
221
227
  ...(strict !== undefined ? { strict } : {}),
222
228
  })
@@ -13,9 +13,7 @@ describe('generate-files', () => {
13
13
 
14
14
  const { content: result } = generateFile(schema, 'Document')
15
15
 
16
- expect(result).toContain(
17
- "import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
18
- )
16
+ expect(result).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
19
17
  })
20
18
 
21
19
  it('calls imported parser for optional $ref property', () => {
@@ -29,7 +27,7 @@ describe('generate-files', () => {
29
27
  const { content: result } = generateFile(schema, 'Document')
30
28
 
31
29
  // With local variable caching, optional $ref properties use the cached variable
32
- expect(result).toContain('_contact !== undefined && { contact: parseContactObject(_contact) }')
30
+ expect(result).toContain('_contact !== undefined && { contact: parseContact(_contact) }')
33
31
  })
34
32
 
35
33
  it('calls imported parser for required $ref property', () => {
@@ -44,7 +42,7 @@ describe('generate-files', () => {
44
42
  const { content: result } = generateFile(schema, 'Document')
45
43
 
46
44
  // With local variable caching, required $ref properties use the cached variable
47
- expect(result).toContain('contact: parseContactObject(_contact),')
45
+ expect(result).toContain('contact: parseContact(_contact),')
48
46
  })
49
47
 
50
48
  it('generates combined imports for multiple $ref properties', () => {
@@ -58,12 +56,8 @@ describe('generate-files', () => {
58
56
 
59
57
  const { content: result } = generateFile(schema, 'Document')
60
58
 
61
- expect(result).toContain(
62
- "import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
63
- )
64
- expect(result).toContain(
65
- "import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
66
- )
59
+ expect(result).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
60
+ expect(result).toContain("import { type Server, parseServer, validateServerShape } from './server';")
67
61
  })
68
62
 
69
63
  it('generates correct parser name for multi-word $ref', () => {
@@ -77,9 +71,9 @@ describe('generate-files', () => {
77
71
  const { content: result } = generateFile(schema, 'Document')
78
72
 
79
73
  expect(result).toContain(
80
- "import { type ExternalDocumentationObject, parseExternalDocumentationObject, validateExternalDocumentationObjectShape } from './external-documentation';",
74
+ "import { type ExternalDocumentation, parseExternalDocumentation, validateExternalDocumentationShape } from './external-documentation';",
81
75
  )
82
- expect(result).toContain('parseExternalDocumentationObject(_externalDoc)')
76
+ expect(result).toContain('parseExternalDocumentation(_externalDoc)')
83
77
  })
84
78
 
85
79
  it('does not generate ref imports when no $ref properties exist', () => {
@@ -113,7 +107,7 @@ describe('generate-files', () => {
113
107
  // Non-ref property should have inline validation against the cached local
114
108
  expect(result).toContain('typeof _name === "string"')
115
109
  // Ref property should call imported parser using cached variable
116
- expect(result).toContain('parseContactObject(_contact)')
110
+ expect(result).toContain('parseContact(_contact)')
117
111
  })
118
112
 
119
113
  it('generates a correct import for -or-reference style $ref names', () => {
@@ -128,9 +122,9 @@ describe('generate-files', () => {
128
122
  const { content: result } = generateFile(schema, 'Document')
129
123
 
130
124
  expect(result).toContain(
131
- "import { type CallbacksOrReferenceObject, parseCallbacksOrReferenceObject, validateCallbacksOrReferenceObjectShape } from './callbacks-or-reference';",
125
+ "import { type CallbacksOrReference, parseCallbacksOrReference, validateCallbacksOrReferenceShape } from './callbacks-or-reference';",
132
126
  )
133
- expect(result).toContain('parseCallbacksOrReferenceObject(_callbacks)')
127
+ expect(result).toContain('parseCallbacksOrReference(_callbacks)')
134
128
  })
135
129
 
136
130
  it('generates arrow function parser', () => {
@@ -160,11 +154,9 @@ describe('generate-files', () => {
160
154
 
161
155
  const { content: result } = generateFile(schema, 'Document')
162
156
 
163
- expect(result).toContain(
164
- "import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
165
- )
157
+ expect(result).toContain("import { type Server, parseServer, validateServerShape } from './server';")
166
158
  // With local variable caching, array validators use the cached variable
167
- expect(result).toContain('validateArray(_servers, parseServerObject)')
159
+ expect(result).toContain('validateArray(_servers, parseServer)')
168
160
  })
169
161
 
170
162
  it('maps array items through imported parser for required array with $ref items', () => {
@@ -182,14 +174,12 @@ describe('generate-files', () => {
182
174
  const { content: result } = generateFile(schema, 'Document')
183
175
 
184
176
  // Check for required imports
185
- expect(result).toContain(
186
- "import { type ServerObject, parseServerObject, validateServerObjectShape } from './server';",
187
- )
177
+ expect(result).toContain("import { type Server, parseServer, validateServerShape } from './server';")
188
178
  expect(result).toContain("import { validateArray } from '@amritk/helpers/validate-array';")
189
179
  expect(result).toContain("import { isObject } from '@amritk/helpers/is-object';")
190
180
 
191
181
  // Check for required array validation using cached variable
192
- expect(result).toContain('servers: validateArray(_servers, parseServerObject),')
182
+ expect(result).toContain('servers: validateArray(_servers, parseServer),')
193
183
 
194
184
  // Should not use spread operator for required fields
195
185
  expect(result).not.toContain('...(_servers')
@@ -261,8 +251,8 @@ describe('generate-files', () => {
261
251
 
262
252
  const { content: result } = generateFile(schema, 'Document', { typesOnly: true })
263
253
 
264
- expect(result).toContain("import type { ContactObject } from './contact';")
265
- expect(result).not.toContain('parseContactObject')
254
+ expect(result).toContain("import type { Contact } from './contact';")
255
+ expect(result).not.toContain('parseContact')
266
256
  })
267
257
 
268
258
  it('does not include runtime helper imports in types-only mode', () => {
@@ -311,8 +301,8 @@ describe('generate-files', () => {
311
301
 
312
302
  const { content: result } = generateFile(schema, 'Document', { typesOnly: true })
313
303
 
314
- expect(result).toContain("import type { ServerObject } from './server';")
315
- expect(result).not.toContain('parseServerObject')
304
+ expect(result).toContain("import type { Server } from './server';")
305
+ expect(result).not.toContain('parseServer')
316
306
  expect(result).not.toContain('validateArray')
317
307
  })
318
308
 
@@ -324,8 +314,8 @@ describe('generate-files', () => {
324
314
 
325
315
  const { content: result } = generateFile(schema, 'Paths', { typesOnly: true })
326
316
 
327
- expect(result).toContain("import type { PathItemObject } from './path-item';")
328
- expect(result).not.toContain('parsePathItemObject')
317
+ expect(result).toContain("import type { PathItem } from './path-item';")
318
+ expect(result).not.toContain('parsePathItem')
329
319
  expect(result).not.toContain('validateRecord')
330
320
  })
331
321
 
@@ -336,10 +326,10 @@ describe('generate-files', () => {
336
326
 
337
327
  const { content: result } = generateFile(schema, 'Pet', { typesOnly: true })
338
328
 
339
- expect(result).toContain("import type { CatObject } from './cat';")
340
- expect(result).toContain("import type { DogObject } from './dog';")
341
- expect(result).not.toContain('parseCatObject')
342
- expect(result).not.toContain('parseDogObject')
329
+ expect(result).toContain("import type { Cat } from './cat';")
330
+ expect(result).toContain("import type { Dog } from './dog';")
331
+ expect(result).not.toContain('parseCat')
332
+ expect(result).not.toContain('parseDog')
343
333
  })
344
334
 
345
335
  it('generates type-only imports for anyOf $ref variants in types-only mode', () => {
@@ -354,10 +344,10 @@ describe('generate-files', () => {
354
344
 
355
345
  const { content: result } = generateFile(schema, 'Wrapper', { typesOnly: true })
356
346
 
357
- expect(result).toContain("import type { StringValueObject } from './string-value';")
358
- expect(result).toContain("import type { NumberValueObject } from './number-value';")
359
- expect(result).not.toContain('parseStringValueObject')
360
- expect(result).not.toContain('parseNumberValueObject')
347
+ expect(result).toContain("import type { StringValue } from './string-value';")
348
+ expect(result).toContain("import type { NumberValue } from './number-value';")
349
+ expect(result).not.toContain('parseStringValue')
350
+ expect(result).not.toContain('parseNumberValue')
361
351
  })
362
352
 
363
353
  it('does not change default behavior when typesOnly is false', () => {
@@ -372,16 +362,14 @@ describe('generate-files', () => {
372
362
  const { content: explicitFalseResult } = generateFile(schema, 'Document', { typesOnly: false })
373
363
 
374
364
  expect(defaultResult).toBe(explicitFalseResult)
375
- expect(defaultResult).toContain(
376
- "import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
377
- )
365
+ expect(defaultResult).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
378
366
  expect(defaultResult).toContain('export const parseDocument')
379
367
  })
380
368
 
381
369
  it('does not generate a self-import for recursive schemas in types-only mode', () => {
382
- // Mirrors the 3.2.0 encoding schema: EncodingObject has properties itemEncoding, prefixEncoding,
370
+ // Mirrors the 3.2.0 encoding schema: Encoding has properties itemEncoding, prefixEncoding,
383
371
  // and encoding that all $ref back to #/$defs/encoding. The generated encoding.ts must not
384
- // import EncodingObject from itself.
372
+ // import Encoding from itself.
385
373
  const schema = {
386
374
  type: 'object',
387
375
  properties: {
@@ -401,14 +389,14 @@ describe('generate-files', () => {
401
389
  },
402
390
  }
403
391
 
404
- const { content: result } = generateFile(schema, 'EncodingObject', {
392
+ const { content: result } = generateFile(schema, 'Encoding', {
405
393
  typesOnly: true,
406
394
  selfRef: '#/$defs/encoding',
407
395
  })
408
396
 
409
- expect(result).not.toContain("import type { EncodingObject } from './encoding';")
410
- expect(result).toContain("import type { HeaderOrReferenceObject } from './header-or-reference';")
411
- expect(result).toContain('export type EncodingObject')
397
+ expect(result).not.toContain("import type { Encoding } from './encoding';")
398
+ expect(result).toContain("import type { HeaderOrReference } from './header-or-reference';")
399
+ expect(result).toContain('export type Encoding')
412
400
  })
413
401
 
414
402
  it('does not generate a self-import for recursive schemas in full parser mode', () => {
@@ -423,13 +411,38 @@ describe('generate-files', () => {
423
411
  },
424
412
  }
425
413
 
426
- const { content: result } = generateFile(schema, 'EncodingObject', { selfRef: '#/$defs/encoding' })
414
+ const { content: result } = generateFile(schema, 'Encoding', { selfRef: '#/$defs/encoding' })
427
415
 
428
- expect(result).not.toContain(
429
- "import { type EncodingObject, parseEncodingObject, validateEncodingObjectShape } from './encoding';",
430
- )
416
+ expect(result).not.toContain("import { type Encoding, parseEncoding, validateEncodingShape } from './encoding';")
431
417
  expect(result).toContain(
432
- "import { type HeaderOrReferenceObject, parseHeaderOrReferenceObject, validateHeaderOrReferenceObjectShape } from './header-or-reference';",
418
+ "import { type HeaderOrReference, parseHeaderOrReference, validateHeaderOrReferenceShape } from './header-or-reference';",
433
419
  )
434
420
  })
421
+
422
+ describe('typeSuffix', () => {
423
+ const schema = {
424
+ type: 'object',
425
+ properties: {
426
+ contact: { $ref: '#/$defs/contact' },
427
+ },
428
+ required: ['contact'],
429
+ }
430
+
431
+ it('appends the suffix to ref-derived type, parser, and import names', () => {
432
+ const { content: result } = generateFile(schema, 'Document', { typeSuffix: 'Object' })
433
+
434
+ expect(result).toContain(
435
+ "import { type ContactObject, parseContactObject, validateContactObjectShape } from './contact';",
436
+ )
437
+ expect(result).toContain('contact: ContactObject;')
438
+ expect(result).toContain('contact: parseContactObject(_contact)')
439
+ })
440
+
441
+ it('emits no suffix by default', () => {
442
+ const { content: result } = generateFile(schema, 'Document')
443
+
444
+ expect(result).toContain("import { type Contact, parseContact, validateContactShape } from './contact';")
445
+ expect(result).not.toContain('ContactObject')
446
+ })
447
+ })
435
448
  })
@@ -59,6 +59,11 @@ type GenerateFileOptions = {
59
59
  * definitions is emitted as `readonly`.
60
60
  */
61
61
  readonly readonly?: boolean
62
+ /**
63
+ * Suffix appended to every type/parser name derived from a `$ref`.
64
+ * Defaults to `''` (no suffix). Set to e.g. `'Object'` to emit `ContactObject`.
65
+ */
66
+ readonly typeSuffix?: string
62
67
  }
63
68
 
64
69
  /** Result of generating a single parser file. */
@@ -105,11 +110,12 @@ export const generateFile = (
105
110
  const selfRef = options?.selfRef
106
111
  const rootSchema = options?.rootSchema
107
112
  const helpersMode: HelpersMode = options?.helpersMode ?? 'package'
108
- const typeDefinition = generateTypeDefinition(schema, typeName, { readonly: options?.readonly ?? false })
113
+ const typeSuffix = options?.typeSuffix ?? ''
114
+ const typeDefinition = generateTypeDefinition(schema, typeName, { readonly: options?.readonly ?? false, typeSuffix })
109
115
 
110
116
  if (typesOnly) {
111
117
  // In types-only mode, skip the parser function and use type-only imports
112
- const imports = collectImports(schema, { typesOnly: true, selfRef, rootSchema })
118
+ const imports = collectImports(schema, { typesOnly: true, selfRef, rootSchema, typeSuffix })
113
119
  let result = ''
114
120
 
115
121
  if (imports.length > 0) {
@@ -125,13 +131,14 @@ export const generateFile = (
125
131
 
126
132
  const parserFunction = generateParserFunction(schema, typeName, {
127
133
  useRefImports: true,
134
+ typeSuffix,
128
135
  ...(options?.logWarnings !== undefined ? { logWarnings: options.logWarnings } : {}),
129
136
  ...(options?.strict !== undefined ? { strict: options.strict } : {}),
130
137
  })
131
- const shapeValidator = generateShapeValidator(schema, typeName, true)
138
+ const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix)
132
139
  const combinedFunctions = `${shapeValidator}\n\n${parserFunction}`
133
140
  const helpers: CollectedHelpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix)
134
- const imports = [...collectImports(schema, { selfRef, rootSchema }), ...helpers.imports]
141
+ const imports = [...collectImports(schema, { selfRef, rootSchema, typeSuffix }), ...helpers.imports]
135
142
 
136
143
  // Build file output using string concatenation instead of array join for performance
137
144
  let result = ''