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.
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/globals.d.ts +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/config.d.ts +28 -0
- package/dist/internal/config.d.ts.map +1 -1
- package/dist/internal/config.js +10 -2
- package/dist/internal/config.js.map +1 -1
- package/dist/internal/llms.d.ts +21 -2
- package/dist/internal/llms.d.ts.map +1 -1
- package/dist/internal/llms.js +41 -1
- package/dist/internal/llms.js.map +1 -1
- package/dist/internal/markdown-negotiation.d.ts +36 -0
- package/dist/internal/markdown-negotiation.d.ts.map +1 -0
- package/dist/internal/markdown-negotiation.js +81 -0
- package/dist/internal/markdown-negotiation.js.map +1 -0
- package/dist/internal/markdown.d.ts.map +1 -1
- package/dist/internal/markdown.js +5 -0
- package/dist/internal/markdown.js.map +1 -1
- package/dist/internal/openapi/anchors.d.ts +25 -0
- package/dist/internal/openapi/anchors.d.ts.map +1 -0
- package/dist/internal/openapi/anchors.js +37 -0
- package/dist/internal/openapi/anchors.js.map +1 -0
- package/dist/internal/openapi/app.d.ts +89 -0
- package/dist/internal/openapi/app.d.ts.map +1 -0
- package/dist/internal/openapi/app.js +62 -0
- package/dist/internal/openapi/app.js.map +1 -0
- package/dist/internal/openapi/index.d.ts +8 -0
- package/dist/internal/openapi/index.d.ts.map +1 -0
- package/dist/internal/openapi/index.js +5 -0
- package/dist/internal/openapi/index.js.map +1 -0
- package/dist/internal/openapi/markdown.d.ts +42 -0
- package/dist/internal/openapi/markdown.d.ts.map +1 -0
- package/dist/internal/openapi/markdown.js +235 -0
- package/dist/internal/openapi/markdown.js.map +1 -0
- package/dist/internal/openapi/openapi.d.ts +187 -0
- package/dist/internal/openapi/openapi.d.ts.map +1 -0
- package/dist/internal/openapi/openapi.js +44 -0
- package/dist/internal/openapi/openapi.js.map +1 -0
- package/dist/internal/openapi/openrpc.d.ts +90 -0
- package/dist/internal/openapi/openrpc.d.ts.map +1 -0
- package/dist/internal/openapi/openrpc.js +213 -0
- package/dist/internal/openapi/openrpc.js.map +1 -0
- package/dist/internal/openapi/parser.d.ts +181 -0
- package/dist/internal/openapi/parser.d.ts.map +1 -0
- package/dist/internal/openapi/parser.js +329 -0
- package/dist/internal/openapi/parser.js.map +1 -0
- package/dist/internal/openapi/registry.d.ts +36 -0
- package/dist/internal/openapi/registry.d.ts.map +1 -0
- package/dist/internal/openapi/registry.js +79 -0
- package/dist/internal/openapi/registry.js.map +1 -0
- package/dist/internal/openapi/sample.d.ts +115 -0
- package/dist/internal/openapi/sample.d.ts.map +1 -0
- package/dist/internal/openapi/sample.js +434 -0
- package/dist/internal/openapi/sample.js.map +1 -0
- package/dist/internal/openapi/search.d.ts +19 -0
- package/dist/internal/openapi/search.d.ts.map +1 -0
- package/dist/internal/openapi/search.js +98 -0
- package/dist/internal/openapi/search.js.map +1 -0
- package/dist/internal/openapi/sidebar.d.ts +30 -0
- package/dist/internal/openapi/sidebar.d.ts.map +1 -0
- package/dist/internal/openapi/sidebar.js +67 -0
- package/dist/internal/openapi/sidebar.js.map +1 -0
- package/dist/internal/openapi/union.d.ts +36 -0
- package/dist/internal/openapi/union.d.ts.map +1 -0
- package/dist/internal/openapi/union.js +69 -0
- package/dist/internal/openapi/union.js.map +1 -0
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +20 -0
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/vite-plugins.d.ts +12 -0
- package/dist/internal/vite-plugins.d.ts.map +1 -1
- package/dist/internal/vite-plugins.js +102 -11
- package/dist/internal/vite-plugins.js.map +1 -1
- package/dist/react/Badge.d.ts +2 -3
- package/dist/react/Badge.d.ts.map +1 -1
- package/dist/react/Layout.client.d.ts.map +1 -1
- package/dist/react/Layout.client.js +2 -2
- package/dist/react/Layout.client.js.map +1 -1
- package/dist/react/OpenApi.d.ts +6 -0
- package/dist/react/OpenApi.d.ts.map +1 -0
- package/dist/react/OpenApi.js +6 -0
- package/dist/react/OpenApi.js.map +1 -0
- package/dist/react/internal/CodeToHtml.client.d.ts +53 -2
- package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -1
- package/dist/react/internal/CodeToHtml.client.js +154 -21
- package/dist/react/internal/CodeToHtml.client.js.map +1 -1
- package/dist/react/internal/Sidebar.d.ts.map +1 -1
- package/dist/react/internal/Sidebar.js +99 -2
- package/dist/react/internal/Sidebar.js.map +1 -1
- package/dist/react/internal/openapi/CodeSample.client.d.ts +21 -0
- package/dist/react/internal/openapi/CodeSample.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/CodeSample.client.js +134 -0
- package/dist/react/internal/openapi/CodeSample.client.js.map +1 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts +17 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.js +18 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.js.map +1 -0
- package/dist/react/internal/openapi/Disclosure.client.d.ts +28 -0
- package/dist/react/internal/openapi/Disclosure.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/Disclosure.client.js +38 -0
- package/dist/react/internal/openapi/Disclosure.client.js.map +1 -0
- package/dist/react/internal/openapi/Endpoints.d.ts +26 -0
- package/dist/react/internal/openapi/Endpoints.d.ts.map +1 -0
- package/dist/react/internal/openapi/Endpoints.js +33 -0
- package/dist/react/internal/openapi/Endpoints.js.map +1 -0
- package/dist/react/internal/openapi/EndpointsView.d.ts +24 -0
- package/dist/react/internal/openapi/EndpointsView.d.ts.map +1 -0
- package/dist/react/internal/openapi/EndpointsView.js +26 -0
- package/dist/react/internal/openapi/EndpointsView.js.map +1 -0
- package/dist/react/internal/openapi/EnumValues.client.d.ts +14 -0
- package/dist/react/internal/openapi/EnumValues.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/EnumValues.client.js +20 -0
- package/dist/react/internal/openapi/EnumValues.client.js.map +1 -0
- package/dist/react/internal/openapi/HeadingAnchor.d.ts +15 -0
- package/dist/react/internal/openapi/HeadingAnchor.d.ts.map +1 -0
- package/dist/react/internal/openapi/HeadingAnchor.js +12 -0
- package/dist/react/internal/openapi/HeadingAnchor.js.map +1 -0
- package/dist/react/internal/openapi/OpenApiPage.d.ts +79 -0
- package/dist/react/internal/openapi/OpenApiPage.d.ts.map +1 -0
- package/dist/react/internal/openapi/OpenApiPage.js +72 -0
- package/dist/react/internal/openapi/OpenApiPage.js.map +1 -0
- package/dist/react/internal/openapi/Operation.d.ts +25 -0
- package/dist/react/internal/openapi/Operation.d.ts.map +1 -0
- package/dist/react/internal/openapi/Operation.js +101 -0
- package/dist/react/internal/openapi/Operation.js.map +1 -0
- package/dist/react/internal/openapi/Playground.client.d.ts +33 -0
- package/dist/react/internal/openapi/Playground.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/Playground.client.js +170 -0
- package/dist/react/internal/openapi/Playground.client.js.map +1 -0
- package/dist/react/internal/openapi/PropertyExample.client.d.ts +17 -0
- package/dist/react/internal/openapi/PropertyExample.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/PropertyExample.client.js +21 -0
- package/dist/react/internal/openapi/PropertyExample.client.js.map +1 -0
- package/dist/react/internal/openapi/Reference.d.ts +55 -0
- package/dist/react/internal/openapi/Reference.d.ts.map +1 -0
- package/dist/react/internal/openapi/Reference.js +42 -0
- package/dist/react/internal/openapi/Reference.js.map +1 -0
- package/dist/react/internal/openapi/Schema.d.ts +110 -0
- package/dist/react/internal/openapi/Schema.d.ts.map +1 -0
- package/dist/react/internal/openapi/Schema.js +239 -0
- package/dist/react/internal/openapi/Schema.js.map +1 -0
- package/dist/react/internal/openapi/SchemaUnion.client.d.ts +25 -0
- package/dist/react/internal/openapi/SchemaUnion.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/SchemaUnion.client.js +48 -0
- package/dist/react/internal/openapi/SchemaUnion.client.js.map +1 -0
- package/dist/react/internal/openapi/anchor-navigation.client.d.ts +47 -0
- package/dist/react/internal/openapi/anchor-navigation.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/anchor-navigation.client.js +120 -0
- package/dist/react/internal/openapi/anchor-navigation.client.js.map +1 -0
- package/dist/react/internal/openapi/auth.d.ts +28 -0
- package/dist/react/internal/openapi/auth.d.ts.map +1 -0
- package/dist/react/internal/openapi/auth.js +75 -0
- package/dist/react/internal/openapi/auth.js.map +1 -0
- package/dist/react/useLayout.d.ts +2 -0
- package/dist/react/useLayout.d.ts.map +1 -1
- package/dist/react/useLayout.js +2 -0
- package/dist/react/useLayout.js.map +1 -1
- package/dist/server/handlers.d.ts +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/handlers.js +26 -5
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/og-assets.d.ts +5 -0
- package/dist/server/og-assets.d.ts.map +1 -0
- package/dist/server/og-assets.js +11 -0
- package/dist/server/og-assets.js.map +1 -0
- package/dist/server/openapi/assets.d.ts +33 -0
- package/dist/server/openapi/assets.d.ts.map +1 -0
- package/dist/server/openapi/assets.generated.d.ts +9 -0
- package/dist/server/openapi/assets.generated.d.ts.map +1 -0
- package/dist/server/openapi/assets.generated.js +1091 -0
- package/dist/server/openapi/assets.generated.js.map +1 -0
- package/dist/server/openapi/assets.js +32 -0
- package/dist/server/openapi/assets.js.map +1 -0
- package/dist/server/openapi/handler.d.ts +103 -0
- package/dist/server/openapi/handler.d.ts.map +1 -0
- package/dist/server/openapi/handler.js +198 -0
- package/dist/server/openapi/handler.js.map +1 -0
- package/dist/server/openapi/handler.test.d.ts +2 -0
- package/dist/server/openapi/handler.test.d.ts.map +1 -0
- package/dist/server/openapi/handler.test.js +203 -0
- package/dist/server/openapi/handler.test.js.map +1 -0
- package/dist/server/openapi/html.d.ts +16 -0
- package/dist/server/openapi/html.d.ts.map +1 -0
- package/dist/server/openapi/html.js +75 -0
- package/dist/server/openapi/html.js.map +1 -0
- package/dist/server/openapi/pages.d.ts +33 -0
- package/dist/server/openapi/pages.d.ts.map +1 -0
- package/dist/server/openapi/pages.js +130 -0
- package/dist/server/openapi/pages.js.map +1 -0
- package/dist/server/openapi/pages.test.d.ts +2 -0
- package/dist/server/openapi/pages.test.d.ts.map +1 -0
- package/dist/server/openapi/pages.test.js +94 -0
- package/dist/server/openapi/pages.test.js.map +1 -0
- package/dist/server/openapi/state.d.ts +42 -0
- package/dist/server/openapi/state.d.ts.map +1 -0
- package/dist/server/openapi/state.js +101 -0
- package/dist/server/openapi/state.js.map +1 -0
- package/dist/styles/index.css +16 -0
- package/dist/styles/markdown.css +9 -7
- package/dist/styles/openapi-playground.css +80 -0
- package/dist/styles/openapi.css +660 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +1 -0
- package/dist/vite.js.map +1 -1
- package/dist/waku/internal/middleware/md-router.d.ts +0 -4
- package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
- package/dist/waku/internal/middleware/md-router.js +3 -48
- package/dist/waku/internal/middleware/md-router.js.map +1 -1
- package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js +1 -1
- package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js.map +1 -1
- package/dist/waku/internal/patches/router.d.ts.map +1 -1
- package/dist/waku/internal/patches/router.js +114 -1
- package/dist/waku/internal/patches/router.js.map +1 -1
- package/package.json +5 -1
- package/src/config.ts +1 -0
- package/src/globals.d.ts +16 -0
- package/src/index.ts +1 -0
- package/src/internal/config.ts +40 -1
- package/src/internal/llms.ts +51 -1
- package/src/internal/markdown-negotiation.test.ts +42 -0
- package/src/internal/markdown-negotiation.ts +95 -0
- package/src/internal/markdown.ts +5 -0
- package/src/internal/openapi/anchors.ts +44 -0
- package/src/internal/openapi/app.ts +127 -0
- package/src/internal/openapi/index.ts +24 -0
- package/src/internal/openapi/markdown.test.ts +115 -0
- package/src/internal/openapi/markdown.ts +275 -0
- package/src/internal/openapi/openapi.ts +212 -0
- package/src/internal/openapi/openrpc.test.ts +239 -0
- package/src/internal/openapi/openrpc.ts +295 -0
- package/src/internal/openapi/parser.test.ts +203 -0
- package/src/internal/openapi/parser.ts +613 -0
- package/src/internal/openapi/registry.test.ts +89 -0
- package/src/internal/openapi/registry.ts +89 -0
- package/src/internal/openapi/sample.test.ts +283 -0
- package/src/internal/openapi/sample.ts +562 -0
- package/src/internal/openapi/search.test.ts +62 -0
- package/src/internal/openapi/search.ts +108 -0
- package/src/internal/openapi/sidebar.test.ts +131 -0
- package/src/internal/openapi/sidebar.ts +94 -0
- package/src/internal/openapi/union.test.ts +51 -0
- package/src/internal/openapi/union.ts +74 -0
- package/src/internal/search.ts +20 -0
- package/src/internal/test/virtual-config.stub.ts +14 -0
- package/src/internal/vite-plugins.ts +106 -11
- package/src/openapi-app/App.tsx +64 -0
- package/src/openapi-app/blocks.tsx +33 -0
- package/src/openapi-app/client.tsx +25 -0
- package/src/openapi-app/links.test.ts +84 -0
- package/src/openapi-app/links.ts +66 -0
- package/src/openapi-app/payload.ts +20 -0
- package/src/openapi-app/virtual/config.ts +7 -0
- package/src/openapi-app/virtual/group-icons.ts +2 -0
- package/src/openapi-app/virtual/langs.ts +6 -0
- package/src/openapi-app/virtual/openapi.ts +10 -0
- package/src/openapi-app/virtual/search-index.ts +21 -0
- package/src/openapi-app/virtual/slots.ts +4 -0
- package/src/openapi-app/virtual/user-styles.ts +2 -0
- package/src/openapi-app/waku.tsx +154 -0
- package/src/react/Badge.tsx +2 -3
- package/src/react/Layout.client.tsx +17 -4
- package/src/react/OpenApi.tsx +5 -0
- package/src/react/internal/CodeToHtml.client.tsx +283 -22
- package/src/react/internal/Sidebar.tsx +126 -22
- package/src/react/internal/openapi/CodeSample.client.tsx +294 -0
- package/src/react/internal/openapi/CollapsibleChildren.client.tsx +41 -0
- package/src/react/internal/openapi/Disclosure.client.tsx +67 -0
- package/src/react/internal/openapi/Endpoints.tsx +58 -0
- package/src/react/internal/openapi/EndpointsView.tsx +76 -0
- package/src/react/internal/openapi/EnumValues.client.tsx +49 -0
- package/src/react/internal/openapi/HeadingAnchor.tsx +28 -0
- package/src/react/internal/openapi/OpenApiPage.tsx +173 -0
- package/src/react/internal/openapi/Operation.test.tsx +101 -0
- package/src/react/internal/openapi/Operation.tsx +335 -0
- package/src/react/internal/openapi/Playground.client.tsx +234 -0
- package/src/react/internal/openapi/PropertyExample.client.tsx +55 -0
- package/src/react/internal/openapi/Reference.tsx +120 -0
- package/src/react/internal/openapi/Schema.tsx +467 -0
- package/src/react/internal/openapi/SchemaUnion.client.tsx +123 -0
- package/src/react/internal/openapi/anchor-navigation.client.ts +154 -0
- package/src/react/internal/openapi/auth.ts +69 -0
- package/src/react/useLayout.ts +4 -0
- package/src/server/handlers.ts +31 -6
- package/src/server/og-assets.ts +14 -0
- package/src/server/openapi/assets.generated.ts +1093 -0
- package/src/server/openapi/assets.ts +57 -0
- package/src/server/openapi/handler.test.ts +244 -0
- package/src/server/openapi/handler.ts +277 -0
- package/src/server/openapi/html.ts +84 -0
- package/src/server/openapi/pages.test.ts +111 -0
- package/src/server/openapi/pages.ts +153 -0
- package/src/server/openapi/state.ts +136 -0
- package/src/styles/index.css +16 -0
- package/src/styles/markdown.css +9 -7
- package/src/styles/openapi-playground.css +80 -0
- package/src/styles/openapi.css +660 -0
- package/src/vite.ts +1 -0
- package/src/waku/internal/middleware/md-router.ts +8 -52
- package/src/waku/internal/patches/adapters/vercel-build-enhancer.ts +1 -1
- 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
|
+
}
|