@open-mercato/shared 0.4.2-canary-c02407ff85

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 (324) hide show
  1. package/build.mjs +101 -0
  2. package/dist/index.js +1 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/lib/api/crud.js +47 -0
  5. package/dist/lib/api/crud.js.map +7 -0
  6. package/dist/lib/api/scoped.js +140 -0
  7. package/dist/lib/api/scoped.js.map +7 -0
  8. package/dist/lib/auth/jwt.js +34 -0
  9. package/dist/lib/auth/jwt.js.map +7 -0
  10. package/dist/lib/auth/server.js +157 -0
  11. package/dist/lib/auth/server.js.map +7 -0
  12. package/dist/lib/boolean.js +22 -0
  13. package/dist/lib/boolean.js.map +7 -0
  14. package/dist/lib/bootstrap/appResolver.js +43 -0
  15. package/dist/lib/bootstrap/appResolver.js.map +7 -0
  16. package/dist/lib/bootstrap/dynamicLoader.js +108 -0
  17. package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
  18. package/dist/lib/bootstrap/factory.js +59 -0
  19. package/dist/lib/bootstrap/factory.js.map +7 -0
  20. package/dist/lib/bootstrap/index.js +11 -0
  21. package/dist/lib/bootstrap/index.js.map +7 -0
  22. package/dist/lib/bootstrap/types.js +1 -0
  23. package/dist/lib/bootstrap/types.js.map +7 -0
  24. package/dist/lib/cache/segments.js +36 -0
  25. package/dist/lib/cache/segments.js.map +7 -0
  26. package/dist/lib/cli/progress.js +46 -0
  27. package/dist/lib/cli/progress.js.map +7 -0
  28. package/dist/lib/commands/command-bus.js +285 -0
  29. package/dist/lib/commands/command-bus.js.map +7 -0
  30. package/dist/lib/commands/customFieldSnapshots.js +66 -0
  31. package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
  32. package/dist/lib/commands/helpers.js +98 -0
  33. package/dist/lib/commands/helpers.js.map +7 -0
  34. package/dist/lib/commands/index.js +8 -0
  35. package/dist/lib/commands/index.js.map +7 -0
  36. package/dist/lib/commands/operationMetadata.js +32 -0
  37. package/dist/lib/commands/operationMetadata.js.map +7 -0
  38. package/dist/lib/commands/registry.js +43 -0
  39. package/dist/lib/commands/registry.js.map +7 -0
  40. package/dist/lib/commands/scope.js +44 -0
  41. package/dist/lib/commands/scope.js.map +7 -0
  42. package/dist/lib/commands/types.js +8 -0
  43. package/dist/lib/commands/types.js.map +7 -0
  44. package/dist/lib/crud/cache-stats.js +98 -0
  45. package/dist/lib/crud/cache-stats.js.map +7 -0
  46. package/dist/lib/crud/cache.js +175 -0
  47. package/dist/lib/crud/cache.js.map +7 -0
  48. package/dist/lib/crud/custom-fields-client.js +52 -0
  49. package/dist/lib/crud/custom-fields-client.js.map +7 -0
  50. package/dist/lib/crud/custom-fields.js +467 -0
  51. package/dist/lib/crud/custom-fields.js.map +7 -0
  52. package/dist/lib/crud/errors.js +24 -0
  53. package/dist/lib/crud/errors.js.map +7 -0
  54. package/dist/lib/crud/exporters.js +154 -0
  55. package/dist/lib/crud/exporters.js.map +7 -0
  56. package/dist/lib/crud/factory.js +1311 -0
  57. package/dist/lib/crud/factory.js.map +7 -0
  58. package/dist/lib/crud/types.js +1 -0
  59. package/dist/lib/crud/types.js.map +7 -0
  60. package/dist/lib/custom-fields/normalize.js +36 -0
  61. package/dist/lib/custom-fields/normalize.js.map +7 -0
  62. package/dist/lib/data/engine.js +396 -0
  63. package/dist/lib/data/engine.js.map +7 -0
  64. package/dist/lib/db/escapeLikePattern.js +5 -0
  65. package/dist/lib/db/escapeLikePattern.js.map +7 -0
  66. package/dist/lib/db/mikro.js +82 -0
  67. package/dist/lib/db/mikro.js.map +7 -0
  68. package/dist/lib/di/container.js +94 -0
  69. package/dist/lib/di/container.js.map +7 -0
  70. package/dist/lib/email/send.js +12 -0
  71. package/dist/lib/email/send.js.map +7 -0
  72. package/dist/lib/encryption/aes.js +58 -0
  73. package/dist/lib/encryption/aes.js.map +7 -0
  74. package/dist/lib/encryption/customFieldValues.js +49 -0
  75. package/dist/lib/encryption/customFieldValues.js.map +7 -0
  76. package/dist/lib/encryption/entityFields.js +26 -0
  77. package/dist/lib/encryption/entityFields.js.map +7 -0
  78. package/dist/lib/encryption/entityIds.js +80 -0
  79. package/dist/lib/encryption/entityIds.js.map +7 -0
  80. package/dist/lib/encryption/find.js +45 -0
  81. package/dist/lib/encryption/find.js.map +7 -0
  82. package/dist/lib/encryption/indexDoc.js +69 -0
  83. package/dist/lib/encryption/indexDoc.js.map +7 -0
  84. package/dist/lib/encryption/kms.js +282 -0
  85. package/dist/lib/encryption/kms.js.map +7 -0
  86. package/dist/lib/encryption/subscriber.js +330 -0
  87. package/dist/lib/encryption/subscriber.js.map +7 -0
  88. package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
  89. package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
  90. package/dist/lib/encryption/toggles.js +18 -0
  91. package/dist/lib/encryption/toggles.js.map +7 -0
  92. package/dist/lib/entities/naming.js +9 -0
  93. package/dist/lib/entities/naming.js.map +7 -0
  94. package/dist/lib/entities/system-entities.js +43 -0
  95. package/dist/lib/entities/system-entities.js.map +7 -0
  96. package/dist/lib/frontend/organizationEvents.js +41 -0
  97. package/dist/lib/frontend/organizationEvents.js.map +7 -0
  98. package/dist/lib/frontend/useOrganizationScope.js +32 -0
  99. package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
  100. package/dist/lib/hotkeys/index.js +128 -0
  101. package/dist/lib/hotkeys/index.js.map +7 -0
  102. package/dist/lib/i18n/app-dictionaries.js +17 -0
  103. package/dist/lib/i18n/app-dictionaries.js.map +7 -0
  104. package/dist/lib/i18n/config.js +7 -0
  105. package/dist/lib/i18n/config.js.map +7 -0
  106. package/dist/lib/i18n/context.js +50 -0
  107. package/dist/lib/i18n/context.js.map +7 -0
  108. package/dist/lib/i18n/server.js +68 -0
  109. package/dist/lib/i18n/server.js.map +7 -0
  110. package/dist/lib/i18n/translate.js +45 -0
  111. package/dist/lib/i18n/translate.js.map +7 -0
  112. package/dist/lib/indexers/error-log.js +82 -0
  113. package/dist/lib/indexers/error-log.js.map +7 -0
  114. package/dist/lib/indexers/status-log.js +80 -0
  115. package/dist/lib/indexers/status-log.js.map +7 -0
  116. package/dist/lib/lib/auth/jwt.js +34 -0
  117. package/dist/lib/lib/auth/jwt.js.map +7 -0
  118. package/dist/lib/lib/auth/server.js +77 -0
  119. package/dist/lib/lib/auth/server.js.map +7 -0
  120. package/dist/lib/lib/email/send.js +12 -0
  121. package/dist/lib/lib/email/send.js.map +7 -0
  122. package/dist/lib/lib/i18n/config.js +7 -0
  123. package/dist/lib/lib/i18n/config.js.map +7 -0
  124. package/dist/lib/lib/i18n/context.js +31 -0
  125. package/dist/lib/lib/i18n/context.js.map +7 -0
  126. package/dist/lib/lib/utils.js +9 -0
  127. package/dist/lib/lib/utils.js.map +7 -0
  128. package/dist/lib/location/countries.js +68 -0
  129. package/dist/lib/location/countries.js.map +7 -0
  130. package/dist/lib/modules/index.js +6 -0
  131. package/dist/lib/modules/index.js.map +7 -0
  132. package/dist/lib/modules/registry.js +18 -0
  133. package/dist/lib/modules/registry.js.map +7 -0
  134. package/dist/lib/openapi/crud.js +137 -0
  135. package/dist/lib/openapi/crud.js.map +7 -0
  136. package/dist/lib/openapi/generator.js +1131 -0
  137. package/dist/lib/openapi/generator.js.map +7 -0
  138. package/dist/lib/openapi/index.js +10 -0
  139. package/dist/lib/openapi/index.js.map +7 -0
  140. package/dist/lib/openapi/sanitize.js +110 -0
  141. package/dist/lib/openapi/sanitize.js.map +7 -0
  142. package/dist/lib/openapi/types.js +1 -0
  143. package/dist/lib/openapi/types.js.map +7 -0
  144. package/dist/lib/profiler/index.js +258 -0
  145. package/dist/lib/profiler/index.js.map +7 -0
  146. package/dist/lib/query/engine.js +729 -0
  147. package/dist/lib/query/engine.js.map +7 -0
  148. package/dist/lib/query/join-utils.js +195 -0
  149. package/dist/lib/query/join-utils.js.map +7 -0
  150. package/dist/lib/query/types.js +9 -0
  151. package/dist/lib/query/types.js.map +7 -0
  152. package/dist/lib/search/config.js +32 -0
  153. package/dist/lib/search/config.js.map +7 -0
  154. package/dist/lib/search/tokenize.js +34 -0
  155. package/dist/lib/search/tokenize.js.map +7 -0
  156. package/dist/lib/slugify.js +24 -0
  157. package/dist/lib/slugify.js.map +7 -0
  158. package/dist/lib/testing/bootstrap.js +51 -0
  159. package/dist/lib/testing/bootstrap.js.map +7 -0
  160. package/dist/lib/testing/index.js +17 -0
  161. package/dist/lib/testing/index.js.map +7 -0
  162. package/dist/lib/testing/renderWithProviders.js +15 -0
  163. package/dist/lib/testing/renderWithProviders.js.map +7 -0
  164. package/dist/lib/url.js +12 -0
  165. package/dist/lib/url.js.map +7 -0
  166. package/dist/lib/utils.js +13 -0
  167. package/dist/lib/utils.js.map +7 -0
  168. package/dist/lib/version.js +7 -0
  169. package/dist/lib/version.js.map +7 -0
  170. package/dist/modules/dashboard/widgets.js +1 -0
  171. package/dist/modules/dashboard/widgets.js.map +7 -0
  172. package/dist/modules/dsl.js +30 -0
  173. package/dist/modules/dsl.js.map +7 -0
  174. package/dist/modules/entities/kinds.js +22 -0
  175. package/dist/modules/entities/kinds.js.map +7 -0
  176. package/dist/modules/entities/options.js +26 -0
  177. package/dist/modules/entities/options.js.map +7 -0
  178. package/dist/modules/entities/validation.js +102 -0
  179. package/dist/modules/entities/validation.js.map +7 -0
  180. package/dist/modules/entities/validators.js +88 -0
  181. package/dist/modules/entities/validators.js.map +7 -0
  182. package/dist/modules/entities.js +1 -0
  183. package/dist/modules/entities.js.map +7 -0
  184. package/dist/modules/navigation/sidebarPreferences.js +50 -0
  185. package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
  186. package/dist/modules/perspectives/types.js +1 -0
  187. package/dist/modules/perspectives/types.js.map +7 -0
  188. package/dist/modules/registry.js +96 -0
  189. package/dist/modules/registry.js.map +7 -0
  190. package/dist/modules/search.js +15 -0
  191. package/dist/modules/search.js.map +7 -0
  192. package/dist/modules/vector.js +1 -0
  193. package/dist/modules/vector.js.map +7 -0
  194. package/dist/modules/widgets/injection-loader.js +180 -0
  195. package/dist/modules/widgets/injection-loader.js.map +7 -0
  196. package/dist/modules/widgets/injection.js +1 -0
  197. package/dist/modules/widgets/injection.js.map +7 -0
  198. package/dist/security/features.js +23 -0
  199. package/dist/security/features.js.map +7 -0
  200. package/dist/types/pg.d.js +1 -0
  201. package/dist/types/pg.d.js.map +7 -0
  202. package/dist/types/react-email.d.js +1 -0
  203. package/dist/types/react-email.d.js.map +7 -0
  204. package/dist/types/resend.d.js +1 -0
  205. package/dist/types/resend.d.js.map +7 -0
  206. package/jest.config.cjs +22 -0
  207. package/package.json +88 -0
  208. package/src/index.ts +0 -0
  209. package/src/lib/api/__tests__/scoped.test.ts +38 -0
  210. package/src/lib/api/crud.ts +59 -0
  211. package/src/lib/api/scoped.ts +239 -0
  212. package/src/lib/auth/jwt.ts +39 -0
  213. package/src/lib/auth/server.ts +199 -0
  214. package/src/lib/boolean.ts +17 -0
  215. package/src/lib/bootstrap/appResolver.ts +85 -0
  216. package/src/lib/bootstrap/dynamicLoader.ts +177 -0
  217. package/src/lib/bootstrap/factory.ts +108 -0
  218. package/src/lib/bootstrap/index.ts +23 -0
  219. package/src/lib/bootstrap/types.ts +31 -0
  220. package/src/lib/cache/segments.ts +56 -0
  221. package/src/lib/cli/progress.ts +55 -0
  222. package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
  223. package/src/lib/commands/__tests__/helpers.test.ts +42 -0
  224. package/src/lib/commands/command-bus.ts +349 -0
  225. package/src/lib/commands/customFieldSnapshots.ts +86 -0
  226. package/src/lib/commands/helpers.ts +143 -0
  227. package/src/lib/commands/index.ts +4 -0
  228. package/src/lib/commands/operationMetadata.ts +40 -0
  229. package/src/lib/commands/registry.ts +46 -0
  230. package/src/lib/commands/scope.ts +59 -0
  231. package/src/lib/commands/types.ts +63 -0
  232. package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
  233. package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
  234. package/src/lib/crud/cache-stats.ts +127 -0
  235. package/src/lib/crud/cache.ts +205 -0
  236. package/src/lib/crud/custom-fields-client.ts +54 -0
  237. package/src/lib/crud/custom-fields.ts +607 -0
  238. package/src/lib/crud/errors.ts +23 -0
  239. package/src/lib/crud/exporters.ts +188 -0
  240. package/src/lib/crud/factory.ts +1622 -0
  241. package/src/lib/crud/types.ts +29 -0
  242. package/src/lib/custom-fields/normalize.ts +45 -0
  243. package/src/lib/data/engine.ts +562 -0
  244. package/src/lib/db/escapeLikePattern.ts +2 -0
  245. package/src/lib/db/mikro.ts +100 -0
  246. package/src/lib/di/container.ts +105 -0
  247. package/src/lib/email/send.ts +18 -0
  248. package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
  249. package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
  250. package/src/lib/encryption/aes.ts +64 -0
  251. package/src/lib/encryption/customFieldValues.ts +67 -0
  252. package/src/lib/encryption/entityFields.ts +39 -0
  253. package/src/lib/encryption/entityIds.ts +107 -0
  254. package/src/lib/encryption/find.ts +81 -0
  255. package/src/lib/encryption/indexDoc.ts +104 -0
  256. package/src/lib/encryption/kms.ts +337 -0
  257. package/src/lib/encryption/subscriber.ts +416 -0
  258. package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
  259. package/src/lib/encryption/toggles.ts +15 -0
  260. package/src/lib/entities/naming.ts +6 -0
  261. package/src/lib/entities/system-entities.ts +43 -0
  262. package/src/lib/frontend/organizationEvents.ts +55 -0
  263. package/src/lib/frontend/useOrganizationScope.ts +30 -0
  264. package/src/lib/hotkeys/index.ts +168 -0
  265. package/src/lib/i18n/app-dictionaries.ts +18 -0
  266. package/src/lib/i18n/config.ts +4 -0
  267. package/src/lib/i18n/context.tsx +66 -0
  268. package/src/lib/i18n/server.ts +74 -0
  269. package/src/lib/i18n/translate.ts +54 -0
  270. package/src/lib/indexers/error-log.ts +106 -0
  271. package/src/lib/indexers/status-log.ts +119 -0
  272. package/src/lib/lib/auth/jwt.ts +39 -0
  273. package/src/lib/lib/auth/server.ts +94 -0
  274. package/src/lib/lib/email/send.ts +18 -0
  275. package/src/lib/lib/i18n/config.ts +4 -0
  276. package/src/lib/lib/i18n/context.tsx +38 -0
  277. package/src/lib/lib/utils.ts +6 -0
  278. package/src/lib/location/countries.ts +97 -0
  279. package/src/lib/modules/index.ts +1 -0
  280. package/src/lib/modules/registry.ts +18 -0
  281. package/src/lib/openapi/crud.ts +218 -0
  282. package/src/lib/openapi/generator.ts +1311 -0
  283. package/src/lib/openapi/index.ts +4 -0
  284. package/src/lib/openapi/sanitize.ts +137 -0
  285. package/src/lib/openapi/types.ts +79 -0
  286. package/src/lib/profiler/index.ts +371 -0
  287. package/src/lib/query/__tests__/engine.test.ts +274 -0
  288. package/src/lib/query/engine.ts +837 -0
  289. package/src/lib/query/join-utils.ts +238 -0
  290. package/src/lib/query/types.ts +121 -0
  291. package/src/lib/search/config.ts +49 -0
  292. package/src/lib/search/tokenize.ts +45 -0
  293. package/src/lib/slugify.ts +28 -0
  294. package/src/lib/testing/bootstrap.ts +124 -0
  295. package/src/lib/testing/index.ts +15 -0
  296. package/src/lib/testing/renderWithProviders.tsx +31 -0
  297. package/src/lib/url.ts +12 -0
  298. package/src/lib/utils.ts +17 -0
  299. package/src/lib/version.ts +5 -0
  300. package/src/modules/__tests__/dsl.test.ts +35 -0
  301. package/src/modules/__tests__/registry.test.ts +300 -0
  302. package/src/modules/dashboard/widgets.ts +57 -0
  303. package/src/modules/dsl.ts +32 -0
  304. package/src/modules/entities/__tests__/validation.test.ts +52 -0
  305. package/src/modules/entities/kinds.ts +20 -0
  306. package/src/modules/entities/options.ts +36 -0
  307. package/src/modules/entities/validation.ts +118 -0
  308. package/src/modules/entities/validators.ts +93 -0
  309. package/src/modules/entities.ts +102 -0
  310. package/src/modules/navigation/sidebarPreferences.ts +62 -0
  311. package/src/modules/perspectives/types.ts +40 -0
  312. package/src/modules/registry.ts +249 -0
  313. package/src/modules/search.ts +325 -0
  314. package/src/modules/vector.ts +122 -0
  315. package/src/modules/widgets/__tests__/injection.test.ts +48 -0
  316. package/src/modules/widgets/injection-loader.ts +235 -0
  317. package/src/modules/widgets/injection.ts +120 -0
  318. package/src/security/features.ts +22 -0
  319. package/src/types/pg.d.ts +2 -0
  320. package/src/types/react-email.d.ts +2 -0
  321. package/src/types/resend.d.ts +2 -0
  322. package/tsconfig.build.json +11 -0
  323. package/tsconfig.json +9 -0
  324. package/watch.mjs +6 -0
@@ -0,0 +1,1311 @@
1
+ import { z, type ZodTypeAny } from 'zod'
2
+ import type { Module, ModuleApi, ModuleApiLegacy, ModuleApiRouteFile, HttpMethod } from '@open-mercato/shared/modules/registry'
3
+ import type {
4
+ OpenApiDocument,
5
+ OpenApiDocumentOptions,
6
+ OpenApiMethodDoc,
7
+ OpenApiRequestBodyDoc,
8
+ OpenApiResponseDoc,
9
+ OpenApiRouteDoc,
10
+ } from './types'
11
+
12
+ type PathParamInfo = {
13
+ name: string
14
+ catchAll?: boolean
15
+ optional?: boolean
16
+ }
17
+
18
+ type ParameterLocation = 'query' | 'path' | 'header'
19
+
20
+ type JsonSchema = Record<string, unknown>
21
+
22
+ type SchemaConversionContext = {
23
+ memo: WeakMap<ZodTypeAny, JsonSchema>
24
+ }
25
+
26
+ type ExampleGenerationContext = {
27
+ stack: WeakSet<ZodTypeAny>
28
+ }
29
+
30
+ type ExampleMap = {
31
+ query?: unknown
32
+ body?: unknown
33
+ path?: Record<string, unknown>
34
+ headers?: Record<string, unknown>
35
+ }
36
+
37
+ const HTTP_METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
38
+
39
+ function resolveType(def: any): string | undefined {
40
+ if (!def) return undefined
41
+ if (typeof def.typeName === 'string' && def.typeName.length) return def.typeName
42
+ if (typeof def.type === 'string' && def.type.length) return def.type
43
+ return undefined
44
+ }
45
+
46
+ function getShape(def: any): Record<string, ZodTypeAny> {
47
+ if (!def) return {}
48
+ const shape = typeof def.shape === 'function' ? def.shape() : def.shape
49
+ if (shape && typeof shape === 'object') return shape as Record<string, ZodTypeAny>
50
+ return {}
51
+ }
52
+
53
+ function normalizeChecks(checks: any[] | undefined): Array<{ kind?: string; value?: unknown; extra?: Record<string, unknown> }> {
54
+ if (!Array.isArray(checks)) return []
55
+ return checks.map((check) => {
56
+ if (!check) return {}
57
+ const base = (check as any)?._zod?.def ?? (check as any)?.def ?? check
58
+ const kind = typeof (check as any)?.kind === 'string'
59
+ ? (check as any).kind
60
+ : typeof base?.check === 'string'
61
+ ? base.check
62
+ : undefined
63
+ const value =
64
+ base?.value ??
65
+ base?.minimum ??
66
+ base?.maximum ??
67
+ base?.exact ??
68
+ base?.length ??
69
+ base?.limit ??
70
+ base?.includes ??
71
+ base?.min ??
72
+ base?.max
73
+ return { kind, value, extra: base && typeof base === 'object' ? base : undefined }
74
+ })
75
+ }
76
+
77
+ const DEFAULT_EXAMPLE_VALUES = {
78
+ string: 'string',
79
+ number: 1,
80
+ integer: 1,
81
+ boolean: true,
82
+ uuid: '00000000-0000-4000-8000-000000000000',
83
+ email: 'user@example.com',
84
+ url: 'https://example.com/resource',
85
+ datetime: new Date('2025-01-01T00:00:00.000Z').toISOString(),
86
+ }
87
+
88
+ function toTitle(str: string): string {
89
+ if (!str) return ''
90
+ return str
91
+ .split(/[_\-\s]+/)
92
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
93
+ .join(' ')
94
+ }
95
+
96
+ function normalizePath(path: string): { path: string; params: PathParamInfo[] } {
97
+ const segments = path.split('/').filter((seg) => seg.length > 0)
98
+ const params: PathParamInfo[] = []
99
+ const normalized = segments
100
+ .map((seg) => {
101
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/)
102
+ if (catchAll) {
103
+ params.push({ name: catchAll[1], catchAll: true })
104
+ return `{${catchAll[1]}}`
105
+ }
106
+ const optCatchAll = seg.match(/^\[\[\.\.\.(.+)\]\]$/)
107
+ if (optCatchAll) {
108
+ params.push({ name: optCatchAll[1], catchAll: true, optional: true })
109
+ return `{${optCatchAll[1]}}`
110
+ }
111
+ const dyn = seg.match(/^\[(.+)\]$/)
112
+ if (dyn) {
113
+ params.push({ name: dyn[1] })
114
+ return `{${dyn[1]}}`
115
+ }
116
+ return seg
117
+ })
118
+ .join('/')
119
+ return { path: '/' + normalized, params }
120
+ }
121
+
122
+ function unwrap(schema?: ZodTypeAny): {
123
+ schema: ZodTypeAny | undefined
124
+ optional: boolean
125
+ nullable: boolean
126
+ defaultValue?: unknown
127
+ } {
128
+ if (!schema) {
129
+ return { schema: undefined, optional: true, nullable: false }
130
+ }
131
+
132
+ let current: ZodTypeAny = schema
133
+ let optional = false
134
+ let nullable = false
135
+ let defaultValue: unknown
136
+ // eslint-disable-next-line no-constant-condition
137
+ while (true) {
138
+ const def = (current as any)?._def
139
+ if (!def) {
140
+ return { schema: current, optional, nullable, defaultValue }
141
+ }
142
+ const typeName = resolveType(def)
143
+ if (typeName === 'ZodOptional' || typeName === 'optional') {
144
+ optional = true
145
+ current = (current as any)._def.innerType
146
+ continue
147
+ }
148
+ if (typeName === 'ZodNullable' || typeName === 'nullable') {
149
+ nullable = true
150
+ current = (current as any)._def.innerType
151
+ continue
152
+ }
153
+ if (typeName === 'ZodDefault' || typeName === 'default') {
154
+ optional = true
155
+ const rawDefault = (current as any)._def.defaultValue
156
+ defaultValue = typeof rawDefault === 'function' ? rawDefault() : rawDefault
157
+ current = (current as any)._def.innerType
158
+ continue
159
+ }
160
+ if (typeName === 'ZodPipeline' || typeName === 'pipe') {
161
+ current = (current as any)._def.out ?? (current as any)._def.innerType ?? (current as any)._def.schema
162
+ continue
163
+ }
164
+ if (typeName === 'transformer') {
165
+ current = (current as any)._def.output
166
+ continue
167
+ }
168
+ if (typeName === 'ZodLazy' || typeName === 'lazy') {
169
+ const getter = (current as any)._def.getter
170
+ current = typeof getter === 'function' ? getter() : current
171
+ if (current === schema) break
172
+ continue
173
+ }
174
+ if (typeName === 'ZodPromise' || typeName === 'promise') {
175
+ current = (current as any)._def.type
176
+ continue
177
+ }
178
+ if (typeName === 'ZodCatch' || typeName === 'catch') {
179
+ current = (current as any)._def.innerType
180
+ continue
181
+ }
182
+ if (typeName === 'ZodReadonly' || typeName === 'readonly') {
183
+ current = (current as any)._def.innerType
184
+ continue
185
+ }
186
+ if (typeName === 'ZodBranded' || typeName === 'branded') {
187
+ current = (current as any)._def.type
188
+ continue
189
+ }
190
+ break
191
+ }
192
+ return { schema: current, optional, nullable, defaultValue }
193
+ }
194
+
195
+ function zodToJsonSchema(schema?: ZodTypeAny, ctx?: SchemaConversionContext): JsonSchema | undefined {
196
+ if (!schema) return undefined
197
+ const context: SchemaConversionContext = ctx ?? { memo: new WeakMap<ZodTypeAny, JsonSchema>() }
198
+
199
+ const cached = context.memo.get(schema)
200
+ if (cached) return cached
201
+
202
+ const placeholder: JsonSchema = {}
203
+ context.memo.set(schema, placeholder)
204
+
205
+ const { schema: inner, nullable } = unwrap(schema)
206
+ if (!inner) {
207
+ if (nullable) placeholder.nullable = true
208
+ return placeholder
209
+ }
210
+
211
+ if (inner !== schema && typeof inner === 'object') {
212
+ if (!context.memo.has(inner as ZodTypeAny)) {
213
+ context.memo.set(inner as ZodTypeAny, placeholder)
214
+ }
215
+ }
216
+
217
+ const def = (inner as any)._def
218
+ if (!def) return placeholder
219
+ const typeName = resolveType(def) as string | undefined
220
+
221
+ const result = placeholder
222
+
223
+ switch (typeName) {
224
+ case 'ZodString':
225
+ case 'string': {
226
+ result.type = 'string'
227
+ const checks = normalizeChecks(def.checks)
228
+ for (const check of checks) {
229
+ if (!check.kind) continue
230
+ if (check.kind === 'uuid' || check.extra?.format === 'uuid') {
231
+ result.format = 'uuid'
232
+ } else if (check.kind === 'email' || check.extra?.format === 'email') {
233
+ result.format = 'email'
234
+ } else if (check.kind === 'url' || check.extra?.format === 'uri' || check.extra?.format === 'url') {
235
+ result.format = 'uri'
236
+ } else if (check.kind === 'regex' && check.extra?.pattern instanceof RegExp) {
237
+ result.pattern = check.extra.pattern.source
238
+ } else if (check.kind === 'string_format' && typeof check.extra?.format === 'string') {
239
+ result.format = check.extra.format
240
+ } else if (check.kind === 'datetime' || check.extra?.format === 'date-time') {
241
+ result.format = 'date-time'
242
+ } else if (['length', 'len', 'exact_length'].includes(check.kind ?? '')) {
243
+ if (typeof check.value === 'number') {
244
+ result.minLength = check.value
245
+ result.maxLength = check.value
246
+ }
247
+ } else if (check.kind === 'min' || check.kind === 'min_length') {
248
+ if (typeof check.value === 'number') result.minLength = check.value
249
+ } else if (check.kind === 'max' || check.kind === 'max_length') {
250
+ if (typeof check.value === 'number') result.maxLength = check.value
251
+ }
252
+ }
253
+ break
254
+ }
255
+ case 'ZodNumber':
256
+ case 'number': {
257
+ result.type = 'number'
258
+ const checks = normalizeChecks(def.checks)
259
+ for (const check of checks) {
260
+ if (!check.kind) continue
261
+ if (check.kind === 'int' || check.kind === 'isInteger') result.type = 'integer'
262
+ if ((check.kind === 'min' || check.kind === 'gte') && typeof check.value === 'number') result.minimum = check.value
263
+ if ((check.kind === 'max' || check.kind === 'lte') && typeof check.value === 'number') result.maximum = check.value
264
+ if (check.kind === 'multipleOf' && typeof check.value === 'number') result.multipleOf = check.value
265
+ }
266
+ break
267
+ }
268
+ case 'ZodBigInt':
269
+ case 'bigint':
270
+ result.type = 'integer'
271
+ result.format = 'int64'
272
+ break
273
+ case 'ZodBoolean':
274
+ case 'boolean':
275
+ result.type = 'boolean'
276
+ break
277
+ case 'ZodLiteral':
278
+ case 'literal': {
279
+ const value = def.value ?? (Array.isArray(def.values) ? def.values[0] : undefined)
280
+ result.type = typeof value
281
+ result.enum = [value]
282
+ break
283
+ }
284
+ case 'ZodEnum':
285
+ case 'enum': {
286
+ const entries = def.values ?? def.entries
287
+ const values = Array.isArray(entries) ? entries : entries ? Object.values(entries) : []
288
+ const enumerators = values.filter((v: unknown) => typeof v === 'string' || typeof v === 'number')
289
+ const allString = enumerators.every((v: unknown) => typeof v === 'string')
290
+ result.type = allString ? 'string' : 'number'
291
+ result.enum = enumerators
292
+ break
293
+ }
294
+ case 'ZodNativeEnum': {
295
+ const values = Object.values(def.values).filter((v) => typeof v === 'string' || typeof v === 'number')
296
+ const allString = values.every((v) => typeof v === 'string')
297
+ result.type = allString ? 'string' : 'number'
298
+ result.enum = values
299
+ break
300
+ }
301
+ case 'ZodUnion':
302
+ case 'union': {
303
+ const options = def.options || []
304
+ result.oneOf = options.map((option: ZodTypeAny) => zodToJsonSchema(option, context) ?? {})
305
+ break
306
+ }
307
+ case 'ZodIntersection':
308
+ case 'intersection': {
309
+ result.allOf = [
310
+ zodToJsonSchema(def.left, context) ?? {},
311
+ zodToJsonSchema(def.right, context) ?? {},
312
+ ]
313
+ break
314
+ }
315
+ case 'ZodPipeline':
316
+ case 'pipe': {
317
+ const resolved = zodToJsonSchema(def.out ?? def.innerType ?? def.schema, context) ?? {}
318
+ Object.assign(result, resolved)
319
+ break
320
+ }
321
+ case 'ZodLazy':
322
+ case 'lazy': {
323
+ const next = typeof def.getter === 'function' ? def.getter() : undefined
324
+ const resolved = next ? zodToJsonSchema(next, context) : undefined
325
+ if (resolved) Object.assign(result, resolved)
326
+ break
327
+ }
328
+ case 'ZodPromise':
329
+ case 'promise': {
330
+ const resolved = zodToJsonSchema(def.type, context)
331
+ if (resolved) Object.assign(result, resolved)
332
+ break
333
+ }
334
+ case 'ZodCatch':
335
+ case 'catch': {
336
+ const resolved = zodToJsonSchema(def.innerType ?? def.type, context)
337
+ if (resolved) Object.assign(result, resolved)
338
+ break
339
+ }
340
+ case 'ZodReadonly':
341
+ case 'readonly': {
342
+ const resolved = zodToJsonSchema(def.innerType ?? def.type, context)
343
+ if (resolved) Object.assign(result, resolved)
344
+ break
345
+ }
346
+ case 'ZodArray':
347
+ case 'array': {
348
+ const elementSchema =
349
+ def.type && typeof def.type === 'object'
350
+ ? def.type
351
+ : (def.element && typeof def.element === 'object' ? def.element : undefined)
352
+ result.type = 'array'
353
+ result.items = zodToJsonSchema(elementSchema as ZodTypeAny, context) ?? {}
354
+ if (typeof def.minLength === 'number') result.minItems = def.minLength
355
+ if (typeof def.maxLength === 'number') result.maxItems = def.maxLength
356
+ const checks = normalizeChecks(def.checks)
357
+ for (const check of checks) {
358
+ if (check.kind === 'min_length' && typeof check.value === 'number') result.minItems = check.value
359
+ if (check.kind === 'max_length' && typeof check.value === 'number') result.maxItems = check.value
360
+ if (check.kind === 'length' && typeof check.value === 'number') {
361
+ result.minItems = check.value
362
+ result.maxItems = check.value
363
+ }
364
+ }
365
+ break
366
+ }
367
+ case 'ZodTuple':
368
+ case 'tuple': {
369
+ const items = def.items || []
370
+ result.type = 'array'
371
+ result.prefixItems = items.map((item: ZodTypeAny) => zodToJsonSchema(item, context) ?? {})
372
+ result.minItems = items.length
373
+ result.maxItems = items.length
374
+ break
375
+ }
376
+ case 'ZodRecord':
377
+ case 'record': {
378
+ result.type = 'object'
379
+ result.additionalProperties = zodToJsonSchema(def.valueType ?? def.value, context) ?? {}
380
+ break
381
+ }
382
+ case 'ZodObject':
383
+ case 'object': {
384
+ result.type = 'object'
385
+ const shape = getShape(def)
386
+ const properties: Record<string, JsonSchema> = {}
387
+ const required: string[] = []
388
+ for (const [key, rawSchema] of Object.entries(shape)) {
389
+ const unwrapped = unwrap(rawSchema as ZodTypeAny)
390
+ const childSchema = zodToJsonSchema(unwrapped.schema, context)
391
+ if (!childSchema) continue
392
+ const baseSchema = childSchema
393
+ let propertySchema: JsonSchema = baseSchema
394
+ if (unwrapped.nullable) {
395
+ propertySchema = {
396
+ anyOf: [{ type: 'null' }, propertySchema],
397
+ }
398
+ }
399
+ if (unwrapped.defaultValue !== undefined) {
400
+ if (propertySchema === baseSchema) {
401
+ propertySchema = { allOf: [baseSchema], default: unwrapped.defaultValue }
402
+ } else {
403
+ propertySchema = { ...propertySchema, default: unwrapped.defaultValue }
404
+ }
405
+ }
406
+ properties[key] = propertySchema
407
+ if (!unwrapped.optional) required.push(key)
408
+ }
409
+ result.properties = properties
410
+ if (required.length > 0) result.required = required
411
+ if (def.unknownKeys === 'passthrough') {
412
+ result.additionalProperties = true
413
+ } else if (def.catchall && resolveType(def.catchall._def) !== 'ZodNever' && resolveType(def.catchall._def) !== 'never') {
414
+ result.additionalProperties = zodToJsonSchema(def.catchall, context) ?? true
415
+ } else {
416
+ result.additionalProperties = false
417
+ }
418
+ break
419
+ }
420
+ case 'ZodDate':
421
+ case 'date':
422
+ result.type = 'string'
423
+ result.format = 'date-time'
424
+ break
425
+ case 'ZodNull':
426
+ case 'null':
427
+ result.type = 'null'
428
+ break
429
+ case 'ZodVoid':
430
+ case 'void':
431
+ case 'ZodNever':
432
+ case 'never':
433
+ break
434
+ case 'ZodAny':
435
+ case 'any':
436
+ case 'ZodUnknown':
437
+ case 'unknown':
438
+ case 'ZodNaN':
439
+ case 'nan':
440
+ default:
441
+ break
442
+ }
443
+
444
+ if (nullable) {
445
+ if (result.type && result.type !== 'null') {
446
+ result.nullable = true
447
+ } else if (!result.type) {
448
+ const clone = { ...result }
449
+ result.anyOf = [{ type: 'null' }, clone]
450
+ }
451
+ }
452
+
453
+ return result
454
+ }
455
+
456
+ function generateExample(schema?: ZodTypeAny, ctx?: ExampleGenerationContext): unknown {
457
+ if (!schema) return undefined
458
+ if ((typeof schema !== 'object' || schema === null) && typeof schema !== 'function') return undefined
459
+ const trackable = schema as object
460
+ const context: ExampleGenerationContext = ctx ?? { stack: new WeakSet<ZodTypeAny>() }
461
+ if (context.stack.has(trackable as ZodTypeAny)) return undefined
462
+ context.stack.add(trackable as ZodTypeAny)
463
+
464
+ try {
465
+ const { schema: inner, optional, nullable, defaultValue } = unwrap(schema)
466
+ if (!inner) {
467
+ if (defaultValue !== undefined) return defaultValue
468
+ if (nullable) return null
469
+ if (optional) return undefined
470
+ return undefined
471
+ }
472
+ const def = (inner as any)._def
473
+ const typeName = resolveType(def) as string | undefined
474
+ if (defaultValue !== undefined) return defaultValue
475
+
476
+ if (nullable) return null
477
+ if (optional) return undefined
478
+
479
+ switch (typeName) {
480
+ case 'ZodString':
481
+ case 'string': {
482
+ const checks = normalizeChecks(def?.checks)
483
+ for (const check of checks) {
484
+ if (!check.kind && !check.extra?.format) continue
485
+ if (check.kind === 'uuid' || check.extra?.format === 'uuid') return DEFAULT_EXAMPLE_VALUES.uuid
486
+ if (check.kind === 'email' || check.extra?.format === 'email') return DEFAULT_EXAMPLE_VALUES.email
487
+ if (check.kind === 'url' || check.extra?.format === 'url' || check.extra?.format === 'uri') return DEFAULT_EXAMPLE_VALUES.url
488
+ if (check.kind === 'datetime' || check.extra?.format === 'date-time') return DEFAULT_EXAMPLE_VALUES.datetime
489
+ }
490
+ return DEFAULT_EXAMPLE_VALUES.string
491
+ }
492
+ case 'ZodNumber':
493
+ case 'number': {
494
+ const checks = normalizeChecks(def?.checks)
495
+ const isInt = checks.some((check) => check.kind === 'int' || check.kind === 'isInteger')
496
+ return isInt ? DEFAULT_EXAMPLE_VALUES.integer : DEFAULT_EXAMPLE_VALUES.number
497
+ }
498
+ case 'ZodBigInt':
499
+ case 'bigint':
500
+ return BigInt(1)
501
+ case 'ZodBoolean':
502
+ case 'boolean':
503
+ return DEFAULT_EXAMPLE_VALUES.boolean
504
+ case 'ZodEnum':
505
+ case 'enum': {
506
+ const entries = def?.values ?? def?.entries
507
+ const values = Array.isArray(entries) ? entries : entries ? Object.values(entries) : []
508
+ return values[0]
509
+ }
510
+ case 'ZodNativeEnum': {
511
+ const values = Object.values(def?.values || [])
512
+ return values[0]
513
+ }
514
+ case 'ZodLiteral':
515
+ case 'literal':
516
+ return def?.value ?? (Array.isArray(def?.values) ? def.values[0] : undefined)
517
+ case 'ZodArray':
518
+ case 'array': {
519
+ const elementSchema =
520
+ def?.type && typeof def.type === 'object'
521
+ ? def.type
522
+ : (def?.element && typeof def.element === 'object' ? def.element : undefined)
523
+ const child = generateExample(elementSchema, context)
524
+ return child === undefined ? [] : [child]
525
+ }
526
+ case 'ZodTuple':
527
+ case 'tuple': {
528
+ const items = def?.items || []
529
+ return items.map((item: ZodTypeAny) => generateExample(item, context))
530
+ }
531
+ case 'ZodObject':
532
+ case 'object': {
533
+ const shape = getShape(def)
534
+ const obj: Record<string, unknown> = {}
535
+ for (const [key, value] of Object.entries(shape)) {
536
+ const example = generateExample(value as ZodTypeAny, context)
537
+ if (example !== undefined) obj[key] = example
538
+ }
539
+ return obj
540
+ }
541
+ case 'ZodRecord':
542
+ case 'record': {
543
+ const valueExample = generateExample(def?.valueType ?? def?.value, context)
544
+ return valueExample === undefined ? {} : { key: valueExample }
545
+ }
546
+ case 'ZodUnion':
547
+ case 'union': {
548
+ const options = def?.options || []
549
+ return options.length ? generateExample(options[0], context) : undefined
550
+ }
551
+ case 'ZodPipeline':
552
+ case 'pipe':
553
+ return generateExample(def?.out ?? def?.innerType ?? def?.schema, context)
554
+ case 'ZodLazy':
555
+ case 'lazy': {
556
+ const next = typeof def?.getter === 'function' ? def.getter() : undefined
557
+ return next ? generateExample(next, context) : undefined
558
+ }
559
+ case 'ZodPromise':
560
+ case 'promise':
561
+ return generateExample(def?.type, context)
562
+ case 'ZodCatch':
563
+ case 'catch':
564
+ return generateExample(def?.innerType ?? def?.type, context)
565
+ case 'ZodReadonly':
566
+ case 'readonly':
567
+ return generateExample(def?.innerType ?? def?.type, context)
568
+ case 'ZodIntersection':
569
+ case 'intersection': {
570
+ const left = generateExample(def?.left, context)
571
+ const right = generateExample(def?.right, context)
572
+ if (typeof left === 'object' && left && typeof right === 'object' && right) {
573
+ return { ...(left as object), ...(right as object) }
574
+ }
575
+ return left ?? right
576
+ }
577
+ case 'ZodDate':
578
+ case 'date':
579
+ return DEFAULT_EXAMPLE_VALUES.datetime
580
+ default:
581
+ return undefined
582
+ }
583
+ } finally {
584
+ context.stack.delete(trackable as ZodTypeAny)
585
+ }
586
+ }
587
+
588
+ function buildParameters(
589
+ schema: ZodTypeAny | undefined,
590
+ location: ParameterLocation,
591
+ pathParamNames?: PathParamInfo[]
592
+ ): Array<Record<string, unknown>> {
593
+ if (!schema && location !== 'path') return []
594
+
595
+ const params: Array<Record<string, unknown>> = []
596
+
597
+ if (location === 'path' && pathParamNames && pathParamNames.length) {
598
+ const merged = mergePathParamSchemas(schema, pathParamNames)
599
+ for (const { name, schema: paramSchema, optional } of merged) {
600
+ const jsonSchema = zodToJsonSchema(paramSchema)
601
+ const example = generateExample(paramSchema)
602
+ params.push({
603
+ name,
604
+ in: 'path',
605
+ required: !optional,
606
+ schema: jsonSchema ?? { type: 'string' },
607
+ example,
608
+ })
609
+ }
610
+ return params
611
+ }
612
+
613
+ if (!schema) return params
614
+
615
+ const { schema: unwrapped } = unwrap(schema)
616
+ if (!unwrapped) return params
617
+ const def = (unwrapped as any)._def
618
+ const typeName = resolveType(def) as string | undefined
619
+ if (typeName === 'ZodObject' || typeName === 'object') {
620
+ const shape = getShape(def)
621
+ for (const [key, raw] of Object.entries(shape)) {
622
+ const details = unwrap(raw as ZodTypeAny)
623
+ if (!details.schema) continue
624
+ const jsonSchema = zodToJsonSchema(details.schema)
625
+ const example = generateExample(details.schema)
626
+ params.push({
627
+ name: key,
628
+ in: location,
629
+ required: location === 'path' ? true : !details.optional,
630
+ schema: jsonSchema ?? {},
631
+ example,
632
+ })
633
+ }
634
+ } else {
635
+ const jsonSchema = zodToJsonSchema(unwrapped)
636
+ const example = generateExample(unwrapped)
637
+ params.push({
638
+ name: location === 'header' ? 'X-Custom-Header' : 'value',
639
+ in: location,
640
+ required: location === 'path',
641
+ schema: jsonSchema ?? {},
642
+ example,
643
+ })
644
+ }
645
+
646
+ return params
647
+ }
648
+
649
+ function mergePathParamSchemas(schema: ZodTypeAny | undefined, params: PathParamInfo[]) {
650
+ const merged: Array<{ name: string; schema: ZodTypeAny | undefined; optional: boolean }> = []
651
+ const map: Record<string, ZodTypeAny> = {}
652
+ if (schema) {
653
+ const { schema: unwrapped } = unwrap(schema)
654
+ if (unwrapped && (unwrapped as any)._def && (resolveType((unwrapped as any)._def) === 'ZodObject' || resolveType((unwrapped as any)._def) === 'object')) {
655
+ const shape = getShape((unwrapped as any)._def)
656
+ for (const [key, value] of Object.entries(shape)) {
657
+ map[key] = value as ZodTypeAny
658
+ }
659
+ }
660
+ }
661
+ for (const param of params) {
662
+ merged.push({
663
+ name: param.name,
664
+ schema: map[param.name],
665
+ optional: !!param.optional,
666
+ })
667
+ }
668
+ return merged
669
+ }
670
+
671
+ function buildRequestBody(request?: OpenApiRequestBodyDoc): Record<string, unknown> | undefined {
672
+ if (!request) return undefined
673
+ const schema = zodToJsonSchema(request.schema)
674
+ const example = request.example ?? generateExample(request.schema)
675
+ const contentType = request.contentType ?? 'application/json'
676
+ return {
677
+ required: true,
678
+ content: {
679
+ [contentType]: {
680
+ schema: schema ?? {},
681
+ example,
682
+ },
683
+ },
684
+ description: request.description,
685
+ }
686
+ }
687
+
688
+ function buildResponses(
689
+ method: HttpMethod,
690
+ responses?: OpenApiResponseDoc[],
691
+ errors?: OpenApiResponseDoc[],
692
+ metadata?: any
693
+ ): Record<string, unknown> {
694
+ const entries: Record<string, unknown> = {}
695
+ const list = [...(responses ?? [])]
696
+ const errorList = [...(errors ?? [])]
697
+ if (metadata?.requireAuth) {
698
+ errorList.push({
699
+ status: 401,
700
+ description: 'Unauthorized',
701
+ schema: z.object({ error: z.string() }),
702
+ xAutoGenerated: true,
703
+ })
704
+ }
705
+ if (Array.isArray(metadata?.requireFeatures) && metadata.requireFeatures.length) {
706
+ errorList.push({
707
+ status: 403,
708
+ description: 'Forbidden – missing required features',
709
+ schema: z.object({ error: z.string() }),
710
+ xAutoGenerated: true,
711
+ })
712
+ }
713
+ if (!list.some((res) => res.status >= 200 && res.status < 300)) {
714
+ const fallbackStatus = method === 'POST' ? 201 : method === 'DELETE' ? 204 : 200
715
+ list.push({
716
+ status: fallbackStatus,
717
+ description: fallbackStatus === 204 ? 'Success' : 'Success response',
718
+ })
719
+ }
720
+ for (const res of [...list, ...errorList]) {
721
+ const status = String(res.status || 200)
722
+ const mediaType = res.mediaType ?? 'application/json'
723
+ const schema = res.schema ? zodToJsonSchema(res.schema) : undefined
724
+ const example = res.schema ? res.example ?? generateExample(res.schema) : res.example
725
+ const isNoContent = res.status === 204
726
+ entries[status] = {
727
+ description: res.description ?? '',
728
+ ...(isNoContent
729
+ ? {}
730
+ : {
731
+ content: {
732
+ [mediaType]: {
733
+ schema: schema ?? { type: 'object' },
734
+ ...(example !== undefined ? { example } : {}),
735
+ },
736
+ },
737
+ }),
738
+ ...(res.xAutoGenerated ? { 'x-autoGenerated': true } : {}),
739
+ }
740
+ }
741
+ return entries
742
+ }
743
+
744
+ function buildSecurity(metadata: any, methodDoc?: OpenApiMethodDoc, defaults?: string[]) {
745
+ const securitySchemes = new Set<string>()
746
+ if (Array.isArray(methodDoc?.security)) methodDoc.security.forEach((s) => securitySchemes.add(s))
747
+ if (metadata?.requireAuth) securitySchemes.add('bearerAuth')
748
+ if (defaults) defaults.forEach((s) => securitySchemes.add(s))
749
+ if (securitySchemes.size === 0) return undefined
750
+ return Array.from(securitySchemes.values()).map((scheme) => ({ [scheme]: [] }))
751
+ }
752
+
753
+ function collectExamples(
754
+ querySchema?: ZodTypeAny,
755
+ bodySchema?: ZodTypeAny,
756
+ pathSchema?: ZodTypeAny,
757
+ headerSchema?: ZodTypeAny,
758
+ metadata?: any
759
+ ): ExampleMap {
760
+ const examples: ExampleMap = {}
761
+ const queryExample = querySchema ? generateExample(querySchema) : undefined
762
+ if (queryExample && typeof queryExample === 'object') examples.query = queryExample
763
+ const bodyExample = bodySchema ? generateExample(bodySchema) : undefined
764
+ if (bodyExample !== undefined) examples.body = bodyExample
765
+ const pathExample = pathSchema ? generateExample(pathSchema) : undefined
766
+ if (pathExample && typeof pathExample === 'object') examples.path = pathExample as Record<string, unknown>
767
+ const headerExample = headerSchema ? generateExample(headerSchema) : undefined
768
+ if (headerExample && typeof headerExample === 'object') examples.headers = headerExample as Record<string, unknown>
769
+ if (metadata?.requireAuth) {
770
+ if (!examples.headers) examples.headers = {}
771
+ if (typeof examples.headers.authorization !== 'string') {
772
+ examples.headers.authorization = 'Bearer <token>'
773
+ }
774
+ }
775
+ return examples
776
+ }
777
+
778
+ function toFormUrlEncoded(value: unknown): string {
779
+ if (!value || typeof value !== 'object') return ''
780
+ const params = new URLSearchParams()
781
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
782
+ if (raw === undefined) continue
783
+ params.append(key, raw === null ? '' : String(raw))
784
+ }
785
+ return params.toString()
786
+ }
787
+
788
+ function stringifyBodyExample(value: unknown, mediaType?: string): string {
789
+ if (value === undefined) return ''
790
+ if (mediaType === 'application/x-www-form-urlencoded') {
791
+ return toFormUrlEncoded(value)
792
+ }
793
+ if (!mediaType || mediaType === 'application/json') {
794
+ try {
795
+ return JSON.stringify(value, null, 2)
796
+ } catch {
797
+ return ''
798
+ }
799
+ }
800
+ if (typeof value === 'string') return value
801
+ try {
802
+ return JSON.stringify(value, null, 2)
803
+ } catch {
804
+ return String(value)
805
+ }
806
+ }
807
+
808
+ function buildQueryString(example: unknown): string {
809
+ if (!example || typeof example !== 'object') return ''
810
+ const parts: string[] = []
811
+ for (const [key, value] of Object.entries(example as Record<string, unknown>)) {
812
+ if (value === undefined || value === null) continue
813
+ const encoded = encodeURIComponent(String(value))
814
+ parts.push(`${encodeURIComponent(key)}=${encoded}`)
815
+ }
816
+ return parts.length ? `?${parts.join('&')}` : ''
817
+ }
818
+
819
+ function injectPathExamples(path: string, params: PathParamInfo[], examples?: Record<string, unknown>): string {
820
+ if (!params.length) return path
821
+ let result = path
822
+ for (const param of params) {
823
+ const placeholder = `{${param.name}}`
824
+ const example = examples && examples[param.name] !== undefined ? examples[param.name] : `:${param.name}`
825
+ result = result.replace(placeholder, String(example))
826
+ }
827
+ return result
828
+ }
829
+
830
+ function buildCurlSample(
831
+ method: HttpMethod,
832
+ path: string,
833
+ params: PathParamInfo[],
834
+ examples: ExampleMap,
835
+ baseUrl: string,
836
+ metadata: any,
837
+ requestBody?: OpenApiRequestBodyDoc
838
+ ): string {
839
+ const lines: string[] = []
840
+ const pathWithExamples = injectPathExamples(path, params, examples.path)
841
+ const query = buildQueryString(examples.query)
842
+ const url = baseUrl.replace(/\/$/, '') + pathWithExamples + query
843
+ lines.push(`curl -X ${method} "${url}"`)
844
+
845
+ lines.push(' -H "Accept: application/json"')
846
+
847
+ const headers: Record<string, unknown> = { ...(examples.headers ?? {}) }
848
+ if (metadata?.requireAuth && !headers.Authorization && !headers.authorization) {
849
+ headers.Authorization = 'Bearer <token>'
850
+ }
851
+ for (const [key, value] of Object.entries(headers)) {
852
+ lines.push(` -H "${key.replace(/"/g, '')}: ${String(value).replace(/"/g, '')}"`)
853
+ }
854
+
855
+ const bodyExample = examples.body ?? requestBody?.example
856
+ const requestContentType = requestBody?.contentType ?? 'application/json'
857
+ if (bodyExample !== undefined) {
858
+ lines.push(` -H "Content-Type: ${requestContentType}"`)
859
+ const serialized = stringifyBodyExample(bodyExample, requestContentType)
860
+ if (serialized) {
861
+ const escapedSerialized = escapeShellDoubleQuotes(serialized)
862
+ lines.push(` -d "${escapedSerialized}"`)
863
+ }
864
+ }
865
+
866
+ return lines.join(' \\\n')
867
+ }
868
+
869
+ function escapeShellDoubleQuotes(value: string): string {
870
+ return value.replace(/[\\`"$]/g, '\\$&')
871
+ }
872
+
873
+ function ensureSecurityComponents(doc: OpenApiDocument) {
874
+ if (!doc.components) doc.components = {}
875
+ if (!doc.components.securitySchemes) doc.components.securitySchemes = {}
876
+ if (!doc.components.securitySchemes.bearerAuth) {
877
+ doc.components.securitySchemes.bearerAuth = {
878
+ type: 'http',
879
+ scheme: 'bearer',
880
+ bearerFormat: 'JWT',
881
+ description: 'Send an `Authorization: Bearer <token>` header with a valid API token.',
882
+ }
883
+ }
884
+ }
885
+
886
+ function resolveOperationId(moduleId: string, path: string, method: HttpMethod): string {
887
+ const cleaned = normalizeOperationIdSegment(path)
888
+ return [moduleId, method.toLowerCase(), cleaned].filter(Boolean).join('_')
889
+ }
890
+
891
+ function normalizeOperationIdSegment(input: string): string {
892
+ let output = ''
893
+ let previousUnderscore = false
894
+
895
+ for (const character of input) {
896
+ const codePoint = character.charCodeAt(0)
897
+ const isLower = codePoint >= 97 && codePoint <= 122
898
+ const isUpper = codePoint >= 65 && codePoint <= 90
899
+ const isNumber = codePoint >= 48 && codePoint <= 57
900
+ const isAlphaNumeric = isLower || isUpper || isNumber
901
+
902
+ if (isAlphaNumeric) {
903
+ output += character
904
+ previousUnderscore = false
905
+ continue
906
+ }
907
+
908
+ if (!previousUnderscore) {
909
+ output += '_'
910
+ previousUnderscore = true
911
+ }
912
+ }
913
+
914
+ while (output.startsWith('_')) output = output.slice(1)
915
+ while (output.endsWith('_')) output = output.slice(0, -1)
916
+
917
+ return output
918
+ }
919
+
920
+ function collectRouteDoc(api: ModuleApi, moduleId: string): OpenApiRouteDoc | undefined {
921
+ if ('handlers' in api) {
922
+ const route = api as ModuleApiRouteFile & { docs?: OpenApiRouteDoc }
923
+ if (route.docs) return route.docs
924
+ const maybe = (route.handlers as any)?.openApi
925
+ if (maybe && typeof maybe === 'object') return maybe as OpenApiRouteDoc
926
+ } else {
927
+ const legacy = api as ModuleApiLegacy & { docs?: OpenApiMethodDoc }
928
+ if (legacy.docs) {
929
+ return {
930
+ methods: { [legacy.method]: legacy.docs },
931
+ }
932
+ }
933
+ const maybe = (legacy.handler as any)?.openApi
934
+ if (maybe && typeof maybe === 'object') {
935
+ return {
936
+ methods: { [legacy.method]: maybe as OpenApiMethodDoc },
937
+ }
938
+ }
939
+ }
940
+ return undefined
941
+ }
942
+
943
+ export function buildOpenApiDocument(modules: Module[], options: OpenApiDocumentOptions = {}): OpenApiDocument {
944
+ const doc: OpenApiDocument = {
945
+ openapi: '3.1.0',
946
+ info: {
947
+ title: options.title ?? 'Open Mercato API',
948
+ version: options.version ?? '1.0.0',
949
+ description: options.description,
950
+ },
951
+ servers: options.servers,
952
+ paths: {},
953
+ }
954
+
955
+ ensureSecurityComponents(doc)
956
+
957
+ const tags = new Map<string, string | undefined>()
958
+
959
+ for (const moduleEntry of modules) {
960
+ const defaultTag = moduleEntry.info?.title ?? toTitle(moduleEntry.id)
961
+ if (defaultTag) tags.set(defaultTag, moduleEntry.info?.description)
962
+
963
+ const apis = moduleEntry.apis ?? []
964
+ for (const api of apis) {
965
+ const routeDoc = collectRouteDoc(api, moduleEntry.id)
966
+ const moduleTag = routeDoc?.tag ?? defaultTag
967
+ const normalized = normalizePath((api as any).path ?? (api as any).path ?? '')
968
+ const pathKey = normalized.path
969
+ if (!doc.paths[pathKey]) doc.paths[pathKey] = {}
970
+ const availableMethods: HttpMethod[] =
971
+ 'handlers' in api
972
+ ? HTTP_METHODS.filter((method) => typeof (api as ModuleApiRouteFile).handlers?.[method] === 'function')
973
+ : [api.method as HttpMethod]
974
+
975
+ for (const method of availableMethods) {
976
+ const methodLower = method.toLowerCase() as Lowercase<HttpMethod>
977
+ const existing = doc.paths[pathKey][methodLower]
978
+ if (existing) continue
979
+
980
+ const metadata = 'handlers' in api ? (api as ModuleApiRouteFile).metadata?.[method] : undefined
981
+ const methodDoc = routeDoc?.methods?.[method]
982
+ const summary = methodDoc?.summary ?? routeDoc?.summary ?? `${method} ${pathKey}`
983
+ const baseDescription = methodDoc?.description ?? routeDoc?.description
984
+ const meta = metadata && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : undefined
985
+ const requireFeatures = Array.isArray(meta?.['requireFeatures'])
986
+ ? (meta!['requireFeatures'] as string[])
987
+ : undefined
988
+ const requireRoles = Array.isArray(meta?.['requireRoles'])
989
+ ? (meta!['requireRoles'] as string[])
990
+ : undefined
991
+ const requireAuth = meta?.['requireAuth'] === true
992
+ const descriptionParts: string[] = []
993
+ if (baseDescription) descriptionParts.push(baseDescription)
994
+ if (Array.isArray(requireFeatures) && requireFeatures.length) {
995
+ descriptionParts.push(`Requires features: ${requireFeatures.join(', ')}`)
996
+ }
997
+ if (Array.isArray(requireRoles) && requireRoles.length) {
998
+ descriptionParts.push(`Requires roles: ${requireRoles.join(', ')}`)
999
+ }
1000
+
1001
+ const querySchema = methodDoc?.query
1002
+ const pathSchema = methodDoc?.pathParams ?? routeDoc?.pathParams
1003
+ const headerSchema = methodDoc?.headers
1004
+ const requestBody = methodDoc?.requestBody
1005
+ const examples = collectExamples(querySchema, requestBody?.schema, pathSchema, headerSchema, metadata)
1006
+ const curlSample = buildCurlSample(
1007
+ method,
1008
+ pathKey,
1009
+ normalized.params,
1010
+ examples,
1011
+ options.baseUrlForExamples ?? 'https://api.open-mercato.local',
1012
+ metadata,
1013
+ requestBody
1014
+ )
1015
+
1016
+ doc.paths[pathKey][methodLower] = {
1017
+ operationId: methodDoc?.operationId ?? resolveOperationId(moduleEntry.id, pathKey, method),
1018
+ summary,
1019
+ description: descriptionParts.length ? descriptionParts.join('\n\n') : undefined,
1020
+ tags: methodDoc?.tags ?? (moduleTag ? [moduleTag] : undefined),
1021
+ deprecated: methodDoc?.deprecated,
1022
+ externalDocs: methodDoc?.externalDocs,
1023
+ parameters: [
1024
+ ...buildParameters(pathSchema, 'path', normalized.params),
1025
+ ...buildParameters(querySchema, 'query'),
1026
+ ...buildParameters(headerSchema, 'header'),
1027
+ ].filter(Boolean),
1028
+ requestBody: buildRequestBody(requestBody),
1029
+ responses: buildResponses(method, methodDoc?.responses, methodDoc?.errors, metadata),
1030
+ security: buildSecurity(metadata, methodDoc, options.defaultSecurity),
1031
+ 'x-codeSamples': methodDoc?.codeSamples ?? [
1032
+ {
1033
+ lang: 'curl',
1034
+ label: 'cURL',
1035
+ source: curlSample,
1036
+ },
1037
+ ],
1038
+ ...(Array.isArray(requireFeatures) && requireFeatures.length ? { 'x-require-features': requireFeatures } : {}),
1039
+ ...(Array.isArray(requireRoles) && requireRoles.length ? { 'x-require-roles': requireRoles } : {}),
1040
+ ...(requireAuth ? { 'x-require-auth': true } : {}),
1041
+ ...(methodDoc?.extensions ?? {}),
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ doc.tags = Array.from(tags.entries()).map(([name, description]) => ({
1048
+ name,
1049
+ description: description ?? undefined,
1050
+ }))
1051
+
1052
+ return doc
1053
+ }
1054
+
1055
+ function formatMarkdownTable(rows: Array<[string, string, string, string]>): string {
1056
+ if (!rows.length) return ''
1057
+ const header = ['Name', 'Location', 'Type', 'Description']
1058
+ const align = ['---', '---', '---', '---']
1059
+ const lines = [
1060
+ `| ${header.join(' | ')} |`,
1061
+ `| ${align.join(' | ')} |`,
1062
+ ...rows.map((row) => `| ${row.join(' | ')} |`),
1063
+ ]
1064
+ return lines.join('\n')
1065
+ }
1066
+
1067
+ function schemaTypeLabel(schema: any): string {
1068
+ if (!schema) return 'any'
1069
+ if (schema.type) return schema.type
1070
+ if (schema.oneOf) return schema.oneOf.map(schemaTypeLabel).join(' | ')
1071
+ if (schema.allOf) return schema.allOf.map(schemaTypeLabel).join(' & ')
1072
+ return 'any'
1073
+ }
1074
+
1075
+ function schemaHasDetails(schema: unknown): boolean {
1076
+ if (!schema || typeof schema !== 'object') return false
1077
+ const schemaObj = schema as Record<string, unknown>
1078
+ if (Array.isArray(schemaObj.enum) && schemaObj.enum.length) return true
1079
+ if (schemaObj.const !== undefined) return true
1080
+ if (typeof schemaObj.format === 'string') return true
1081
+ if (Array.isArray(schemaObj.oneOf) && schemaObj.oneOf.some((s: unknown) => schemaHasDetails(s))) return true
1082
+ if (Array.isArray(schemaObj.anyOf) && schemaObj.anyOf.some((s: unknown) => schemaHasDetails(s))) return true
1083
+ if (Array.isArray(schemaObj.allOf) && schemaObj.allOf.some((s: unknown) => schemaHasDetails(s))) return true
1084
+ if (schemaObj.items && schemaHasDetails(schemaObj.items)) return true
1085
+ if (schemaObj.properties && Object.keys(schemaObj.properties as Record<string, unknown>).length) return true
1086
+ if (Array.isArray(schemaObj.prefixItems) && schemaObj.prefixItems.some((s: unknown) => schemaHasDetails(s))) return true
1087
+ if (schemaObj.type && schemaObj.type !== 'object') return true
1088
+ return false
1089
+ }
1090
+
1091
+ type ContentSelection = {
1092
+ mediaType: string
1093
+ entry: any
1094
+ }
1095
+
1096
+ type DisplaySnippet = {
1097
+ value: string
1098
+ language: string
1099
+ }
1100
+
1101
+ function selectContentVariant(content?: Record<string, any>): ContentSelection | undefined {
1102
+ if (!content) return undefined
1103
+ const entries = Object.entries(content)
1104
+ if (!entries.length) return undefined
1105
+ const preferred = [
1106
+ 'application/json',
1107
+ 'application/x-www-form-urlencoded',
1108
+ 'multipart/form-data',
1109
+ 'text/plain',
1110
+ ]
1111
+ for (const mediaType of preferred) {
1112
+ const match = entries.find(([type]) => type === mediaType)
1113
+ if (match) {
1114
+ const [selectedType, entry] = match
1115
+ return { mediaType: selectedType, entry }
1116
+ }
1117
+ }
1118
+ const [mediaType, entry] = entries[0]
1119
+ return { mediaType, entry }
1120
+ }
1121
+
1122
+ function formatExampleForDisplay(example: unknown, mediaType?: string): DisplaySnippet | null {
1123
+ if (example === undefined) return null
1124
+ if (mediaType === 'application/x-www-form-urlencoded') {
1125
+ const encoded = toFormUrlEncoded(example)
1126
+ if (!encoded) return null
1127
+ return { value: encoded, language: 'text' }
1128
+ }
1129
+ if (mediaType === 'multipart/form-data') {
1130
+ if (example && typeof example === 'object') {
1131
+ const lines = Object.entries(example as Record<string, unknown>).map(([key, value]) => {
1132
+ const rendered = value === undefined || value === null ? '' : String(value)
1133
+ return `${key}=${rendered}`
1134
+ })
1135
+ if (lines.length) {
1136
+ return { value: lines.join('\n'), language: 'text' }
1137
+ }
1138
+ }
1139
+ if (typeof example === 'string') {
1140
+ return { value: example, language: 'text' }
1141
+ }
1142
+ }
1143
+ if (!mediaType || mediaType === 'application/json') {
1144
+ try {
1145
+ return { value: JSON.stringify(example, null, 2), language: 'json' }
1146
+ } catch {
1147
+ return null
1148
+ }
1149
+ }
1150
+ if (typeof example === 'string') {
1151
+ return { value: example, language: 'text' }
1152
+ }
1153
+ try {
1154
+ return { value: JSON.stringify(example, null, 2), language: 'json' }
1155
+ } catch {
1156
+ return { value: String(example), language: 'text' }
1157
+ }
1158
+ }
1159
+
1160
+ function formatSchemaForDisplay(schema: any): DisplaySnippet | null {
1161
+ if (!schema) return null
1162
+ try {
1163
+ return { value: JSON.stringify(schema, null, 2), language: 'json' }
1164
+ } catch {
1165
+ return null
1166
+ }
1167
+ }
1168
+
1169
+ export function generateMarkdownFromOpenApi(doc: OpenApiDocument): string {
1170
+ const lines: string[] = []
1171
+ lines.push(`# ${doc.info.title}`)
1172
+ lines.push('')
1173
+ lines.push(`Version: ${doc.info.version}`)
1174
+ if (doc.info.description) {
1175
+ lines.push('')
1176
+ lines.push(doc.info.description)
1177
+ }
1178
+ if (doc.servers && doc.servers.length) {
1179
+ lines.push('')
1180
+ lines.push('## Servers')
1181
+ for (const server of doc.servers) {
1182
+ lines.push(`- ${server.url}${server.description ? ` – ${server.description}` : ''}`)
1183
+ }
1184
+ }
1185
+
1186
+ const sortedPaths = Object.keys(doc.paths).sort()
1187
+ for (const path of sortedPaths) {
1188
+ const operations = doc.paths[path]
1189
+ const methods = Object.keys(operations).sort()
1190
+ for (const method of methods) {
1191
+ const op: any = operations[method]
1192
+ lines.push('')
1193
+ lines.push(`## ${method.toUpperCase()} \`${path}\``)
1194
+ if (op.summary) {
1195
+ lines.push('')
1196
+ lines.push(op.summary)
1197
+ }
1198
+ if (op.description) {
1199
+ lines.push('')
1200
+ lines.push(op.description)
1201
+ }
1202
+ if (op.tags && op.tags.length) {
1203
+ lines.push('')
1204
+ lines.push(`**Tags:** ${op.tags.join(', ')}`)
1205
+ }
1206
+ if (op['x-require-auth']) {
1207
+ lines.push('')
1208
+ lines.push(`**Requires authentication.**`)
1209
+ }
1210
+ if (op['x-require-features']) {
1211
+ lines.push('')
1212
+ lines.push(`**Features:** ${(op['x-require-features'] as string[]).join(', ')}`)
1213
+ }
1214
+ if (op['x-require-roles']) {
1215
+ lines.push('')
1216
+ lines.push(`**Roles:** ${(op['x-require-roles'] as string[]).join(', ')}`)
1217
+ }
1218
+
1219
+ const parameters = (op.parameters as any[]) ?? []
1220
+ if (parameters.length) {
1221
+ lines.push('')
1222
+ lines.push('### Parameters')
1223
+ const rows: Array<[string, string, string, string]> = parameters.map((p) => [
1224
+ p.name,
1225
+ p.in,
1226
+ schemaTypeLabel(p.schema),
1227
+ p.required ? 'Required' : 'Optional',
1228
+ ])
1229
+ lines.push(formatMarkdownTable(rows))
1230
+ }
1231
+
1232
+ if (op.requestBody) {
1233
+ const selection = selectContentVariant(op.requestBody.content)
1234
+ if (selection) {
1235
+ const { mediaType, entry } = selection
1236
+ const example = entry?.example ?? entry?.examples?.default?.value
1237
+ const formatted = formatExampleForDisplay(example, mediaType)
1238
+ const schemaFormatted =
1239
+ entry?.schema && schemaHasDetails(entry.schema) ? formatSchemaForDisplay(entry.schema) : null
1240
+ lines.push('')
1241
+ lines.push('### Request Body')
1242
+ lines.push('')
1243
+ lines.push(`Content-Type: \`${mediaType}\``)
1244
+ if (formatted) {
1245
+ lines.push('')
1246
+ lines.push(`\`\`\`${formatted.language}`)
1247
+ lines.push(formatted.value)
1248
+ lines.push('```')
1249
+ } else if (schemaFormatted) {
1250
+ lines.push('')
1251
+ lines.push(`\`\`\`${schemaFormatted.language}`)
1252
+ lines.push(schemaFormatted.value)
1253
+ lines.push('```')
1254
+ } else {
1255
+ lines.push('')
1256
+ lines.push('No example available for this content type.')
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ const responses = op.responses ?? {}
1262
+ const responseStatuses = Object.keys(responses).sort()
1263
+ if (responseStatuses.length) {
1264
+ lines.push('')
1265
+ lines.push('### Responses')
1266
+ for (const status of responseStatuses) {
1267
+ const response = responses[status]
1268
+ if (response?.['x-autoGenerated']) continue
1269
+ lines.push('')
1270
+ lines.push(`**${status}** – ${response.description || 'Response'}`)
1271
+ const selection = selectContentVariant(response.content)
1272
+ if (selection) {
1273
+ const { mediaType, entry } = selection
1274
+ const example = entry?.example ?? entry?.examples?.default?.value
1275
+ const formatted = formatExampleForDisplay(example, mediaType)
1276
+ const schemaFormatted =
1277
+ entry?.schema && schemaHasDetails(entry.schema) ? formatSchemaForDisplay(entry.schema) : null
1278
+ lines.push('')
1279
+ lines.push(`Content-Type: \`${mediaType}\``)
1280
+ if (formatted) {
1281
+ lines.push('')
1282
+ lines.push(`\`\`\`${formatted.language}`)
1283
+ lines.push(formatted.value)
1284
+ lines.push('```')
1285
+ } else if (schemaFormatted) {
1286
+ lines.push('')
1287
+ lines.push(`\`\`\`${schemaFormatted.language}`)
1288
+ lines.push(schemaFormatted.value)
1289
+ lines.push('```')
1290
+ }
1291
+ }
1292
+ }
1293
+ }
1294
+
1295
+ const samples = op['x-codeSamples'] as any[] | undefined
1296
+ if (samples && samples.length) {
1297
+ const curl = samples.find((sample) => String(sample.lang).toLowerCase() === 'curl') ?? samples[0]
1298
+ if (curl?.source) {
1299
+ lines.push('')
1300
+ lines.push('### Example')
1301
+ lines.push('')
1302
+ lines.push('```bash')
1303
+ lines.push(curl.source)
1304
+ lines.push('```')
1305
+ }
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ return lines.join('\n')
1311
+ }