@redocly/cli 2.70.0 → 2.71.1

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 (161) hide show
  1. package/THIRD_PARTY_NOTICES +3 -3
  2. package/lib/chunks/25RSTUQA.js +7 -0
  3. package/lib/chunks/{K4NCFIYY.js → 2IUGP2IH.js} +3 -3
  4. package/lib/chunks/{7R4MBJSO.js → 2NYYNBAC.js} +1 -1
  5. package/lib/chunks/{27HOIIUB.js → 3N7LKSDD.js} +1 -1
  6. package/lib/chunks/4DUOPCYP.js +7 -0
  7. package/lib/chunks/{636S3S3W.js → 54EKLNNI.js} +1 -1
  8. package/lib/chunks/5BQNATJZ.js +20 -0
  9. package/lib/chunks/{PF43QQZA.js → 5DW3QRJB.js} +2 -2
  10. package/lib/chunks/{57DEU3FG.js → 5SSWNONM.js} +1 -1
  11. package/lib/chunks/6TCEQRVX.js +10 -0
  12. package/lib/chunks/{U6PNFV4R.js → 6ZNJ7RDX.js} +2 -2
  13. package/lib/chunks/{EHJFDQN5.js → 7HFLPQAB.js} +2 -2
  14. package/lib/chunks/{BXUA6AY4.js → ACAPGKSR.js} +59 -25
  15. package/lib/chunks/ACECCMWJ.js +14 -0
  16. package/lib/chunks/AH6ANNYP.js +9 -0
  17. package/lib/chunks/B5SUTVG4.js +7 -0
  18. package/lib/chunks/{HAOBCQWB.js → BE4Z5FOC.js} +10 -8
  19. package/lib/chunks/{PL7WHX67.js → BOWZV5S6.js} +1 -1
  20. package/lib/chunks/{IULINGO6.js → BPSWCUZE.js} +1 -1
  21. package/lib/chunks/{7CBQD5PF.js → BXXX3ZCV.js} +1 -1
  22. package/lib/chunks/{SV2XHB2S.js → CHX7O5AM.js} +7 -7
  23. package/lib/chunks/{2NVGTELR.js → CJWIDJTB.js} +1 -1
  24. package/lib/chunks/{ACLT4ABL.js → CX76LEFC.js} +1 -1
  25. package/lib/chunks/{WBWCX3LS.js → DORXGE7Z.js} +93 -93
  26. package/lib/chunks/{LD5MEXEL.js → DP3T6VZX.js} +1 -1
  27. package/lib/chunks/{FKKC7G6X.js → DURXFJ7H.js} +1 -1
  28. package/lib/chunks/DVLD4FET.js +20 -0
  29. package/lib/chunks/{YQZIKJQK.js → EAFZ6DNQ.js} +1 -1
  30. package/lib/chunks/{LS3G7CUH.js → EBHQXW6Z.js} +1 -1
  31. package/lib/chunks/{6RKDRRBP.js → EO65RJ4Z.js} +1 -1
  32. package/lib/chunks/{T55LQK2H.js → EQOFXQKD.js} +1 -1
  33. package/lib/chunks/FBEEMKTB.js +7 -0
  34. package/lib/chunks/{BU4AMVHN.js → FGJDZKD2.js} +1 -1
  35. package/lib/chunks/G6SB4ZP5.js +7 -0
  36. package/lib/chunks/{LRMOPAZ4.js → GAKWND4S.js} +1 -1
  37. package/lib/chunks/HAGYQ4BK.js +20 -0
  38. package/lib/chunks/{UW36CSXE.js → HBWCMTP3.js} +1 -1
  39. package/lib/chunks/{2R2CXBRH.js → HWOL3HHR.js} +1 -1
  40. package/lib/chunks/{FXHPWQD4.js → ILYXMZRT.js} +1 -1
  41. package/lib/chunks/{U32PWLWM.js → INGAWMLB.js} +1 -1
  42. package/lib/chunks/{23VRM4VC.js → IYV7EO2J.js} +1 -1
  43. package/lib/chunks/IZMDARCL.js +22 -0
  44. package/lib/chunks/JOWPA3MJ.js +7 -0
  45. package/lib/chunks/KFQC57GK.js +145 -0
  46. package/lib/chunks/KJIUDCYZ.js +7 -0
  47. package/lib/chunks/{EV2R6OHD.js → L2O63VRM.js} +1 -1
  48. package/lib/chunks/{6P2KU7XN.js → MH5X2EVH.js} +1 -1
  49. package/lib/chunks/{RLJVKAOL.js → NLAPR7D4.js} +1 -1
  50. package/lib/chunks/O53IFFP3.js +98 -0
  51. package/lib/chunks/PBQOGZ5C.js +8 -0
  52. package/lib/chunks/{AFZIICGE.js → Q3IJURM3.js} +1 -1
  53. package/lib/chunks/{HHCU6T2V.js → QXBSAWH3.js} +1 -1
  54. package/lib/chunks/QXHFS5G6.js +7 -0
  55. package/lib/chunks/{IPDHEEHQ.js → QZJAXPYF.js} +1 -1
  56. package/lib/chunks/{LT6WLL3O.js → RBJCAITC.js} +1 -1
  57. package/lib/chunks/{LG6FRFOY.js → RBNQC5ZB.js} +1 -1
  58. package/lib/chunks/{2CMKIFTE.js → RNA6JDRC.js} +1 -1
  59. package/lib/chunks/{DHP4BBTH.js → RNL7TC2W.js} +1 -1
  60. package/lib/chunks/{WKDC6LN3.js → RUPTJCF5.js} +1 -1
  61. package/lib/chunks/{MKYAX4HH.js → S6W63XAS.js} +1 -1
  62. package/lib/chunks/{B3GE7EIX.js → TN3JQHMS.js} +1 -1
  63. package/lib/chunks/TNRIBQHV.js +7 -0
  64. package/lib/chunks/TZROZMVE.js +20 -0
  65. package/lib/chunks/{BMAUX474.js → U7KIB7CD.js} +1 -1
  66. package/lib/chunks/{BD5WLGAJ.js → UMN7TKTH.js} +2 -2
  67. package/lib/chunks/{Q43RV27Q.js → V34D5CUU.js} +1 -1
  68. package/lib/chunks/{7PKQV3SS.js → W2CABGMW.js} +1 -1
  69. package/lib/chunks/{ODGGZDFZ.js → WCJ2HAN3.js} +1 -1
  70. package/lib/chunks/{YNBDXA7I.js → WJ3YDHQA.js} +1 -1
  71. package/lib/chunks/{CKDLDSPG.js → WLI2XLEP.js} +1 -1
  72. package/lib/chunks/X7C43UBI.js +7 -0
  73. package/lib/chunks/{EH6VW7JO.js → XGPGFZMH.js} +1 -1
  74. package/lib/chunks/{7GM5RHYD.js → XZIK3H6V.js} +1 -1
  75. package/lib/chunks/{NOS4FCD7.js → Y3LDWYZD.js} +1 -1
  76. package/lib/chunks/{YYPD75S5.js → Y5DDX2MU.js} +1 -1
  77. package/lib/chunks/{MT4W54PL.js → YL3LVBZF.js} +1 -1
  78. package/lib/chunks/{EIXR5LCC.js → ZBB6PS6I.js} +1 -1
  79. package/lib/chunks/{NZE22F7Q.js → ZQTHHPK3.js} +2 -2
  80. package/lib/eject-assets/generators/cli/docs.ts +1 -1
  81. package/lib/eject-assets/generators/cli/engine-source.ts +1 -1
  82. package/lib/eject-assets/generators/cli/index.ts +2 -2
  83. package/lib/eject-assets/generators/cli/render.ts +1 -1
  84. package/lib/eject-assets/generators/go/client.ts +1 -1
  85. package/lib/eject-assets/generators/go/descriptor.ts +1 -1
  86. package/lib/eject-assets/generators/go/index.ts +2 -2
  87. package/lib/eject-assets/generators/go/models.ts +7 -2
  88. package/lib/eject-assets/generators/go/naming.ts +1 -1
  89. package/lib/eject-assets/generators/go/operations.ts +1 -1
  90. package/lib/eject-assets/generators/go/pagination.ts +1 -1
  91. package/lib/eject-assets/generators/go/types.ts +1 -1
  92. package/lib/eject-assets/generators/mock/faker.ts +17 -9
  93. package/lib/eject-assets/generators/mock/index.ts +4 -3
  94. package/lib/eject-assets/generators/mock/render.ts +128 -9
  95. package/lib/eject-assets/generators/mock/sample.ts +87 -4
  96. package/lib/eject-assets/generators/mock/values.ts +1 -1
  97. package/lib/eject-assets/generators/php/client.ts +1 -1
  98. package/lib/eject-assets/generators/php/descriptor.ts +10 -1
  99. package/lib/eject-assets/generators/php/index.ts +2 -2
  100. package/lib/eject-assets/generators/php/models.ts +8 -3
  101. package/lib/eject-assets/generators/php/naming.ts +1 -1
  102. package/lib/eject-assets/generators/php/operations.ts +11 -6
  103. package/lib/eject-assets/generators/php/pagination.ts +3 -2
  104. package/lib/eject-assets/generators/php/types.ts +12 -2
  105. package/lib/eject-assets/generators/python/client.ts +1 -1
  106. package/lib/eject-assets/generators/python/descriptor.ts +1 -1
  107. package/lib/eject-assets/generators/python/index.ts +3 -3
  108. package/lib/eject-assets/generators/python/models.ts +9 -3
  109. package/lib/eject-assets/generators/python/naming.ts +1 -1
  110. package/lib/eject-assets/generators/python/operations.ts +6 -5
  111. package/lib/eject-assets/generators/python/pagination.ts +1 -1
  112. package/lib/eject-assets/generators/python/types.ts +1 -1
  113. package/lib/eject-assets/generators/swr/index.ts +2 -2
  114. package/lib/eject-assets/generators/swr/render.ts +1 -1
  115. package/lib/eject-assets/generators/tanstack-query/index.ts +2 -2
  116. package/lib/eject-assets/generators/tanstack-query/render.ts +6 -5
  117. package/lib/eject-assets/generators/transformers/index.ts +2 -2
  118. package/lib/eject-assets/generators/transformers/render.ts +34 -7
  119. package/lib/eject-assets/generators/typescript/banner.ts +1 -1
  120. package/lib/eject-assets/generators/typescript/client.ts +1 -1
  121. package/lib/eject-assets/generators/typescript/descriptor.ts +1 -1
  122. package/lib/eject-assets/generators/typescript/index.ts +2 -2
  123. package/lib/eject-assets/generators/typescript/inline-runtime.ts +1 -1
  124. package/lib/eject-assets/generators/typescript/operation-signature.ts +1 -1
  125. package/lib/eject-assets/generators/typescript/operation-types.ts +1 -1
  126. package/lib/eject-assets/generators/typescript/operations.ts +1 -1
  127. package/lib/eject-assets/generators/typescript/response-headers.ts +1 -1
  128. package/lib/eject-assets/generators/typescript/type-guards.ts +1 -1
  129. package/lib/eject-assets/generators/typescript/types.ts +1 -1
  130. package/lib/eject-assets/generators/zod/index.ts +2 -2
  131. package/lib/eject-assets/generators/zod/schemas.ts +1 -1
  132. package/lib/eject-assets/skills/go-generator/SKILL.md +3 -0
  133. package/lib/eject-assets/skills/mock-generator/SKILL.md +22 -0
  134. package/lib/eject-assets/skills/php-generator/SKILL.md +9 -2
  135. package/lib/eject-assets/skills/python-generator/SKILL.md +6 -0
  136. package/lib/eject-assets/skills/tanstack-query-generator/SKILL.md +2 -1
  137. package/lib/eject-assets/skills/transformers-generator/SKILL.md +4 -0
  138. package/lib/eject-assets/skills/typescript-generator/SKILL.md +5 -0
  139. package/lib/index.js +37 -37
  140. package/lib/skills/redocly-cli/SKILL.md +20 -1
  141. package/package.json +1 -1
  142. package/lib/chunks/2Y7MGFQS.js +0 -105
  143. package/lib/chunks/AHEGLURA.js +0 -20
  144. package/lib/chunks/DQDJVO3X.js +0 -7
  145. package/lib/chunks/EK7EYCIE.js +0 -7
  146. package/lib/chunks/ERYWHU32.js +0 -19
  147. package/lib/chunks/GBN5W6T3.js +0 -9
  148. package/lib/chunks/GKXY6U4S.js +0 -145
  149. package/lib/chunks/HJWQEB7Y.js +0 -7
  150. package/lib/chunks/JKDUUCNL.js +0 -22
  151. package/lib/chunks/JM7BMZLD.js +0 -13
  152. package/lib/chunks/JUWXQD7B.js +0 -8
  153. package/lib/chunks/KGWCBODF.js +0 -7
  154. package/lib/chunks/LAML5K4D.js +0 -7
  155. package/lib/chunks/MOK3JA6R.js +0 -7
  156. package/lib/chunks/QKUESS2C.js +0 -20
  157. package/lib/chunks/RGVTUA3V.js +0 -7
  158. package/lib/chunks/RWKEGTOO.js +0 -7
  159. package/lib/chunks/S56YFLS5.js +0 -7
  160. package/lib/chunks/S6P42X5N.js +0 -20
  161. package/lib/chunks/UW4UC7ZI.js +0 -9
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "mock" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "mock" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator mock --update`.
@@ -13,9 +13,13 @@ import {
13
13
  allOperations,
14
14
  type ApiModel,
15
15
  type DateType,
16
+ flattenAllOf,
17
+ isNullable,
18
+ type ModelPagination,
16
19
  type NamedSchemaModel,
17
20
  type OperationModel,
18
21
  type ResponseBodyModel,
22
+ schemaAtPointer,
19
23
  type SchemaModel,
20
24
  } from '@redocly/client-generator';
21
25
  import {
@@ -30,6 +34,7 @@ import { sampleValue, SampleExpression } from './sample.ts';
30
34
  import {
31
35
  expr,
32
36
  isObjectValue,
37
+ type MockEntry,
33
38
  type MockValue,
34
39
  objectValue,
35
40
  renderMockValue,
@@ -54,6 +59,8 @@ export type MockOptions = {
54
59
  mockData?: 'static' | 'faker';
55
60
  /** When set in `'faker'` mode, emit a top-level `faker.seed(<n>);` so runs reproduce. */
56
61
  mockSeed?: number;
62
+ /** The run's resolved pagination: a paginated handler serves the last page. */
63
+ pagination?: ModelPagination;
57
64
  };
58
65
 
59
66
  /** The body value for `schema` under the active data mode: a static literal tree
@@ -150,7 +157,7 @@ function codeIdent(value: string): string {
150
157
  function handlerFor(op: OperationModel, model: ApiModel, opts: MockOptions): string {
151
158
  const override = overrideParam(op, model, opts);
152
159
  const params = override ?? '';
153
- const call = `http.${codeIdent(op.method)}(${JSON.stringify(mswPath(op.path))}, () => ${responseExpression(op, model, opts)})`;
160
+ const call = `http.${codeIdent(op.method)}(${JSON.stringify(mswPath(op.path))}, ${resolverFor(op, model, opts)})`;
154
161
  return `export const ${codeIdent(op.name)}Handler = (${params}) => ${call};`;
155
162
  }
156
163
 
@@ -212,8 +219,8 @@ function errorBodyType(op: OperationModel): string {
212
219
  * A `ref` success body forwards `override` to `create<Schema>(override)`, so its type
213
220
  * mirrors the factory's parameter (`Partial<Schema>`, or the full type when the factory
214
221
  * replaces wholesale — see `factoryFor`). An inline object body spreads `override`, typed
215
- * `Record<string, unknown>`. A body-less or non-object inline response has nothing to
216
- * override, so the handler takes no parameter.
222
+ * `Record<string, unknown>`, or `Partial<Omit<Schema, …>>` for an `omit` body. A body-less
223
+ * or non-object inline response has nothing to override, so the handler takes no parameter.
217
224
  */
218
225
  function overrideParam(op: OperationModel, model: ApiModel, opts: MockOptions): string | undefined {
219
226
  const success = op.successResponses[0];
@@ -226,16 +233,128 @@ function overrideParam(op: OperationModel, model: ApiModel, opts: MockOptions):
226
233
  return `override?: ${type}`;
227
234
  }
228
235
  if (!isObjectValue(bodyValue(success.schema, model, opts))) return undefined;
236
+ if (success.schema.kind === 'omit') {
237
+ const keys = success.schema.keys.map((key) => JSON.stringify(key)).join(' | ');
238
+ return `override?: Partial<Omit<${success.schema.base}, ${keys}>>`;
239
+ }
229
240
  return 'override?: Record<string, unknown>';
230
241
  }
231
242
 
243
+ /**
244
+ * The handler's resolver. A paginated operation serves the LAST page, so `.pages()` over
245
+ * the default handlers ends after one page: a cursor page carries no next cursor (and a
246
+ * `false` "has more" flag), and a `body-link` page no next link; an offset or page-number
247
+ * page answers any position past the first with an empty item list, because those styles
248
+ * stop only on an empty page.
249
+ */
250
+ function resolverFor(op: OperationModel, model: ApiModel, opts: MockOptions): string {
251
+ const spec = opts.pagination?.get(op.name)?.spec;
252
+ const success = op.successResponses[0];
253
+ if (!spec || spec.style === 'link' || !success)
254
+ return `() => ${responseExpression(op, model, opts)}`;
255
+ const page = bodyValue(success.schema, model, opts);
256
+ const status = statusCode(success.status);
257
+ // An object page spreads `override` in; a page that is the item array is replaced by it.
258
+ const takesOverride = overrideParam(op, model, opts) !== undefined;
259
+ const withOverride = (value: MockValue): MockValue => {
260
+ if (isObjectValue(value)) return spreadInto(value, 'override');
261
+ return takesOverride ? { kind: 'wrap', before: 'override ?? ', value, after: '' } : value;
262
+ };
263
+ const json = (value: MockValue): string => {
264
+ const data = renderMockValue(value, '');
265
+ return status === 200
266
+ ? `HttpResponse.json(${data})`
267
+ : `HttpResponse.json(${data}, { status: ${status} })`;
268
+ };
269
+ if (spec.style === 'cursor' || spec.style === 'body-link') {
270
+ const next = spec.style === 'cursor' ? spec.nextCursor : spec.nextLink;
271
+ let last = withPointer(page, pointerKeys(next), cursorStop(success.schema, next, model));
272
+ if (spec.style === 'cursor' && spec.hasMore !== undefined)
273
+ last = withPointer(last, pointerKeys(spec.hasMore), expr('false'));
274
+ return `() => ${json(withOverride(last))}`;
275
+ }
276
+ const first = spec.style === 'page' ? 1 : 0;
277
+ const position = `Number(new URL(request.url).searchParams.get(${codeLiteral(spec.param)}) ?? ${first})`;
278
+ const empty = emptyPage(page, pointerKeys(spec.items));
279
+ return `({ request }) => ${position} > ${first} ? ${json(empty)} : ${json(withOverride(page))}`;
280
+ }
281
+
282
+ /**
283
+ * The page past the last one: the page with `override` spread in, and the items at `keys`
284
+ * set AFTER the spread, so an override that sets the items cannot refill the empty page.
285
+ * The top-level property that holds the items is written out from the sample.
286
+ */
287
+ function emptyPage(page: MockValue, keys: string[]): MockValue {
288
+ if (keys.length === 0 || !isObjectValue(page)) return withPointer(page, keys, expr('[]'));
289
+ const [key, ...rest] = keys;
290
+ const isKey = (entry: MockEntry) => !('spread' in entry) && entry.key === key;
291
+ const holder = page.entries.find(isKey);
292
+ const inner = holder && !('spread' in holder) ? holder.value : objectValue([]);
293
+ return objectValue([
294
+ ...page.entries.filter((entry) => !isKey(entry)),
295
+ { spread: 'override' },
296
+ { key, value: withPointer(inner, rest, expr('[]')) },
297
+ ]);
298
+ }
299
+
300
+ /**
301
+ * What ends a cursor or `body-link` walk at `pointer`: `null` when the value is nullable,
302
+ * nothing (the property is left out) when it is optional, and an empty string otherwise.
303
+ */
304
+ function cursorStop(schema: SchemaModel, pointer: string, model: ApiModel): MockValue | undefined {
305
+ const cursor = schemaAtPointer(schema, pointer, model);
306
+ if (cursor?.kind === 'null' || (cursor && isNullable(cursor))) return expr('null');
307
+ const keys = pointerKeys(pointer);
308
+ const parent = schemaAtPointer(schema, pointer.slice(0, pointer.lastIndexOf('/')), model);
309
+ const property =
310
+ parent &&
311
+ flattenAllOf(parent, model)?.properties.find((candidate) => candidate.name === keys.at(-1));
312
+ return property?.required === false ? undefined : expr('""');
313
+ }
314
+
315
+ /** RFC 6901 pointer tokens (`~1` → `/`, `~0` → `~`); none for `''`, the whole value. */
316
+ function pointerKeys(pointer: string): string[] {
317
+ if (pointer === '') return [];
318
+ return pointer
319
+ .slice(1)
320
+ .split('/')
321
+ .map((token) => token.replaceAll('~1', '/').replaceAll('~0', '~'));
322
+ }
323
+
324
+ /**
325
+ * `value` with the property at the `keys` path replaced (added when missing), or removed
326
+ * when `replacement` is `undefined`. An empty path replaces the whole value (a page that
327
+ * is the item array itself). Steps only through objects: a path that reaches into
328
+ * anything else leaves the value unchanged.
329
+ */
330
+ function withPointer(
331
+ value: MockValue,
332
+ keys: string[],
333
+ replacement: MockValue | undefined
334
+ ): MockValue {
335
+ if (keys.length === 0) return replacement ?? value;
336
+ if (!isObjectValue(value)) return value;
337
+ const [key, ...rest] = keys;
338
+ const found = value.entries.some((entry) => !('spread' in entry) && entry.key === key);
339
+ const entries = value.entries.flatMap((entry) => {
340
+ if ('spread' in entry || entry.key !== key) return [entry];
341
+ if (rest.length > 0) return [{ key, value: withPointer(entry.value, rest, replacement) }];
342
+ return replacement === undefined ? [] : [{ key, value: replacement }];
343
+ });
344
+ if (!found && rest.length === 0 && replacement !== undefined) {
345
+ entries.push({ key, value: replacement });
346
+ }
347
+ return objectValue(entries);
348
+ }
349
+
232
350
  /**
233
351
  * The handler's response. A primary success body becomes `HttpResponse.json(…)`:
234
- * a `ref` body calls its named `create<Schema>(override)` factory; an inline body
235
- * is sampled and printed in place with `...override` spread in. A success with no
236
- * usable body (an `unknown` schema, or no success response at all) becomes a
237
- * body-less `new HttpResponse(null, { status })`. The status is the success
238
- * response's declared code, or 200 when it's `default`/absent.
352
+ * a `ref` body calls its named `create<Schema>(override)` factory; an inline body is
353
+ * sampled and printed in place with `...override` spread in. An `omit` body (a named
354
+ * schema without its `writeOnly` keys) is inline too, because the factory keeps the
355
+ * required ones. A success with no usable body (an `unknown` schema, or no success
356
+ * response at all) becomes a body-less `new HttpResponse(null, { status })`. The status
357
+ * is the success response's declared code, or 200 when it's `default`/absent.
239
358
  */
240
359
  function responseExpression(op: OperationModel, model: ApiModel, opts: MockOptions): string {
241
360
  const success = op.successResponses[0];
@@ -1,10 +1,11 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "mock" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "mock" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator mock --update`.
5
5
  import {
6
6
  type DateType,
7
7
  type NamedSchemaModel,
8
+ type PropertyModel,
8
9
  type ScalarKind,
9
10
  type SchemaMetadata,
10
11
  type SchemaModel,
@@ -97,7 +98,10 @@ export function splitIntersection(
97
98
  members: SchemaModel[],
98
99
  byName: Map<string, SchemaModel>
99
100
  ): { merged: SchemaModel | undefined; rest: SchemaModel[] } {
100
- const properties = new Map<string, { schema: SchemaModel; required: boolean; rank: number }>();
101
+ const properties = new Map<
102
+ string,
103
+ { schema: SchemaModel; required: boolean; writeOnly: boolean; rank: number }
104
+ >();
101
105
  const rest: SchemaModel[] = [];
102
106
  let sawObject = false;
103
107
  for (const member of members) {
@@ -116,6 +120,7 @@ export function splitIntersection(
116
120
  properties.set(property.name, {
117
121
  schema: property.schema,
118
122
  required: property.required || (existing?.required ?? false),
123
+ writeOnly: property.writeOnly === true || (existing?.writeOnly ?? false),
119
124
  rank,
120
125
  });
121
126
  }
@@ -129,6 +134,7 @@ export function splitIntersection(
129
134
  name,
130
135
  schema: p.schema,
131
136
  required: p.required,
137
+ ...(p.writeOnly ? { writeOnly: true } : {}),
132
138
  })),
133
139
  },
134
140
  rest,
@@ -195,7 +201,9 @@ function walk(
195
201
  const meta = schema.metadata;
196
202
  // An example/default is honored only when it inhabits the generated type — real specs
197
203
  // carry `example: null` on non-nullable fields and defaults outside a narrowed enum
198
- // (`enum: [cram-md5], default: none`); baking those would not type-check.
204
+ // (`enum: [cram-md5], default: none`); baking those would not type-check. A number
205
+ // outside its bounds or off its `multipleOf` step would fail the zod schema, so it is
206
+ // skipped too.
199
207
  const inhabits = (value: unknown): boolean => {
200
208
  if (value === null) {
201
209
  return (
@@ -205,6 +213,16 @@ function walk(
205
213
  }
206
214
  if (schema.kind === 'enum') return (schema.values as unknown[]).includes(value);
207
215
  if (schema.kind === 'literal') return value === schema.value;
216
+ if (
217
+ schema.kind === 'scalar' &&
218
+ (schema.scalar === 'integer' || schema.scalar === 'number') &&
219
+ typeof value === 'number'
220
+ ) {
221
+ const { min, max, step } = numericRange(schema.scalar, meta);
222
+ // `toPrecision` drops float noise, so `0.3` counts as a multiple of `0.1`.
223
+ const onStep = step === undefined || Number.isInteger(Number((value / step).toPrecision(12)));
224
+ return onStep && (min === undefined || value >= min) && (max === undefined || value <= max);
225
+ }
208
226
  return true;
209
227
  };
210
228
  if (meta?.example !== undefined && inhabits(meta.example)) return meta.example;
@@ -221,6 +239,7 @@ function walk(
221
239
  case 'object':
222
240
  return Object.fromEntries(
223
241
  schema.properties.flatMap((p) => {
242
+ if (omitsFromResponse(p)) return [];
224
243
  const value = walk(p.schema, byName, visiting, dateType);
225
244
  // A cyclic optional property is omitted (a null would not satisfy `T | undefined`);
226
245
  // a cyclic required property is uninhabitable, so null is the only stand-in.
@@ -292,9 +311,73 @@ function walk(
292
311
  }
293
312
  }
294
313
 
314
+ /**
315
+ * Whether a mock leaves a property out. Mocks are response data, and a response never
316
+ * carries a `writeOnly` property. A required one stays, because the named type still
317
+ * demands it; a handler whose response omits it samples that `omit` shape instead.
318
+ */
319
+ export function omitsFromResponse(property: PropertyModel): boolean {
320
+ return property.writeOnly === true && !property.required;
321
+ }
322
+
323
+ /**
324
+ * The inclusive range a numeric schema allows. `minimum`/`maximum` and the exclusive
325
+ * bounds narrow it, aligned to `multipleOf` (an integer steps by 1 without one). An
326
+ * exclusive bound on a number without a step moves in by 1, or to the middle of the
327
+ * range when the range is narrower than that. An unbounded side is `undefined`.
328
+ */
329
+ export function numericRange(
330
+ scalar: 'integer' | 'number',
331
+ meta: SchemaMetadata | undefined
332
+ ): { min?: number; max?: number; step?: number } {
333
+ const step = meta?.multipleOf ?? (scalar === 'integer' ? 1 : undefined);
334
+ // The multiple of `step` that `round` picks for `bound`; `toPrecision` drops float noise.
335
+ const onStep = (bound: number, size: number, round: (steps: number) => number): number =>
336
+ Number((round(bound / size) * size).toPrecision(15));
337
+ const lows: number[] = [];
338
+ const highs: number[] = [];
339
+ if (meta?.minimum !== undefined) {
340
+ const bound = meta.minimum;
341
+ lows.push(step === undefined ? bound : onStep(bound, step, Math.ceil));
342
+ }
343
+ if (meta?.exclusiveMinimum !== undefined) {
344
+ const bound = meta.exclusiveMinimum;
345
+ lows.push(
346
+ step === undefined ? bound + 1 : onStep(bound, step, (steps) => Math.floor(steps) + 1)
347
+ );
348
+ }
349
+ if (meta?.maximum !== undefined) {
350
+ const bound = meta.maximum;
351
+ highs.push(step === undefined ? bound : onStep(bound, step, Math.floor));
352
+ }
353
+ if (meta?.exclusiveMaximum !== undefined) {
354
+ const bound = meta.exclusiveMaximum;
355
+ highs.push(
356
+ step === undefined ? bound - 1 : onStep(bound, step, (steps) => Math.ceil(steps) - 1)
357
+ );
358
+ }
359
+ const min = lows.length > 0 ? Math.max(...lows) : undefined;
360
+ const max = highs.length > 0 ? Math.min(...highs) : undefined;
361
+ if (step === undefined && min !== undefined && max !== undefined && min > max) {
362
+ const low = Math.max(meta?.minimum ?? -Infinity, meta?.exclusiveMinimum ?? -Infinity);
363
+ const high = Math.min(meta?.maximum ?? Infinity, meta?.exclusiveMaximum ?? Infinity);
364
+ const middle = (low + high) / 2;
365
+ return { min: middle, max: middle };
366
+ }
367
+ return { min, max, step };
368
+ }
369
+
370
+ /** `0` when the range allows it, else the end of the range nearest to `0`. */
371
+ function numberSample(scalar: 'integer' | 'number', meta: SchemaMetadata | undefined): number {
372
+ const { min, max } = numericRange(scalar, meta);
373
+ if (min !== undefined && min > 0) return min;
374
+ if (max !== undefined && max < 0) return max;
375
+ return 0;
376
+ }
377
+
295
378
  function scalarSample(scalar: ScalarKind, meta: SchemaMetadata | undefined): unknown {
296
379
  if (scalar === 'boolean') return true;
297
- if (scalar === 'integer' || scalar === 'number') return 0;
380
+ if (scalar === 'integer' || scalar === 'number') return numberSample(scalar, meta);
298
381
  // Type-demanding formats (binary, and date/date-time under `dateType: 'Date'`) are
299
382
  // handled earlier in `walk` via `typeDemandedExpression`; here the date formats fall
300
383
  // through to the ISO-string path (the `dateType: 'string'` types them as `string`).
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "mock" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "mock" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator mock --update`.
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -30,6 +30,15 @@ export function phpSecurityLiteral(op: OperationModel, model: ApiModel): string
30
30
  return `[${alternatives.join(', ')}]`;
31
31
  }
32
32
 
33
+ /**
34
+ * The `resolveAuth` argument: the table's `security` entry, or `[]` for an operation the
35
+ * table gives none. Reading an absent key with `?? []` reads as dead code to PHPStan,
36
+ * which knows the table's exact shape.
37
+ */
38
+ export function phpSecurityArg(op: OperationModel, model: ApiModel): string {
39
+ return phpSecurityLiteral(op, model) === undefined ? '[]' : "$op['security']";
40
+ }
41
+
33
42
  export function phpPaginationLiteral(rule: NeutralPaginationRule): string {
34
43
  const fields = [
35
44
  `'style' => ${phpString(rule.style)}`,
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -234,5 +234,5 @@ export default {
234
234
  "argsStyle": "inputs follow the target language's own idiom",
235
235
  "importExt": "the generated file has no relative imports"
236
236
  },
237
- requiresGenerator: '^0.7.10',
237
+ requiresGenerator: '^0.7.12',
238
238
  };
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -111,10 +111,15 @@ function writeClass(
111
111
  dateType: DateType,
112
112
  description?: string
113
113
  ): void {
114
+ // A response never carries a `writeOnly` property, and responses decode into this same
115
+ // class, so the property is optional even when the description requires it.
116
+ const fields = properties.map((property) =>
117
+ property.writeOnly ? { ...property, required: false } : property
118
+ );
114
119
  // PHP requires defaulted parameters after required ones.
115
120
  const ordered = [
116
- ...properties.filter((property) => property.required),
117
- ...properties.filter((property) => !property.required),
121
+ ...fields.filter((property) => property.required),
122
+ ...fields.filter((property) => !property.required),
118
123
  ];
119
124
  printer.doc(className(name), description);
120
125
  printer.line(`final class ${className(name)}`);
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -18,7 +18,7 @@ import {
18
18
  } from '@redocly/client-generator';
19
19
  import type { PhpPrinter } from '@redocly/client-generator/printers/php';
20
20
 
21
- import { envelopeHeaderSpecs } from './descriptor.ts';
21
+ import { envelopeHeaderSpecs, phpSecurityArg } from './descriptor.ts';
22
22
  import { hydration, serialization } from './models.ts';
23
23
  import { PHP, phpString } from './naming.ts';
24
24
  import { phpElementType, phpNullable, phpType } from './types.ts';
@@ -94,10 +94,15 @@ export function methodArgs(
94
94
  }
95
95
 
96
96
  /** The shared prologue: resolve auth, build query/url, merge headers. */
97
- function writeRequestSetup(printer: PhpPrinter, op: OperationModel, args: MethodArgs): void {
97
+ function writeRequestSetup(
98
+ printer: PhpPrinter,
99
+ op: OperationModel,
100
+ model: ApiModel,
101
+ args: MethodArgs
102
+ ): void {
98
103
  printer.line(`$op = self::OPERATIONS[${phpString(op.specName ?? op.name)}];`);
99
104
  printer.line(
100
- "[$authHeaders, $query, $cookies] = resolveAuth($op['security'] ?? [], $this->config->auth);"
105
+ `[$authHeaders, $query, $cookies] = resolveAuth(${phpSecurityArg(op, model)}, $this->config->auth);`
101
106
  );
102
107
  for (const { php, wire, value } of args.queryArgs) {
103
108
  printer.block(
@@ -154,13 +159,13 @@ export function writePhpMethod(
154
159
  envelope
155
160
  ? `Like ${ident}(), returning an Envelope with the declared response headers.`
156
161
  : (op.summary ?? `${op.method.toUpperCase()} ${op.path}`),
157
- element === undefined ? [] : [`@return ${element}[]`]
162
+ element === undefined ? [] : [`@return ${element.includes('|') ? `(${element})` : element}[]`]
158
163
  );
159
164
  printer.line(`public function ${name}(${args.signature.join(', ')}): ${returnType}`);
160
165
  printer.block(
161
166
  '{',
162
167
  () => {
163
- writeRequestSetup(printer, op, args);
168
+ writeRequestSetup(printer, op, model, args);
164
169
  if (sse !== undefined) {
165
170
  const jsonData = sse.schema !== undefined && sse.schema.kind !== 'unknown';
166
171
  printer.line('$url = appendQuery($url, $query);');
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -13,6 +13,7 @@ import {
13
13
  } from '@redocly/client-generator';
14
14
  import type { PhpPrinter } from '@redocly/client-generator/printers/php';
15
15
 
16
+ import { phpSecurityArg } from './descriptor.ts';
16
17
  import { phpString } from './naming.ts';
17
18
  import { methodArgs } from './operations.ts';
18
19
  import { phpType } from './types.ts';
@@ -51,7 +52,7 @@ export function writePhpPaginationWrappers(
51
52
  '$call = function (array $params) use ($op, $headers): array {',
52
53
  () => {
53
54
  printer.line(
54
- "[$authHeaders, $authQuery, $cookies] = resolveAuth($op['security'] ?? [], $this->config->auth);"
55
+ `[$authHeaders, $authQuery, $cookies] = resolveAuth(${phpSecurityArg(op, model)}, $this->config->auth);`
55
56
  );
56
57
  printer.line(`$url = buildUrl($this->config->serverUrl, $op['path'], [${pathDict}]);`);
57
58
  printer.line('$requestHeaders = array_merge($authHeaders, $headers ?? []);');
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "php" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "php" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator php --update`.
@@ -9,6 +9,7 @@ import {
9
9
  type ApiModel,
10
10
  type DateType,
11
11
  deref,
12
+ discriminatorCases,
12
13
  enumValues,
13
14
  flattenAllOf,
14
15
  isNullable,
@@ -61,7 +62,16 @@ export function phpType(
61
62
  const kind = classify(schema.name, model);
62
63
  if (kind === 'class' || kind === 'enum') return className(schema.name);
63
64
  const target = deref(schema, model);
64
- return target === undefined ? 'mixed' : phpType(target, model, dateType);
65
+ if (target === undefined) return 'mixed';
66
+ const type = phpType(target, model, dateType);
67
+ // A discriminated union hydrates through `unmarshalX`, which hands a value with an
68
+ // unknown discriminator back as the raw array.
69
+ // A nullable type drops its `?` first, because PHP forbids `?` inside a union.
70
+ const raw = discriminatorCases(target, model) !== undefined;
71
+ if (!raw || type === 'mixed' || type.split('|').includes('array')) return type;
72
+ const nullable = type.startsWith('?') || type.endsWith('|null');
73
+ const bare = type.startsWith('?') ? type.slice(1) : type.replace(/\|null$/, '');
74
+ return nullable ? phpNullable(`${bare}|array`) : `${bare}|array`;
65
75
  }
66
76
  case 'enum':
67
77
  // Anonymous (inline) enums keep the wire scalar; only NAMED enums get types.
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "python" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "python" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator python --update`.
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "python" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "python" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator python --update`.
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "python" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "python" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator python --update`.
@@ -138,7 +138,7 @@ export const pythonGenerator: Generator = ({
138
138
  for (const { op, ident } of operationIdents(model)) {
139
139
  paginationSpecs.set(ident, paginationSpec(pagination?.get(op.name)?.spec));
140
140
  }
141
- printer.line('_OPERATIONS = {');
141
+ printer.line('_OPERATIONS: Dict[str, Dict[str, Any]] = {');
142
142
  printer.indent(() => {
143
143
  for (const { op, ident } of operationIdents(model)) {
144
144
  const descriptor = {
@@ -247,5 +247,5 @@ export default {
247
247
  "argsStyle": "inputs follow the target language's own idiom",
248
248
  "importExt": "the generated file has no relative imports"
249
249
  },
250
- requiresGenerator: '^0.7.10',
250
+ requiresGenerator: '^0.7.12',
251
251
  };
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "python" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "python" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator python --update`.
@@ -84,6 +84,11 @@ function writeDataclass(
84
84
  /** The discriminator value this class is mapped to, pinned as a `Literal` (pydantic). */
85
85
  pinned?: DiscriminatorPin
86
86
  ): void {
87
+ // A response never carries a `writeOnly` property, and responses decode into this same
88
+ // class, so the property is optional even when the description requires it.
89
+ const fields = properties.map((property) =>
90
+ property.writeOnly ? { ...property, required: false } : property
91
+ );
87
92
  const pydantic = models === 'pydantic';
88
93
  if (!pydantic) printer.line('@dataclass');
89
94
  const header = pydantic ? `class ${className(name)}(BaseModel):` : `class ${className(name)}:`;
@@ -97,8 +102,8 @@ function writeDataclass(
97
102
  }
98
103
  // Required fields first — a dataclass field without a default may not follow one with.
99
104
  const ordered = [
100
- ...properties.filter((property) => property.required),
101
- ...properties.filter((property) => !property.required),
105
+ ...fields.filter((property) => property.required),
106
+ ...fields.filter((property) => !property.required),
102
107
  ];
103
108
  const fieldMap: Array<[string, string]> = [];
104
109
  if (ordered.length === 0) printer.line('pass');
@@ -157,6 +162,7 @@ export function renderPythonModels(
157
162
  'Optional',
158
163
  'Tuple',
159
164
  'Union',
165
+ 'cast',
160
166
  ];
161
167
  if (models === 'dataclass') typingNames.splice(2, 0, 'ClassVar');
162
168
  if (unions.size > 0) typingNames.unshift('Annotated');
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "python" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "python" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator python --update`.
@@ -1,4 +1,4 @@
1
- // Ejected from @redocly/client-generator@0.7.10 — the built-in "python" generator.
1
+ // Ejected from @redocly/client-generator@0.7.12 — the built-in "python" generator.
2
2
  // This file is yours: edit freely; the generated client stays machine-owned and is
3
3
  // rebuilt by `redocly generate-client`. Newer generator versions merge in with
4
4
  // `redocly eject-generator python --update`.
@@ -72,7 +72,7 @@ export function writeMethod(
72
72
  : sse !== undefined
73
73
  ? `${isAsync ? 'AsyncIterator' : 'Iterator'}[ServerSentEvent]`
74
74
  : errorMode === 'result'
75
- ? 'Result'
75
+ ? `Result[${success === undefined ? 'None' : pythonType(success, dateType)}, Any]`
76
76
  : success === undefined
77
77
  ? 'None'
78
78
  : pythonType(success, dateType);
@@ -103,7 +103,7 @@ export function writeMethod(
103
103
  printer.line(`url = build_url(self._server_url, op["path"], {${pathDict}})`);
104
104
  if (sse !== undefined) {
105
105
  const dataKind = sse.schema !== undefined && sse.schema.kind !== 'unknown' ? 'json' : 'text';
106
- printer.block('def _open(extra_headers: Dict[str, str]):', () => {
106
+ printer.block('def _open(extra_headers: Dict[str, str]) -> Any:', () => {
107
107
  printer.line(
108
108
  'return self._http.stream(op["method"], url, ' +
109
109
  'headers={**auth_headers, **(headers or {}), **extra_headers}, params=params, timeout=timeout)'
@@ -129,10 +129,11 @@ export function writeMethod(
129
129
  `headers={**auth_headers, **(headers or {})}, params=params${bodyKw}, ` +
130
130
  'timeout=timeout, retry=retry, idempotency_key=idempotency_key)'
131
131
  );
132
+ const successType = success === undefined ? undefined : pythonType(success, dateType);
132
133
  const decoded =
133
- success === undefined
134
+ successType === undefined
134
135
  ? 'None'
135
- : `decode(${pythonType(success, dateType)}, _safe_json(response))`;
136
+ : `cast(${successType}, decode(${successType}, _safe_json(response)))`;
136
137
  if (envelope) {
137
138
  printer.block('if not response.is_success:', () => {
138
139
  printer.line(