@rvoh/psychic 0.37.0-beta.4 → 0.37.0-beta.6

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 (22) hide show
  1. package/dist/cjs/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +1 -0
  2. package/dist/cjs/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +1 -0
  3. package/dist/cjs/src/error/openapi/NonSerializerSerializerOverrideProvided.js +5 -1
  4. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +40 -34
  5. package/dist/cjs/src/openapi-renderer/body-segment.js +52 -15
  6. package/dist/cjs/src/openapi-renderer/endpoint.js +2 -8
  7. package/dist/cjs/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +47 -3
  8. package/dist/cjs/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +68 -6
  9. package/dist/esm/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +1 -0
  10. package/dist/esm/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +1 -0
  11. package/dist/esm/src/error/openapi/NonSerializerSerializerOverrideProvided.js +5 -1
  12. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +40 -34
  13. package/dist/esm/src/openapi-renderer/body-segment.js +51 -14
  14. package/dist/esm/src/openapi-renderer/endpoint.js +9 -15
  15. package/dist/esm/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +47 -3
  16. package/dist/esm/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +68 -6
  17. package/dist/types/src/helpers/typeHelpers.d.ts +2 -0
  18. package/dist/types/src/openapi-renderer/body-segment.d.ts +51 -14
  19. package/dist/types/src/openapi-renderer/endpoint.d.ts +2 -1
  20. package/dist/types/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.d.ts +39 -0
  21. package/dist/types/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.d.ts +44 -0
  22. package/package.json +2 -2
@@ -103,10 +103,6 @@ export default class SerializerOpenapiRenderer {
103
103
  const DataTypeForOpenapi = $typeForOpenapi;
104
104
  let referencedSerializers = [];
105
105
  let renderedOpenapi = {};
106
- const openapiRenderingOpts = {
107
- casing: this.casing,
108
- suppressResponseEnums: this.suppressResponseEnums,
109
- };
110
106
  renderedOpenapi = this.serializerBuilder['attributes'].reduce((accumulator, attribute) => {
111
107
  const attributeType = attribute.type;
112
108
  let newlyReferencedSerializers = [];
@@ -166,10 +162,11 @@ export default class SerializerOpenapiRenderer {
166
162
  case 'rendersOne': {
167
163
  try {
168
164
  const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
169
- const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, openapiRenderingOpts);
165
+ const { associationOpts, referencedSerializersAndOpenapiSchemaBodyShorthand } = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers);
166
+ const optional = attribute.options.optional ?? associationOpts.optional;
170
167
  newlyReferencedSerializers =
171
168
  referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
172
- if (attribute.options.flatten && attribute.options.optional) {
169
+ if (attribute.options.flatten && optional) {
173
170
  this.allOfSiblings.push({
174
171
  anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
175
172
  });
@@ -179,7 +176,7 @@ export default class SerializerOpenapiRenderer {
179
176
  this.allOfSiblings.push(referencedSerializersAndOpenapiSchemaBodyShorthand.openapi);
180
177
  //
181
178
  }
182
- else if (attribute.options.optional) {
179
+ else if (optional) {
183
180
  accumulator[outputAttributeName] = {
184
181
  anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
185
182
  };
@@ -206,7 +203,7 @@ export default class SerializerOpenapiRenderer {
206
203
  case 'rendersMany': {
207
204
  try {
208
205
  const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
209
- const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, openapiRenderingOpts);
206
+ const { referencedSerializersAndOpenapiSchemaBodyShorthand } = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers);
210
207
  newlyReferencedSerializers =
211
208
  referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
212
209
  accumulator[outputAttributeName] = {
@@ -233,7 +230,7 @@ export default class SerializerOpenapiRenderer {
233
230
  }
234
231
  }
235
232
  })();
236
- const recursiveNewlyReferencedSerializers = newlyReferencedSerializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers, openapiRenderingOpts));
233
+ const recursiveNewlyReferencedSerializers = newlyReferencedSerializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers));
237
234
  referencedSerializers = [
238
235
  ...referencedSerializers,
239
236
  ...newlyReferencedSerializers,
@@ -260,16 +257,19 @@ export default class SerializerOpenapiRenderer {
260
257
  }
261
258
  }
262
259
  }
263
- function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, opts) {
260
+ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers) {
264
261
  const serializerOverride = attribute.options.serializer;
265
262
  if (serializerOverride) {
266
263
  try {
267
264
  return {
268
- referencedSerializers: [
269
- serializerOverride,
270
- ...descendantSerializers(serializerOverride, alreadyExtractedDescendantSerializers, opts),
271
- ],
272
- openapi: new SerializerOpenapiRenderer(serializerOverride, opts).serializerRef,
265
+ associationOpts: { optional: false },
266
+ referencedSerializersAndOpenapiSchemaBodyShorthand: {
267
+ referencedSerializers: [
268
+ serializerOverride,
269
+ ...descendantSerializers(serializerOverride, alreadyExtractedDescendantSerializers),
270
+ ],
271
+ openapi: new SerializerOpenapiRenderer(serializerOverride).serializerRef,
272
+ },
273
273
  };
274
274
  }
275
275
  catch (error) {
@@ -282,8 +282,8 @@ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDesce
282
282
  const association = DataTypeForOpenapi?.isDream &&
283
283
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
284
284
  DataTypeForOpenapi['getAssociationMetadata'](attribute.name);
285
+ const optional = !!association?.optional;
285
286
  if (association) {
286
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
287
287
  associatedClasses = expandStiClasses(association.modelCB());
288
288
  //
289
289
  }
@@ -309,30 +309,36 @@ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDesce
309
309
  associatedClasses = [associatedClass];
310
310
  }
311
311
  }
312
- const serializersOpenapi = associatedClasses.flatMap(associatedClass => inferSerializersFromDreamClassOrViewModelClass(associatedClass, attribute.options.serializerKey));
313
- if (serializersOpenapi.length === 0)
312
+ const serializers = associatedClasses.flatMap(associatedClass => inferSerializersFromDreamClassOrViewModelClass(associatedClass, attribute.options.serializerKey));
313
+ if (serializers.length === 0)
314
314
  throw new NoSerializerFoundForRendersOneAndMany(attribute.name);
315
- if (serializersOpenapi.length === 1) {
316
- const serializer = serializersOpenapi[0];
315
+ if (serializers.length === 1) {
316
+ const serializer = serializers[0];
317
317
  return {
318
- referencedSerializers: [
319
- serializer,
320
- ...descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts),
321
- ],
322
- openapi: new SerializerOpenapiRenderer(serializer, opts).serializerRef,
318
+ associationOpts: { optional },
319
+ referencedSerializersAndOpenapiSchemaBodyShorthand: {
320
+ referencedSerializers: [
321
+ serializer,
322
+ ...descendantSerializers(serializer, alreadyExtractedDescendantSerializers),
323
+ ],
324
+ openapi: new SerializerOpenapiRenderer(serializer).serializerRef,
325
+ },
323
326
  };
324
327
  }
325
328
  return {
326
- referencedSerializers: [
327
- ...serializersOpenapi,
328
- ...serializersOpenapi.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts)),
329
- ],
330
- openapi: {
331
- anyOf: sortBy(uniq(serializersOpenapi.map(serializer => new SerializerOpenapiRenderer(serializer, opts).serializerRef), ref => ref['$ref']), ref => (ref['$ref'] ? ref['$ref'] : inspect(ref, { depth: 2 }))),
329
+ associationOpts: { optional },
330
+ referencedSerializersAndOpenapiSchemaBodyShorthand: {
331
+ referencedSerializers: [
332
+ ...serializers,
333
+ ...serializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers)),
334
+ ],
335
+ openapi: {
336
+ anyOf: sortBy(uniq(serializers.map(serializer => new SerializerOpenapiRenderer(serializer).serializerRef), ref => ref['$ref']), ref => (ref['$ref'] ? ref['$ref'] : inspect(ref, { depth: 2 }))),
337
+ },
332
338
  },
333
339
  };
334
340
  }
335
- function descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts) {
341
+ function descendantSerializers(serializer, alreadyExtractedDescendantSerializers) {
336
342
  // alreadyExtractedDescendantSerializers is used not only to avoid duplicate
337
343
  // work (and thereby speed up OpenAPI spec generation), but also to avoid
338
344
  // infinite loops (a recursive OpenAPI structure is valid)
@@ -340,10 +346,10 @@ function descendantSerializers(serializer, alreadyExtractedDescendantSerializers
340
346
  return [];
341
347
  if (!isDreamSerializer(serializer))
342
348
  throw new AttemptedToDeriveDescendentSerializersFromNonSerializer(serializer);
343
- const immediateDescendantSerializers = new SerializerOpenapiRenderer(serializer, opts).renderedOpenapi(alreadyExtractedDescendantSerializers).referencedSerializers;
349
+ const immediateDescendantSerializers = new SerializerOpenapiRenderer(serializer).renderedOpenapi(alreadyExtractedDescendantSerializers).referencedSerializers;
344
350
  return [
345
351
  ...immediateDescendantSerializers,
346
- ...immediateDescendantSerializers.flatMap(descendantSerializer => descendantSerializers(descendantSerializer, alreadyExtractedDescendantSerializers, opts)),
352
+ ...immediateDescendantSerializers.flatMap(descendantSerializer => descendantSerializers(descendantSerializer, alreadyExtractedDescendantSerializers)),
347
353
  ];
348
354
  }
349
355
  // When attempting to expand STI children, we might call `.serializers` on
@@ -7,20 +7,57 @@ import maybeNullOpenapiShorthandToOpenapiShorthand from './helpers/maybeNullOpen
7
7
  import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
8
8
  import schemaToRef from './helpers/schemaToRef.js';
9
9
  import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
10
- export default class OpenapiBodySegmentRenderer {
10
+ /**
11
+ * @internal
12
+ *
13
+ * Internal class used to recursively expand OpenAPI shorthand notation into full OpenAPI schema objects.
14
+ *
15
+ * This class handles the transformation of various shorthand formats:
16
+ * - Primitive shorthands like `'string'` → `{ type: 'string' }`
17
+ * - Nullable shorthands like `['string', 'null']` → `{ type: ['string', 'null'] }`
18
+ * - Array shorthands like `'string[]'` → `{ type: 'array', items: { type: 'string' } }`
19
+ * - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
20
+ * - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
21
+ * - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
22
+ *
23
+ * The class recursively processes nested structures (objects, arrays, unions) and maintains
24
+ * a collection of referenced serializers that need to be included in the final OpenAPI document.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // Input shorthand
29
+ * {
30
+ * type: 'object',
31
+ * properties: {
32
+ * name: 'string',
33
+ * tags: 'string[]',
34
+ * user: { $serializer: UserSerializer }
35
+ * }
36
+ * }
37
+ *
38
+ * // Output expanded
39
+ * {
40
+ * type: 'object',
41
+ * properties: {
42
+ * name: { type: 'string' },
43
+ * tags: { type: 'array', items: { type: 'string' } },
44
+ * user: { $ref: '#/components/schemas/UserSerializer' }
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ export default class OpenapiSegmentExpander {
11
50
  bodySegment;
12
51
  casing;
13
52
  suppressResponseEnums;
14
53
  target;
15
- openapiName;
16
54
  /**
17
55
  * @internal
18
56
  *
19
- * Used to recursively parse nested object structures
57
+ * Used to recursively expand nested object structures
20
58
  * within nested openapi objects
21
59
  */
22
- constructor(bodySegment, { openapiName, renderOpts, target }) {
23
- this.openapiName = openapiName;
60
+ constructor(bodySegment, { renderOpts, target }) {
24
61
  this.bodySegment = bodySegment;
25
62
  this.casing = renderOpts.casing;
26
63
  this.suppressResponseEnums = renderOpts.suppressResponseEnums;
@@ -36,7 +73,7 @@ export default class OpenapiBodySegmentRenderer {
36
73
  /**
37
74
  * @internal
38
75
  *
39
- * Recursively parses nested objects and arrays,
76
+ * Recursively expand nested objects and arrays,
40
77
  * as well as primitive types
41
78
  */
42
79
  recursivelyParseBody(bodySegment) {
@@ -162,7 +199,7 @@ export default class OpenapiBodySegmentRenderer {
162
199
  /**
163
200
  * @internal
164
201
  *
165
- * recursively parses a oneOf statement
202
+ * recursively expands a oneOf statement
166
203
  */
167
204
  oneOfStatement(bodySegment) {
168
205
  const oneOfBodySegment = bodySegment;
@@ -186,7 +223,7 @@ export default class OpenapiBodySegmentRenderer {
186
223
  /**
187
224
  * @internal
188
225
  *
189
- * recursively parses an anyOf statement
226
+ * recursively expand an anyOf statement
190
227
  */
191
228
  anyOfStatement(bodySegment) {
192
229
  const anyOfBodySegment = bodySegment;
@@ -210,7 +247,7 @@ export default class OpenapiBodySegmentRenderer {
210
247
  /**
211
248
  * @internal
212
249
  *
213
- * recursively parses an allOf statement
250
+ * recursively expand an allOf statement
214
251
  */
215
252
  allOfStatement(bodySegment) {
216
253
  const allOfBodySegment = bodySegment;
@@ -234,7 +271,7 @@ export default class OpenapiBodySegmentRenderer {
234
271
  /**
235
272
  * @internal
236
273
  *
237
- * recursively parses an array statement
274
+ * recursively expand an array statement
238
275
  */
239
276
  arrayStatement(bodySegment) {
240
277
  const results = this.recursivelyParseBody(bodySegment.items);
@@ -251,7 +288,7 @@ export default class OpenapiBodySegmentRenderer {
251
288
  /**
252
289
  * @internal
253
290
  *
254
- * recursively parses an object statement
291
+ * recursively expand an object statement
255
292
  */
256
293
  objectStatement(bodySegment) {
257
294
  const objectBodySegment = bodySegment;
@@ -288,7 +325,7 @@ export default class OpenapiBodySegmentRenderer {
288
325
  /**
289
326
  * @internal
290
327
  *
291
- * parses either the `properties` or `additionalProperties` values
328
+ * expand either the `properties` or `additionalProperties` values
292
329
  * on an object
293
330
  */
294
331
  parseObjectPropertyStatement(propertySegment) {
@@ -311,7 +348,7 @@ export default class OpenapiBodySegmentRenderer {
311
348
  /**
312
349
  * @internal
313
350
  *
314
- * recursively parses a primitive literal type (i.e. string or boolean[])
351
+ * recursively expand a primitive literal type (i.e. string or boolean[])
315
352
  */
316
353
  primitiveLiteralStatement(bodySegment) {
317
354
  return primitiveOpenapiStatementToOpenapi(bodySegment);
@@ -319,7 +356,7 @@ export default class OpenapiBodySegmentRenderer {
319
356
  /**
320
357
  * @internal
321
358
  *
322
- * recursively parses a primitive object type (i.e. { type: 'string[]' })
359
+ * recursively expand a primitive object type (i.e. { type: 'string[]' })
323
360
  */
324
361
  primitiveObjectStatement(bodySegment) {
325
362
  const safeBodySegment = bodySegment;
@@ -3,7 +3,7 @@ import OpenApiFailedToLookupSerializerForEndpoint from '../error/openapi/FailedT
3
3
  import NonSerializerDerivedInOpenapiEndpointRenderer from '../error/openapi/NonSerializerDerivedInOpenapiEndpointRenderer.js';
4
4
  import NonSerializerDerivedInToSchemaObjects from '../error/openapi/NonSerializerDerivedInToSchemaObjects.js';
5
5
  import OpenApiSerializerForEndpointNotAFunction from '../error/openapi/SerializerForEndpointNotAFunction.js';
6
- import OpenapiBodySegmentRenderer from './body-segment.js';
6
+ import OpenapiSegmentExpander from './body-segment.js';
7
7
  import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
8
8
  import openapiOpts from './helpers/openapiOpts.js';
9
9
  import openapiRoute from './helpers/openapiRoute.js';
@@ -277,7 +277,7 @@ export default class OpenapiEndpointRenderer {
277
277
  * Generates the header portion of the openapi payload's
278
278
  * "parameters" field for a single endpoint.
279
279
  */
280
- queryArray({ openapiName, renderOpts, }) {
280
+ queryArray({ renderOpts, }) {
281
281
  const queryParams = Object.keys(this.query || {}).map((queryName) => {
282
282
  const queryParam = this.query[queryName];
283
283
  let output = {
@@ -300,8 +300,7 @@ export default class OpenapiEndpointRenderer {
300
300
  output = {
301
301
  ...output,
302
302
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
303
- schema: new OpenapiBodySegmentRenderer(queryParam.schema, {
304
- openapiName,
303
+ schema: new OpenapiSegmentExpander(queryParam.schema, {
305
304
  renderOpts,
306
305
  target: 'request',
307
306
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -340,8 +339,7 @@ export default class OpenapiEndpointRenderer {
340
339
  if (this.shouldAutogenerateBody()) {
341
340
  return this.generateRequestBodyForModel({ openapiName, renderOpts });
342
341
  }
343
- let schema = new OpenapiBodySegmentRenderer(this.requestBody, {
344
- openapiName,
342
+ let schema = new OpenapiSegmentExpander(this.requestBody, {
345
343
  renderOpts,
346
344
  target: 'request',
347
345
  }).render().openapi;
@@ -406,7 +404,7 @@ export default class OpenapiEndpointRenderer {
406
404
  * that model that are safe to ingest will be automatically added to
407
405
  * the request body.
408
406
  */
409
- generateRequestBodyForModel({ openapiName, renderOpts, }) {
407
+ generateRequestBodyForModel({ renderOpts, }) {
410
408
  const forDreamClass = this.requestBody?.for;
411
409
  const dreamClass = forDreamClass || this.getSingleDreamModelClass();
412
410
  if (!dreamClass)
@@ -518,8 +516,7 @@ export default class OpenapiEndpointRenderer {
518
516
  if (metadata?.type) {
519
517
  paramsShape.properties = {
520
518
  ...paramsShape.properties,
521
- [columnName]: new OpenapiBodySegmentRenderer(metadata.type, {
522
- openapiName,
519
+ [columnName]: new OpenapiSegmentExpander(metadata.type, {
523
520
  renderOpts,
524
521
  target: 'request',
525
522
  }).render().openapi,
@@ -563,8 +560,7 @@ export default class OpenapiEndpointRenderer {
563
560
  }
564
561
  }
565
562
  }
566
- let processedSchema = new OpenapiBodySegmentRenderer(paramsShape, {
567
- openapiName,
563
+ let processedSchema = new OpenapiSegmentExpander(paramsShape, {
568
564
  renderOpts,
569
565
  target: 'request',
570
566
  }).render().openapi;
@@ -588,7 +584,6 @@ export default class OpenapiEndpointRenderer {
588
584
  parseResponses({ openapiName, renderOpts, }) {
589
585
  let responseData = {};
590
586
  const rendererOpts = {
591
- openapiName,
592
587
  renderOpts,
593
588
  target: 'response',
594
589
  };
@@ -623,7 +618,7 @@ export default class OpenapiEndpointRenderer {
623
618
  const response = cloneDeepSafe(this.responses[statusCodeInt], obj => obj);
624
619
  responseData[statusCodeInt] ||= { description: statusDescription(statusCodeInt) };
625
620
  const statusResponse = responseData[statusCodeInt];
626
- const results = new OpenapiBodySegmentRenderer(response, rendererOpts).render();
621
+ const results = new OpenapiSegmentExpander(response, rendererOpts).render();
627
622
  serializersAppearingInHandWrittenOpenapi = [
628
623
  ...serializersAppearingInHandWrittenOpenapi,
629
624
  ...results.referencedSerializers,
@@ -977,8 +972,7 @@ function serializersToSchemaObjects(controllerClass, actionName, serializers, {
977
972
  serializers.forEach(serializer => {
978
973
  const renderer = new SerializerOpenapiRenderer(serializer, renderOpts);
979
974
  const results = renderer.renderedOpenapi(alreadyExtractedDescendantSerializers);
980
- const segmentRendererResults = new OpenapiBodySegmentRenderer(results.openapi, {
981
- openapiName,
975
+ const segmentRendererResults = new OpenapiSegmentExpander(results.openapi, {
982
976
  renderOpts,
983
977
  target: 'response',
984
978
  }).render();
@@ -1,4 +1,46 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
+ import { inferSerializersFromDreamClassOrViewModelClass, } from '@rvoh/dream';
1
4
  import isObject from '../../helpers/isObject.js';
5
+ /**
6
+ * @internal
7
+ *
8
+ * Recursively scans an OpenAPI schema structure and returns an array of all Serializers referenced within it.
9
+ *
10
+ * This function traverses the provided OpenAPI schema definition looking for `$serializer` properties
11
+ * and collects all unique serializer references found. It performs a deep scan of nested objects and
12
+ * arrays to find all serializer references at any level of nesting.
13
+ *
14
+ * **Important**: This function does _not_ recurse into the OpenAPI schemas generated by the discovered
15
+ * Serializers themselves. It only extracts serializers from the hand-written OpenAPI structure provided
16
+ * as input.
17
+ *
18
+ * @param openapi - The OpenAPI schema definition to scan for serializer references
19
+ * @returns An array of unique serializers found in the schema structure
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const schema = {
24
+ * type: 'object',
25
+ * properties: {
26
+ * user: { $serializer: UserSerializer },
27
+ * posts: {
28
+ * type: 'array',
29
+ * items: { $serializer: PostSerializer }
30
+ * },
31
+ * metadata: {
32
+ * type: 'object',
33
+ * properties: {
34
+ * author: { $serializer: UserSerializer } // Duplicate, will be deduplicated
35
+ * }
36
+ * }
37
+ * }
38
+ * }
39
+ *
40
+ * const serializers = allSerializersFromHandWrittenOpenapi(schema)
41
+ * // Returns: [UserSerializer, PostSerializer]
42
+ * ```
43
+ */
2
44
  export default function allSerializersFromHandWrittenOpenapi(openapi) {
3
45
  const serializers = new Set();
4
46
  extractSerializers(openapi, serializers);
@@ -9,15 +51,17 @@ function extractSerializers(
9
51
  value, serializers) {
10
52
  if (!value)
11
53
  return;
12
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
13
54
  if (value.$serializer) {
14
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
15
55
  serializers.add(value.$serializer);
16
56
  //
17
57
  }
58
+ else if (value.$serializable) {
59
+ const foundSerializers = inferSerializersFromDreamClassOrViewModelClass(value.$serializable, value.$serializableSerializerKey);
60
+ foundSerializers.forEach(serializer => serializers.add(serializer));
61
+ //
62
+ }
18
63
  else if (isObject(value)) {
19
64
  // Recurse into objects
20
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
21
65
  Object.values(value).forEach(val => extractSerializers(val, serializers));
22
66
  //
23
67
  }
@@ -1,5 +1,54 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
5
+ import { inferSerializersFromDreamClassOrViewModelClass, } from '@rvoh/dream';
1
6
  import isObject from '../../helpers/isObject.js';
2
7
  import SerializerOpenapiRenderer from '../SerializerOpenapiRenderer.js';
8
+ /**
9
+ * @internal
10
+ *
11
+ * Transforms OpenAPI schema definitions by replacing serializer shorthand references with proper `$ref` statements.
12
+ *
13
+ * This function is used by the SerializerOpenapiRenderer to ensure that the schemas it generates from
14
+ * Serializers contain `$ref` statements instead of `$serializer` or `$serializable` statements. This
15
+ * transformation enables consistent ordering of `anyOf` statements since `anyOf` arrays are sorted by
16
+ * the `$ref` value, which follows the pattern `'#/components/schemas/TheSerializerOpenapiName'`.
17
+ *
18
+ * The function recursively traverses the schema and transforms:
19
+ * - `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
20
+ * - `{ $serializable: SomeModel, key: 'summary' }` → `{ $ref: '#/components/schemas/ModelSummarySerializer' }`
21
+ *
22
+ * @param openapi - The OpenAPI schema definition that may contain serializer shorthand references
23
+ * @returns The transformed schema with `$ref` statements replacing serializer shorthands
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const input = {
28
+ * type: 'object',
29
+ * properties: {
30
+ * user: { $serializer: UserSerializer },
31
+ * posts: {
32
+ * type: 'array',
33
+ * items: { $serializer: PostSerializer }
34
+ * }
35
+ * }
36
+ * }
37
+ *
38
+ * const output = allSerializersToRefsInOpenapi(input)
39
+ * // Returns:
40
+ * // {
41
+ * // type: 'object',
42
+ * // properties: {
43
+ * // user: { $ref: '#/components/schemas/UserSerializer' },
44
+ * // posts: {
45
+ * // type: 'array',
46
+ * // items: { $ref: '#/components/schemas/PostSerializer' }
47
+ * // }
48
+ * // }
49
+ * // }
50
+ * ```
51
+ */
3
52
  export default function allSerializersToRefsInOpenapi(openapi) {
4
53
  if (!openapi)
5
54
  return {};
@@ -10,24 +59,38 @@ function transformValue(value) {
10
59
  if (!value)
11
60
  return value;
12
61
  // If this is an object with a $serializer property, replace it with $ref
13
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
14
62
  if (value.$serializer) {
15
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
16
63
  const { $serializer, ...rest } = value;
17
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
18
64
  const openapiRenderer = new SerializerOpenapiRenderer($serializer).serializerRef;
19
65
  return {
20
66
  ...rest,
21
67
  ...openapiRenderer,
22
68
  };
69
+ //
70
+ }
71
+ else if (value.$serializable) {
72
+ const { $serializable, $serializableSerializerKey, ...rest } = value;
73
+ const foundSerializers = inferSerializersFromDreamClassOrViewModelClass($serializable, $serializableSerializerKey);
74
+ const refs = foundSerializers.map(serializer => new SerializerOpenapiRenderer(serializer).serializerRef);
75
+ if (refs.length === 0)
76
+ return rest;
77
+ if (refs.length === 1) {
78
+ return {
79
+ ...rest,
80
+ ...refs[0],
81
+ };
82
+ }
83
+ return {
84
+ ...rest,
85
+ anyOf: refs,
86
+ };
87
+ //
23
88
  }
24
89
  else if (isObject(value)) {
25
90
  // Recurse into objects
26
91
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
92
  const transformed = {};
28
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
29
93
  for (const [key, val] of Object.entries(value)) {
30
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
31
94
  transformed[key] = transformValue(val);
32
95
  }
33
96
  return transformed;
@@ -35,7 +98,6 @@ function transformValue(value) {
35
98
  }
36
99
  else if (Array.isArray(value)) {
37
100
  // Recurse into arrays
38
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
39
101
  return value.map(val => transformValue(val));
40
102
  //
41
103
  }
@@ -1,5 +1,7 @@
1
+ import { Dream, DreamOrViewModelClassSerializerKey, DreamSerializableArray, ViewModelClass } from '@rvoh/dream';
1
2
  export type FunctionProperties<T> = {
2
3
  [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
3
4
  };
4
5
  export type FunctionPropertyNames<T> = keyof FunctionProperties<T> & string;
5
6
  export type StringToInt<T extends string> = T extends `${infer N extends number}` ? N : never;
7
+ export type DreamOrViewModelClassSerializerArrayKeys<T extends DreamSerializableArray> = T['length'] extends 0 ? string : T[0] extends typeof Dream | ViewModelClass ? DreamOrViewModelClassSerializerKey<T[0]> & DreamOrViewModelClassSerializerArrayKeys<T extends [any, ...infer Rest] ? Rest : never> : DreamOrViewModelClassSerializerArrayKeys<T extends [any, ...infer Rest] ? Rest : never>;