vocs 2.0.17 → 2.1.2

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 (306) hide show
  1. package/dist/config.d.ts +1 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +1 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/globals.d.ts +16 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/config.d.ts +28 -0
  11. package/dist/internal/config.d.ts.map +1 -1
  12. package/dist/internal/config.js +10 -2
  13. package/dist/internal/config.js.map +1 -1
  14. package/dist/internal/llms.d.ts +21 -2
  15. package/dist/internal/llms.d.ts.map +1 -1
  16. package/dist/internal/llms.js +41 -1
  17. package/dist/internal/llms.js.map +1 -1
  18. package/dist/internal/markdown-negotiation.d.ts +36 -0
  19. package/dist/internal/markdown-negotiation.d.ts.map +1 -0
  20. package/dist/internal/markdown-negotiation.js +81 -0
  21. package/dist/internal/markdown-negotiation.js.map +1 -0
  22. package/dist/internal/markdown.d.ts.map +1 -1
  23. package/dist/internal/markdown.js +5 -0
  24. package/dist/internal/markdown.js.map +1 -1
  25. package/dist/internal/openapi/anchors.d.ts +25 -0
  26. package/dist/internal/openapi/anchors.d.ts.map +1 -0
  27. package/dist/internal/openapi/anchors.js +37 -0
  28. package/dist/internal/openapi/anchors.js.map +1 -0
  29. package/dist/internal/openapi/app.d.ts +89 -0
  30. package/dist/internal/openapi/app.d.ts.map +1 -0
  31. package/dist/internal/openapi/app.js +62 -0
  32. package/dist/internal/openapi/app.js.map +1 -0
  33. package/dist/internal/openapi/index.d.ts +8 -0
  34. package/dist/internal/openapi/index.d.ts.map +1 -0
  35. package/dist/internal/openapi/index.js +5 -0
  36. package/dist/internal/openapi/index.js.map +1 -0
  37. package/dist/internal/openapi/markdown.d.ts +42 -0
  38. package/dist/internal/openapi/markdown.d.ts.map +1 -0
  39. package/dist/internal/openapi/markdown.js +235 -0
  40. package/dist/internal/openapi/markdown.js.map +1 -0
  41. package/dist/internal/openapi/openapi.d.ts +187 -0
  42. package/dist/internal/openapi/openapi.d.ts.map +1 -0
  43. package/dist/internal/openapi/openapi.js +44 -0
  44. package/dist/internal/openapi/openapi.js.map +1 -0
  45. package/dist/internal/openapi/openrpc.d.ts +90 -0
  46. package/dist/internal/openapi/openrpc.d.ts.map +1 -0
  47. package/dist/internal/openapi/openrpc.js +213 -0
  48. package/dist/internal/openapi/openrpc.js.map +1 -0
  49. package/dist/internal/openapi/parser.d.ts +181 -0
  50. package/dist/internal/openapi/parser.d.ts.map +1 -0
  51. package/dist/internal/openapi/parser.js +329 -0
  52. package/dist/internal/openapi/parser.js.map +1 -0
  53. package/dist/internal/openapi/registry.d.ts +36 -0
  54. package/dist/internal/openapi/registry.d.ts.map +1 -0
  55. package/dist/internal/openapi/registry.js +79 -0
  56. package/dist/internal/openapi/registry.js.map +1 -0
  57. package/dist/internal/openapi/sample.d.ts +115 -0
  58. package/dist/internal/openapi/sample.d.ts.map +1 -0
  59. package/dist/internal/openapi/sample.js +434 -0
  60. package/dist/internal/openapi/sample.js.map +1 -0
  61. package/dist/internal/openapi/search.d.ts +19 -0
  62. package/dist/internal/openapi/search.d.ts.map +1 -0
  63. package/dist/internal/openapi/search.js +98 -0
  64. package/dist/internal/openapi/search.js.map +1 -0
  65. package/dist/internal/openapi/sidebar.d.ts +30 -0
  66. package/dist/internal/openapi/sidebar.d.ts.map +1 -0
  67. package/dist/internal/openapi/sidebar.js +67 -0
  68. package/dist/internal/openapi/sidebar.js.map +1 -0
  69. package/dist/internal/openapi/union.d.ts +36 -0
  70. package/dist/internal/openapi/union.d.ts.map +1 -0
  71. package/dist/internal/openapi/union.js +69 -0
  72. package/dist/internal/openapi/union.js.map +1 -0
  73. package/dist/internal/search.d.ts.map +1 -1
  74. package/dist/internal/search.js +20 -0
  75. package/dist/internal/search.js.map +1 -1
  76. package/dist/internal/vite-plugins.d.ts +12 -0
  77. package/dist/internal/vite-plugins.d.ts.map +1 -1
  78. package/dist/internal/vite-plugins.js +102 -11
  79. package/dist/internal/vite-plugins.js.map +1 -1
  80. package/dist/react/Badge.d.ts +2 -3
  81. package/dist/react/Badge.d.ts.map +1 -1
  82. package/dist/react/Layout.client.d.ts.map +1 -1
  83. package/dist/react/Layout.client.js +2 -2
  84. package/dist/react/Layout.client.js.map +1 -1
  85. package/dist/react/OpenApi.d.ts +6 -0
  86. package/dist/react/OpenApi.d.ts.map +1 -0
  87. package/dist/react/OpenApi.js +6 -0
  88. package/dist/react/OpenApi.js.map +1 -0
  89. package/dist/react/internal/CodeToHtml.client.d.ts +53 -2
  90. package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -1
  91. package/dist/react/internal/CodeToHtml.client.js +154 -21
  92. package/dist/react/internal/CodeToHtml.client.js.map +1 -1
  93. package/dist/react/internal/Sidebar.d.ts.map +1 -1
  94. package/dist/react/internal/Sidebar.js +99 -2
  95. package/dist/react/internal/Sidebar.js.map +1 -1
  96. package/dist/react/internal/openapi/CodeSample.client.d.ts +21 -0
  97. package/dist/react/internal/openapi/CodeSample.client.d.ts.map +1 -0
  98. package/dist/react/internal/openapi/CodeSample.client.js +134 -0
  99. package/dist/react/internal/openapi/CodeSample.client.js.map +1 -0
  100. package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts +17 -0
  101. package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts.map +1 -0
  102. package/dist/react/internal/openapi/CollapsibleChildren.client.js +18 -0
  103. package/dist/react/internal/openapi/CollapsibleChildren.client.js.map +1 -0
  104. package/dist/react/internal/openapi/Disclosure.client.d.ts +28 -0
  105. package/dist/react/internal/openapi/Disclosure.client.d.ts.map +1 -0
  106. package/dist/react/internal/openapi/Disclosure.client.js +38 -0
  107. package/dist/react/internal/openapi/Disclosure.client.js.map +1 -0
  108. package/dist/react/internal/openapi/Endpoints.d.ts +26 -0
  109. package/dist/react/internal/openapi/Endpoints.d.ts.map +1 -0
  110. package/dist/react/internal/openapi/Endpoints.js +33 -0
  111. package/dist/react/internal/openapi/Endpoints.js.map +1 -0
  112. package/dist/react/internal/openapi/EndpointsView.d.ts +24 -0
  113. package/dist/react/internal/openapi/EndpointsView.d.ts.map +1 -0
  114. package/dist/react/internal/openapi/EndpointsView.js +26 -0
  115. package/dist/react/internal/openapi/EndpointsView.js.map +1 -0
  116. package/dist/react/internal/openapi/EnumValues.client.d.ts +14 -0
  117. package/dist/react/internal/openapi/EnumValues.client.d.ts.map +1 -0
  118. package/dist/react/internal/openapi/EnumValues.client.js +20 -0
  119. package/dist/react/internal/openapi/EnumValues.client.js.map +1 -0
  120. package/dist/react/internal/openapi/HeadingAnchor.d.ts +15 -0
  121. package/dist/react/internal/openapi/HeadingAnchor.d.ts.map +1 -0
  122. package/dist/react/internal/openapi/HeadingAnchor.js +12 -0
  123. package/dist/react/internal/openapi/HeadingAnchor.js.map +1 -0
  124. package/dist/react/internal/openapi/OpenApiPage.d.ts +79 -0
  125. package/dist/react/internal/openapi/OpenApiPage.d.ts.map +1 -0
  126. package/dist/react/internal/openapi/OpenApiPage.js +72 -0
  127. package/dist/react/internal/openapi/OpenApiPage.js.map +1 -0
  128. package/dist/react/internal/openapi/Operation.d.ts +25 -0
  129. package/dist/react/internal/openapi/Operation.d.ts.map +1 -0
  130. package/dist/react/internal/openapi/Operation.js +101 -0
  131. package/dist/react/internal/openapi/Operation.js.map +1 -0
  132. package/dist/react/internal/openapi/Playground.client.d.ts +33 -0
  133. package/dist/react/internal/openapi/Playground.client.d.ts.map +1 -0
  134. package/dist/react/internal/openapi/Playground.client.js +170 -0
  135. package/dist/react/internal/openapi/Playground.client.js.map +1 -0
  136. package/dist/react/internal/openapi/PropertyExample.client.d.ts +17 -0
  137. package/dist/react/internal/openapi/PropertyExample.client.d.ts.map +1 -0
  138. package/dist/react/internal/openapi/PropertyExample.client.js +21 -0
  139. package/dist/react/internal/openapi/PropertyExample.client.js.map +1 -0
  140. package/dist/react/internal/openapi/Reference.d.ts +55 -0
  141. package/dist/react/internal/openapi/Reference.d.ts.map +1 -0
  142. package/dist/react/internal/openapi/Reference.js +42 -0
  143. package/dist/react/internal/openapi/Reference.js.map +1 -0
  144. package/dist/react/internal/openapi/Schema.d.ts +110 -0
  145. package/dist/react/internal/openapi/Schema.d.ts.map +1 -0
  146. package/dist/react/internal/openapi/Schema.js +239 -0
  147. package/dist/react/internal/openapi/Schema.js.map +1 -0
  148. package/dist/react/internal/openapi/SchemaUnion.client.d.ts +25 -0
  149. package/dist/react/internal/openapi/SchemaUnion.client.d.ts.map +1 -0
  150. package/dist/react/internal/openapi/SchemaUnion.client.js +48 -0
  151. package/dist/react/internal/openapi/SchemaUnion.client.js.map +1 -0
  152. package/dist/react/internal/openapi/anchor-navigation.client.d.ts +47 -0
  153. package/dist/react/internal/openapi/anchor-navigation.client.d.ts.map +1 -0
  154. package/dist/react/internal/openapi/anchor-navigation.client.js +120 -0
  155. package/dist/react/internal/openapi/anchor-navigation.client.js.map +1 -0
  156. package/dist/react/internal/openapi/auth.d.ts +28 -0
  157. package/dist/react/internal/openapi/auth.d.ts.map +1 -0
  158. package/dist/react/internal/openapi/auth.js +75 -0
  159. package/dist/react/internal/openapi/auth.js.map +1 -0
  160. package/dist/react/useLayout.d.ts +2 -0
  161. package/dist/react/useLayout.d.ts.map +1 -1
  162. package/dist/react/useLayout.js +2 -0
  163. package/dist/react/useLayout.js.map +1 -1
  164. package/dist/server/handlers.d.ts +1 -1
  165. package/dist/server/handlers.d.ts.map +1 -1
  166. package/dist/server/handlers.js +26 -5
  167. package/dist/server/handlers.js.map +1 -1
  168. package/dist/server/og-assets.d.ts +5 -0
  169. package/dist/server/og-assets.d.ts.map +1 -0
  170. package/dist/server/og-assets.js +11 -0
  171. package/dist/server/og-assets.js.map +1 -0
  172. package/dist/server/openapi/assets.d.ts +33 -0
  173. package/dist/server/openapi/assets.d.ts.map +1 -0
  174. package/dist/server/openapi/assets.generated.d.ts +9 -0
  175. package/dist/server/openapi/assets.generated.d.ts.map +1 -0
  176. package/dist/server/openapi/assets.generated.js +1091 -0
  177. package/dist/server/openapi/assets.generated.js.map +1 -0
  178. package/dist/server/openapi/assets.js +32 -0
  179. package/dist/server/openapi/assets.js.map +1 -0
  180. package/dist/server/openapi/handler.d.ts +103 -0
  181. package/dist/server/openapi/handler.d.ts.map +1 -0
  182. package/dist/server/openapi/handler.js +198 -0
  183. package/dist/server/openapi/handler.js.map +1 -0
  184. package/dist/server/openapi/handler.test.d.ts +2 -0
  185. package/dist/server/openapi/handler.test.d.ts.map +1 -0
  186. package/dist/server/openapi/handler.test.js +203 -0
  187. package/dist/server/openapi/handler.test.js.map +1 -0
  188. package/dist/server/openapi/html.d.ts +16 -0
  189. package/dist/server/openapi/html.d.ts.map +1 -0
  190. package/dist/server/openapi/html.js +75 -0
  191. package/dist/server/openapi/html.js.map +1 -0
  192. package/dist/server/openapi/pages.d.ts +33 -0
  193. package/dist/server/openapi/pages.d.ts.map +1 -0
  194. package/dist/server/openapi/pages.js +130 -0
  195. package/dist/server/openapi/pages.js.map +1 -0
  196. package/dist/server/openapi/pages.test.d.ts +2 -0
  197. package/dist/server/openapi/pages.test.d.ts.map +1 -0
  198. package/dist/server/openapi/pages.test.js +94 -0
  199. package/dist/server/openapi/pages.test.js.map +1 -0
  200. package/dist/server/openapi/state.d.ts +42 -0
  201. package/dist/server/openapi/state.d.ts.map +1 -0
  202. package/dist/server/openapi/state.js +101 -0
  203. package/dist/server/openapi/state.js.map +1 -0
  204. package/dist/styles/index.css +16 -0
  205. package/dist/styles/markdown.css +9 -7
  206. package/dist/styles/openapi-playground.css +80 -0
  207. package/dist/styles/openapi.css +660 -0
  208. package/dist/vite.d.ts.map +1 -1
  209. package/dist/vite.js +1 -0
  210. package/dist/vite.js.map +1 -1
  211. package/dist/waku/internal/middleware/md-router.d.ts +0 -4
  212. package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
  213. package/dist/waku/internal/middleware/md-router.js +3 -48
  214. package/dist/waku/internal/middleware/md-router.js.map +1 -1
  215. package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js +1 -1
  216. package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js.map +1 -1
  217. package/dist/waku/internal/patches/router.d.ts.map +1 -1
  218. package/dist/waku/internal/patches/router.js +114 -1
  219. package/dist/waku/internal/patches/router.js.map +1 -1
  220. package/package.json +5 -1
  221. package/src/config.ts +1 -0
  222. package/src/globals.d.ts +16 -0
  223. package/src/index.ts +1 -0
  224. package/src/internal/config.ts +40 -1
  225. package/src/internal/llms.ts +51 -1
  226. package/src/internal/markdown-negotiation.test.ts +42 -0
  227. package/src/internal/markdown-negotiation.ts +95 -0
  228. package/src/internal/markdown.ts +5 -0
  229. package/src/internal/openapi/anchors.ts +44 -0
  230. package/src/internal/openapi/app.ts +127 -0
  231. package/src/internal/openapi/index.ts +24 -0
  232. package/src/internal/openapi/markdown.test.ts +115 -0
  233. package/src/internal/openapi/markdown.ts +275 -0
  234. package/src/internal/openapi/openapi.ts +212 -0
  235. package/src/internal/openapi/openrpc.test.ts +239 -0
  236. package/src/internal/openapi/openrpc.ts +295 -0
  237. package/src/internal/openapi/parser.test.ts +203 -0
  238. package/src/internal/openapi/parser.ts +613 -0
  239. package/src/internal/openapi/registry.test.ts +89 -0
  240. package/src/internal/openapi/registry.ts +89 -0
  241. package/src/internal/openapi/sample.test.ts +283 -0
  242. package/src/internal/openapi/sample.ts +562 -0
  243. package/src/internal/openapi/search.test.ts +62 -0
  244. package/src/internal/openapi/search.ts +108 -0
  245. package/src/internal/openapi/sidebar.test.ts +131 -0
  246. package/src/internal/openapi/sidebar.ts +94 -0
  247. package/src/internal/openapi/union.test.ts +51 -0
  248. package/src/internal/openapi/union.ts +74 -0
  249. package/src/internal/search.ts +20 -0
  250. package/src/internal/test/virtual-config.stub.ts +14 -0
  251. package/src/internal/vite-plugins.ts +106 -11
  252. package/src/openapi-app/App.tsx +64 -0
  253. package/src/openapi-app/blocks.tsx +33 -0
  254. package/src/openapi-app/client.tsx +25 -0
  255. package/src/openapi-app/links.test.ts +84 -0
  256. package/src/openapi-app/links.ts +66 -0
  257. package/src/openapi-app/payload.ts +20 -0
  258. package/src/openapi-app/virtual/config.ts +7 -0
  259. package/src/openapi-app/virtual/group-icons.ts +2 -0
  260. package/src/openapi-app/virtual/langs.ts +6 -0
  261. package/src/openapi-app/virtual/openapi.ts +10 -0
  262. package/src/openapi-app/virtual/search-index.ts +21 -0
  263. package/src/openapi-app/virtual/slots.ts +4 -0
  264. package/src/openapi-app/virtual/user-styles.ts +2 -0
  265. package/src/openapi-app/waku.tsx +154 -0
  266. package/src/react/Badge.tsx +2 -3
  267. package/src/react/Layout.client.tsx +17 -4
  268. package/src/react/OpenApi.tsx +5 -0
  269. package/src/react/internal/CodeToHtml.client.tsx +283 -22
  270. package/src/react/internal/Sidebar.tsx +126 -22
  271. package/src/react/internal/openapi/CodeSample.client.tsx +294 -0
  272. package/src/react/internal/openapi/CollapsibleChildren.client.tsx +41 -0
  273. package/src/react/internal/openapi/Disclosure.client.tsx +67 -0
  274. package/src/react/internal/openapi/Endpoints.tsx +58 -0
  275. package/src/react/internal/openapi/EndpointsView.tsx +76 -0
  276. package/src/react/internal/openapi/EnumValues.client.tsx +49 -0
  277. package/src/react/internal/openapi/HeadingAnchor.tsx +28 -0
  278. package/src/react/internal/openapi/OpenApiPage.tsx +173 -0
  279. package/src/react/internal/openapi/Operation.test.tsx +101 -0
  280. package/src/react/internal/openapi/Operation.tsx +335 -0
  281. package/src/react/internal/openapi/Playground.client.tsx +234 -0
  282. package/src/react/internal/openapi/PropertyExample.client.tsx +55 -0
  283. package/src/react/internal/openapi/Reference.tsx +120 -0
  284. package/src/react/internal/openapi/Schema.tsx +467 -0
  285. package/src/react/internal/openapi/SchemaUnion.client.tsx +123 -0
  286. package/src/react/internal/openapi/anchor-navigation.client.ts +154 -0
  287. package/src/react/internal/openapi/auth.ts +69 -0
  288. package/src/react/useLayout.ts +4 -0
  289. package/src/server/handlers.ts +31 -6
  290. package/src/server/og-assets.ts +14 -0
  291. package/src/server/openapi/assets.generated.ts +1093 -0
  292. package/src/server/openapi/assets.ts +57 -0
  293. package/src/server/openapi/handler.test.ts +244 -0
  294. package/src/server/openapi/handler.ts +277 -0
  295. package/src/server/openapi/html.ts +84 -0
  296. package/src/server/openapi/pages.test.ts +111 -0
  297. package/src/server/openapi/pages.ts +153 -0
  298. package/src/server/openapi/state.ts +136 -0
  299. package/src/styles/index.css +16 -0
  300. package/src/styles/markdown.css +9 -7
  301. package/src/styles/openapi-playground.css +80 -0
  302. package/src/styles/openapi.css +660 -0
  303. package/src/vite.ts +1 -0
  304. package/src/waku/internal/middleware/md-router.ts +8 -52
  305. package/src/waku/internal/patches/adapters/vercel-build-enhancer.ts +1 -1
  306. package/src/waku/internal/patches/router.ts +131 -1
@@ -0,0 +1,239 @@
1
+ import { afterEach, describe, expect, test, vi } from 'vitest'
2
+ import * as OpenApi from './openapi.js'
3
+ import { expand } from './openrpc.js'
4
+ import { parse } from './parser.js'
5
+ import { codeSamples } from './sample.js'
6
+
7
+ const openrpc = {
8
+ openrpc: '1.2.4',
9
+ info: { title: 'Demo JSON-RPC', version: '1.0.0' },
10
+ components: {
11
+ schemas: {
12
+ address: { type: 'string', title: 'hex address' },
13
+ uint: { type: 'string', title: 'hex uint' },
14
+ },
15
+ },
16
+ methods: [
17
+ {
18
+ name: 'eth_blockNumber',
19
+ summary: 'Returns the current block number.',
20
+ params: [],
21
+ result: { name: 'Block number', schema: { $ref: '#/components/schemas/uint' } },
22
+ examples: [{ name: 'ex', params: [], result: { name: 'r', value: '0x10' } }],
23
+ },
24
+ {
25
+ name: 'eth_getBalance',
26
+ summary: 'Returns the balance of an account.',
27
+ params: [
28
+ { name: 'Address', required: true, schema: { $ref: '#/components/schemas/address' } },
29
+ { name: 'Block', required: true, schema: { $ref: '#/components/schemas/uint' } },
30
+ ],
31
+ result: { name: 'Balance', schema: { $ref: '#/components/schemas/uint' } },
32
+ examples: [
33
+ {
34
+ name: 'ex',
35
+ params: [
36
+ { name: 'Address', value: '0xabc' },
37
+ { name: 'Block', value: 'latest' },
38
+ ],
39
+ result: { name: 'Balance', value: '0x1' },
40
+ },
41
+ ],
42
+ },
43
+ ],
44
+ }
45
+
46
+ const host = {
47
+ id: 'rpc-request',
48
+ method: 'POST',
49
+ path: '/v1/rpc',
50
+ summary: 'Query RPC',
51
+ parameters: [],
52
+ responses: [],
53
+ }
54
+
55
+ describe('expand', () => {
56
+ test('expands one operation per JSON-RPC method', async () => {
57
+ const { operations, examples } = await expand(host, openrpc)
58
+
59
+ expect(operations.map((operation) => operation.summary)).toEqual([
60
+ 'eth_blockNumber',
61
+ 'eth_getBalance',
62
+ ])
63
+
64
+ // Each operation tags the host request example to preselect when trying it,
65
+ // and a matching named example is emitted for injection onto the host.
66
+ expect(operations.map((operation) => operation.rpcExample)).toEqual([
67
+ 'eth_blockNumber',
68
+ 'eth_getBalance',
69
+ ])
70
+ expect(Object.keys(examples)).toEqual(['eth_blockNumber', 'eth_getBalance'])
71
+ expect(examples['eth_getBalance']?.value).toEqual({
72
+ jsonrpc: '2.0',
73
+ id: 1,
74
+ method: 'eth_getBalance',
75
+ params: ['0xabc', 'latest'],
76
+ })
77
+
78
+ const balance = operations[1]
79
+ if (!balance) throw new Error('expected eth_getBalance operation')
80
+ // All methods reuse the host transport (POST /v1/rpc).
81
+ expect(balance.method).toBe('POST')
82
+ expect(balance.path).toBe('/v1/rpc')
83
+ expect(balance.id).toBe('eth_getbalance')
84
+ expect(balance.description).toContain('balance')
85
+
86
+ // Params are surfaced like path/query params, with refs dereferenced.
87
+ expect(balance.parameters.map((p) => [p.name, p.in, p.example])).toEqual([
88
+ ['Address', 'rpc', '0xabc'],
89
+ ['Block', 'rpc', 'latest'],
90
+ ])
91
+ expect(balance.parameters[0]?.schema).toMatchObject({ type: 'string' })
92
+
93
+ // The request body is the JSON-RPC envelope, hidden from its own section.
94
+ expect(balance.requestBody?.hidden).toBe(true)
95
+ expect(balance.requestBody?.content[0]?.example).toEqual({
96
+ jsonrpc: '2.0',
97
+ id: 1,
98
+ method: 'eth_getBalance',
99
+ params: ['0xabc', 'latest'],
100
+ })
101
+
102
+ // The result is wrapped in a JSON-RPC response envelope.
103
+ expect(balance.responses[0]?.status).toBe('200')
104
+ expect(balance.responses[0]?.content[0]?.example).toEqual({
105
+ jsonrpc: '2.0',
106
+ id: 1,
107
+ result: '0x1',
108
+ })
109
+ })
110
+
111
+ test('generates an accurate JSON-RPC request code sample', async () => {
112
+ const { operations } = await expand(host, openrpc)
113
+ const balance = operations[1]
114
+ if (!balance) throw new Error('expected eth_getBalance operation')
115
+ const samples = codeSamples(balance, 'https://api.example.com')
116
+ const curl = samples.find((sample) => sample.id === 'shell/curl')
117
+ expect(curl?.code).toContain('https://api.example.com/v1/rpc')
118
+ expect(curl?.code).toContain('"method": "eth_getBalance"')
119
+ expect(curl?.code).toContain('"params"')
120
+ })
121
+ })
122
+
123
+ describe('parse with x-openrpc', () => {
124
+ afterEach(() => {
125
+ vi.restoreAllMocks()
126
+ })
127
+
128
+ const spec = {
129
+ openapi: '3.1.0',
130
+ info: { title: 'API', version: '1.0.0' },
131
+ tags: [{ name: 'RPC', description: 'JSON-RPC passthrough.' }],
132
+ paths: {
133
+ '/v1/rpc': {
134
+ post: {
135
+ operationId: 'rpcRequest',
136
+ summary: 'Query RPC',
137
+ tags: ['RPC'],
138
+ 'x-openrpc': openrpc,
139
+ responses: { '200': { description: 'ok' } },
140
+ },
141
+ },
142
+ },
143
+ }
144
+
145
+ test('replaces the host operation with expanded methods', async () => {
146
+ const ir = await parse(OpenApi.from({ spec, path: '/api' }))
147
+ const group = ir.groups.find((candidate) => candidate.name === 'RPC')
148
+ expect(group?.operations.map((operation) => operation.summary)).toEqual([
149
+ 'eth_blockNumber',
150
+ 'eth_getBalance',
151
+ ])
152
+ })
153
+
154
+ test('resolves a relative x-openrpc URL against baseUrl', async () => {
155
+ const relativeSpec = {
156
+ ...spec,
157
+ paths: {
158
+ '/v1/rpc': {
159
+ post: {
160
+ operationId: 'rpcRequest',
161
+ summary: 'Query RPC',
162
+ tags: ['RPC'],
163
+ 'x-openrpc': '/openrpc.json',
164
+ responses: { '200': { description: 'ok' } },
165
+ },
166
+ },
167
+ },
168
+ }
169
+
170
+ const fetchMock = vi
171
+ .spyOn(globalThis, 'fetch')
172
+ .mockResolvedValue(
173
+ new Response(JSON.stringify(openrpc), { headers: { 'content-type': 'application/json' } }),
174
+ )
175
+
176
+ const ir = await parse(OpenApi.from({ spec: relativeSpec, path: '/api' }), {
177
+ baseUrl: 'https://cadent.tempo.xyz',
178
+ })
179
+
180
+ expect(fetchMock).toHaveBeenCalledWith('https://cadent.tempo.xyz/openrpc.json')
181
+ const group = ir.groups.find((candidate) => candidate.name === 'RPC')
182
+ expect(group?.operations.map((operation) => operation.summary)).toEqual([
183
+ 'eth_blockNumber',
184
+ 'eth_getBalance',
185
+ ])
186
+ })
187
+
188
+ test('falls back to the host operation when a relative x-openrpc has no base', async () => {
189
+ const relativeSpec = {
190
+ ...spec,
191
+ paths: {
192
+ '/v1/rpc': {
193
+ post: {
194
+ operationId: 'rpcRequest',
195
+ summary: 'Query RPC',
196
+ tags: ['RPC'],
197
+ 'x-openrpc': '/openrpc.json',
198
+ responses: { '200': { description: 'ok' } },
199
+ },
200
+ },
201
+ },
202
+ }
203
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
204
+
205
+ const ir = await parse(OpenApi.from({ spec: relativeSpec, path: '/api' }))
206
+
207
+ const group = ir.groups.find((candidate) => candidate.name === 'RPC')
208
+ expect(group?.operations.map((operation) => operation.summary)).toEqual(['Query RPC'])
209
+ expect(warn).toHaveBeenCalled()
210
+ })
211
+
212
+ test('injects per-method request examples into the client document', async () => {
213
+ const ir = await parse(OpenApi.from({ spec, path: '/api' }))
214
+ // The interactive client must receive the augmented document (not a bare
215
+ // URL) so it can select each method's example.
216
+ if (!('content' in ir.client)) throw new Error('expected inline client content')
217
+ const content = ir.client.content as {
218
+ paths: Record<
219
+ string,
220
+ {
221
+ post: {
222
+ requestBody: {
223
+ content: Record<string, { examples: Record<string, { value: unknown }> }>
224
+ }
225
+ }
226
+ }
227
+ >
228
+ }
229
+ const media = content.paths['/v1/rpc']?.post.requestBody.content['application/json']
230
+ const examples = media?.examples ?? {}
231
+ expect(Object.keys(examples)).toEqual(['eth_blockNumber', 'eth_getBalance'])
232
+ expect(examples['eth_getBalance']?.value).toEqual({
233
+ jsonrpc: '2.0',
234
+ id: 1,
235
+ method: 'eth_getBalance',
236
+ params: ['0xabc', 'latest'],
237
+ })
238
+ })
239
+ })
@@ -0,0 +1,295 @@
1
+ import { dereference } from '@scalar/openapi-parser'
2
+ import GithubSlugger from 'github-slugger'
3
+ import type { IrOperation, IrParameter } from './parser.js'
4
+
5
+ /**
6
+ * Minimal structural typing for the OpenRPC documents we expand. Only the
7
+ * fields consumed during expansion are modeled; everything else is ignored.
8
+ *
9
+ * @see https://spec.open-rpc.org
10
+ */
11
+ export type Document = {
12
+ openrpc?: string
13
+ info?: { title?: string; version?: string; description?: string }
14
+ methods?: Method[]
15
+ components?: { schemas?: Record<string, unknown> }
16
+ }
17
+
18
+ export type Method = {
19
+ name: string
20
+ summary?: string
21
+ description?: string
22
+ deprecated?: boolean
23
+ params?: ContentDescriptor[]
24
+ result?: ContentDescriptor
25
+ examples?: Example[]
26
+ }
27
+
28
+ type ContentDescriptor = {
29
+ name?: string
30
+ summary?: string
31
+ description?: string
32
+ required?: boolean
33
+ deprecated?: boolean
34
+ schema?: Record<string, unknown>
35
+ }
36
+
37
+ type Example = {
38
+ name?: string
39
+ params?: { name?: string; value?: unknown }[]
40
+ result?: { name?: string; value?: unknown }
41
+ }
42
+
43
+ /**
44
+ * Expands an OpenRPC document into one {@link IrOperation} per JSON-RPC method,
45
+ * modeled after a single host HTTP operation (the JSON-RPC endpoint).
46
+ *
47
+ * JSON-RPC methods all POST the same envelope to one endpoint, which OpenAPI
48
+ * cannot express as distinct path items. Expanding at the IR level instead lets
49
+ * each method become its own sidebar entry and page section while keeping the
50
+ * shared transport: every generated operation reuses the host operation's
51
+ * `method` (POST) and `path`, so request code samples target the real endpoint.
52
+ *
53
+ * Each method maps to:
54
+ * - `summary` → the method name (the sidebar/section title)
55
+ * - `description` → the method summary + description
56
+ * - `parameters` (`in: 'rpc'`) → the JSON-RPC params, rendered like path/query
57
+ * params
58
+ * - `requestBody` → the JSON-RPC request envelope, `hidden` from the body
59
+ * section but used to generate an accurate request code sample
60
+ * - a `200` response → the JSON-RPC response envelope wrapping the method result
61
+ */
62
+ export async function expand(
63
+ host: IrOperation,
64
+ document: Document | string,
65
+ options: expand.Options = {},
66
+ ): Promise<expand.Result> {
67
+ const resolved = await resolve(document, options)
68
+ const methods = resolved.methods ?? []
69
+ const slugger = new GithubSlugger()
70
+
71
+ // Named request examples (keyed by method name) injected onto the host
72
+ // operation so the interactive client (Scalar) can prefill the JSON-RPC
73
+ // envelope for each method. JSON-RPC methods all share one path+verb, so the
74
+ // client distinguishes them by selecting the matching named example.
75
+ const examples: Record<string, expand.RpcExample> = {}
76
+
77
+ const operations = methods.map((method) => {
78
+ const example = method.examples?.[0]
79
+
80
+ const parameters: IrParameter[] = (method.params ?? []).map((param, index) => ({
81
+ name: param.name ?? `param${index}`,
82
+ in: 'rpc',
83
+ required: param.required,
84
+ deprecated: param.deprecated,
85
+ description: param.description ?? param.summary,
86
+ schema: param.schema,
87
+ example: paramExample(param, index, example),
88
+ }))
89
+
90
+ // The wire-format example values (positional `params` array) used for the
91
+ // request code sample and to fill in any params the examples omit.
92
+ const paramValues = parameters.map((parameter) =>
93
+ parameter.example !== undefined ? parameter.example : sampleFromSchema(parameter.schema),
94
+ )
95
+
96
+ const requestExample = {
97
+ jsonrpc: '2.0',
98
+ id: 1,
99
+ method: method.name,
100
+ params: paramValues,
101
+ }
102
+ examples[method.name] = { summary: method.summary, value: requestExample }
103
+
104
+ const resultSchema = method.result?.schema
105
+ const resultValue =
106
+ example?.result?.value !== undefined ? example.result.value : sampleFromSchema(resultSchema)
107
+ const responseExample = { jsonrpc: '2.0', id: 1, result: resultValue }
108
+
109
+ const description = [method.summary, method.description].filter(Boolean).join('\n\n')
110
+
111
+ return {
112
+ id: slugger.slug(method.name),
113
+ method: host.method,
114
+ path: host.path,
115
+ // Name of the host operation's request example to select in the
116
+ // interactive client (matches the key added to `examples` above).
117
+ rpcExample: method.name,
118
+ summary: method.name,
119
+ description: description || undefined,
120
+ deprecated: method.deprecated,
121
+ parameters,
122
+ requestBody: {
123
+ required: true,
124
+ // Hidden from the rendered "Request Body" section: the params already
125
+ // appear in the "Parameters" section and the literal envelope shows in
126
+ // the request code sample. The body is still used to build that sample.
127
+ hidden: true,
128
+ content: [
129
+ {
130
+ mediaType: 'application/json',
131
+ schema: requestEnvelopeSchema(method),
132
+ example: requestExample,
133
+ },
134
+ ],
135
+ },
136
+ responses: [
137
+ {
138
+ status: '200',
139
+ description: method.result?.description ?? 'JSON-RPC response.',
140
+ content: [
141
+ {
142
+ mediaType: 'application/json',
143
+ schema: responseEnvelopeSchema(resultSchema),
144
+ example: responseExample,
145
+ },
146
+ ],
147
+ headers: [],
148
+ },
149
+ ],
150
+ } satisfies IrOperation
151
+ })
152
+
153
+ return { operations, examples }
154
+ }
155
+
156
+ export declare namespace expand {
157
+ type Options = {
158
+ /**
159
+ * Base URL relative `x-openrpc` URLs (e.g. `/openrpc.json`) resolve against:
160
+ * the spec URL when the spec was loaded from a URL, or the request origin in
161
+ * the standalone server handler.
162
+ */
163
+ baseUrl?: string | undefined
164
+ }
165
+ /** A named JSON-RPC request example to inject onto the host operation. */
166
+ type RpcExample = { summary?: string | undefined; value: unknown }
167
+ type Result = {
168
+ /** One IR operation per JSON-RPC method. */
169
+ operations: IrOperation[]
170
+ /** Request examples keyed by method name, for the host operation. */
171
+ examples: Record<string, RpcExample>
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Resolves an OpenRPC source (URL, raw JSON, or object) to a dereferenced doc.
177
+ *
178
+ * A string is treated as raw JSON when it begins with `{`; otherwise it's a
179
+ * URL. Absolute (`http(s)://`) URLs are fetched directly. Relative URLs (e.g.
180
+ * `/openrpc.json`, the host-relative link an OpenAPI document commonly uses) are
181
+ * resolved against `baseUrl` — the spec URL when the spec was loaded from a URL,
182
+ * or the request origin in the standalone server handler — then fetched.
183
+ */
184
+ async function resolve(
185
+ document: Document | string,
186
+ options: expand.Options = {},
187
+ ): Promise<Document> {
188
+ let raw: unknown = document
189
+ if (typeof document === 'string') {
190
+ const trimmed = document.trimStart()
191
+ if (trimmed.startsWith('{')) {
192
+ raw = JSON.parse(trimmed)
193
+ } else {
194
+ const url =
195
+ trimmed.startsWith('http://') || trimmed.startsWith('https://')
196
+ ? trimmed
197
+ : options.baseUrl
198
+ ? new URL(trimmed, options.baseUrl).toString()
199
+ : undefined
200
+ if (!url)
201
+ throw new Error(`Cannot resolve relative OpenRPC document "${trimmed}" without a base URL.`)
202
+ const response = await fetch(url)
203
+ if (!response.ok)
204
+ throw new Error(`Failed to fetch OpenRPC document from ${url}: ${response.statusText}`)
205
+ raw = await response.json()
206
+ }
207
+ }
208
+ // Dereference internal `$ref`s (`#/components/schemas/...`) so the renderer
209
+ // sees concrete schemas, matching how the OpenAPI parser dereferences specs.
210
+ const { schema } = await dereference(raw as never)
211
+ return (schema ?? raw) as Document
212
+ }
213
+
214
+ /** JSON-RPC request envelope schema for a method (used only for code samples). */
215
+ function requestEnvelopeSchema(method: Method): Record<string, unknown> {
216
+ return {
217
+ type: 'object',
218
+ properties: {
219
+ jsonrpc: { type: 'string', const: '2.0' },
220
+ id: { type: 'integer' },
221
+ method: { type: 'string', const: method.name },
222
+ params: {
223
+ type: 'array',
224
+ prefixItems: (method.params ?? []).map((param) => param.schema ?? {}),
225
+ },
226
+ },
227
+ required: ['jsonrpc', 'method'],
228
+ }
229
+ }
230
+
231
+ /** JSON-RPC response envelope schema wrapping a method's result schema. */
232
+ function responseEnvelopeSchema(
233
+ resultSchema: Record<string, unknown> | undefined,
234
+ ): Record<string, unknown> {
235
+ return {
236
+ type: 'object',
237
+ properties: {
238
+ jsonrpc: { type: 'string', const: '2.0' },
239
+ id: { type: 'integer' },
240
+ ...(resultSchema ? { result: resultSchema } : {}),
241
+ },
242
+ }
243
+ }
244
+
245
+ /** Looks up the example value for a param, by name then position. */
246
+ function paramExample(
247
+ param: ContentDescriptor,
248
+ index: number,
249
+ example: Example | undefined,
250
+ ): unknown {
251
+ const entries = example?.params ?? []
252
+ const byName = param.name ? entries.find((entry) => entry.name === param.name) : undefined
253
+ const entry = byName ?? entries[index]
254
+ return entry?.value
255
+ }
256
+
257
+ /**
258
+ * Derives a representative value from a JSON Schema for methods/params that lack
259
+ * authored examples. Intentionally small — prefers authored `example`/`default`/
260
+ * `enum` values and otherwise falls back to a type-based placeholder.
261
+ */
262
+ function sampleFromSchema(schema: Record<string, unknown> | undefined): unknown {
263
+ if (!schema || typeof schema !== 'object') return undefined
264
+ if ('example' in schema) return schema['example']
265
+ if (Array.isArray(schema['examples']) && schema['examples'].length > 0)
266
+ return schema['examples'][0]
267
+ if ('default' in schema) return schema['default']
268
+ if ('const' in schema) return schema['const']
269
+ if (Array.isArray(schema['enum']) && schema['enum'].length > 0) return schema['enum'][0]
270
+
271
+ const type = Array.isArray(schema['type']) ? schema['type'][0] : schema['type']
272
+ switch (type) {
273
+ case 'string':
274
+ return 'string'
275
+ case 'integer':
276
+ case 'number':
277
+ return 0
278
+ case 'boolean':
279
+ return true
280
+ case 'array': {
281
+ const items = schema['items'] as Record<string, unknown> | undefined
282
+ const item = sampleFromSchema(items)
283
+ return item === undefined ? [] : [item]
284
+ }
285
+ case 'object': {
286
+ const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined
287
+ if (!properties) return {}
288
+ const result: Record<string, unknown> = {}
289
+ for (const [key, value] of Object.entries(properties)) result[key] = sampleFromSchema(value)
290
+ return result
291
+ }
292
+ default:
293
+ return undefined
294
+ }
295
+ }