@bielu/scalar-signalr 0.1.0

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.
@@ -0,0 +1,21 @@
1
+ import { resolveDocuments as resolveDocumentsCore } from '@bielu/scalar-core'
2
+ import type { SignalRDocumentRef } from './types'
3
+
4
+ /** How the SignalR bundle discovers its AsyncAPI documents (see `resolveDocuments` in @bielu/scalar-core). */
5
+ export const SIGNALR_DISCOVERY = {
6
+ configKey: 'signalr',
7
+ globalName: '__BIELU_SCALAR_SIGNALR__',
8
+ } as const
9
+
10
+ /**
11
+ * Resolves the documents to scan, in decreasing priority: an explicit override (`documents` prop);
12
+ * a `SignalRPluginConfig` embedded inline on the Scalar config as `config.signalr`; a
13
+ * `window.__BIELU_SCALAR_SIGNALR__` global; otherwise auto-discovery from the Scalar configuration
14
+ * passed as `options`.
15
+ */
16
+ export function resolveDocuments(
17
+ options: Record<string, any> | undefined,
18
+ override?: SignalRDocumentRef[],
19
+ ): SignalRDocumentRef[] {
20
+ return resolveDocumentsCore(options, SIGNALR_DISCOVERY, override)
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { bootstrapConsole } from '@bielu/scalar-core'
2
+ import SignalRConsole from './components/SignalRConsole.vue'
3
+ import { SIGNALR_DISCOVERY } from './discovery'
4
+ import { SIGNALR_PLUGIN_SPEC } from './plugin'
5
+
6
+ export { createSignalRPlugin } from './plugin'
7
+ export { loadSignalRHubs, parseSignalRHubs } from './signalr-bindings'
8
+ export type {
9
+ SignalRDirection,
10
+ SignalRDocumentRef,
11
+ SignalRHubModel,
12
+ SignalROperationModel,
13
+ SignalRPluginConfig,
14
+ } from './types'
15
+
16
+ // Register the console Web Component and hook `window.Scalar.createApiReference` so every Scalar
17
+ // API Reference on the page picks up the SignalR plugin (see @bielu/scalar-core's bootstrap).
18
+ bootstrapConsole(
19
+ {
20
+ ...SIGNALR_PLUGIN_SPEC,
21
+ discovery: SIGNALR_DISCOVERY,
22
+ stylesId: 'bielu-signalr-styles',
23
+ wrappedFlag: '__bieluSignalRWrapped',
24
+ },
25
+ SignalRConsole,
26
+ )
package/src/plugin.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { createConsolePlugin } from '@bielu/scalar-core'
2
+ import type { SignalRDocumentRef } from './types'
3
+
4
+ /** The custom element tag the console is rendered as (registered in `index.ts`). */
5
+ export const SIGNALR_CONSOLE_TAG = 'bielu-signalr-console'
6
+
7
+ /** The Scalar plugin's identifier, used to detect (and de-duplicate) an existing registration. */
8
+ export const SIGNALR_PLUGIN_NAME = 'bielu-signalr'
9
+
10
+ /** How the SignalR console registers with Scalar (plugin name, element tag, sidebar label). */
11
+ export const SIGNALR_PLUGIN_SPEC = {
12
+ pluginName: SIGNALR_PLUGIN_NAME,
13
+ elementTag: SIGNALR_CONSOLE_TAG,
14
+ sidebarLabel: 'SignalR',
15
+ } as const
16
+
17
+ /**
18
+ * The structural shape of a Scalar API Reference plugin. Declared locally (a structural copy of
19
+ * @bielu/scalar-core's type) so the published declaration files never reference the private core
20
+ * package; Scalar validates it at runtime.
21
+ */
22
+ export type ApiReferencePlugin = () => {
23
+ name: string
24
+ extensions: unknown[]
25
+ views?: Record<string, unknown[]>
26
+ hooks?: Record<string, unknown>
27
+ apiClientPlugins?: unknown[]
28
+ }
29
+
30
+ /**
31
+ * Creates the SignalR console plugin for the Scalar API Reference.
32
+ *
33
+ * The view's `component` is the custom element tag name (a string), so Scalar's own Vue renders a
34
+ * plain element — our Web Component (with its own Vue + styles) takes over from there. The AsyncAPI
35
+ * documents (discovered from the Scalar configuration at registration time) are passed to the
36
+ * element as the `documents` prop, so the console does not depend on Scalar binding its config.
37
+ */
38
+ export const createSignalRPlugin = (documents?: SignalRDocumentRef[]): ApiReferencePlugin =>
39
+ createConsolePlugin(SIGNALR_PLUGIN_SPEC, documents)
@@ -0,0 +1,114 @@
1
+ import {
2
+ deref,
3
+ exampleFromSchema,
4
+ extractSecuritySchemes,
5
+ firstServerHost,
6
+ loadAsyncApiDocuments,
7
+ refName,
8
+ resolveServerBaseUrl,
9
+ } from '@bielu/scalar-core'
10
+ import type {
11
+ SignalRDirection,
12
+ SignalRDocumentRef,
13
+ SignalRHubModel,
14
+ SignalRMessageModel,
15
+ SignalROperationModel,
16
+ } from './types'
17
+
18
+ const SIGNALR = 'signalr'
19
+
20
+ type AnyRecord = Record<string, any>
21
+
22
+ /** Resolve the message(s) an operation carries and generate an example payload for each. */
23
+ function buildMessages(doc: AnyRecord, op: AnyRecord): SignalRMessageModel[] {
24
+ const refs = (Array.isArray(op.messages) ? op.messages : [])
25
+ .map((m: AnyRecord) => m?.$ref)
26
+ .filter((ref: unknown): ref is string => typeof ref === 'string')
27
+
28
+ const messages: SignalRMessageModel[] = []
29
+ for (const ref of refs) {
30
+ const message = deref(doc, { $ref: ref })
31
+ const name = message?.name ?? refName(ref) ?? 'message'
32
+ const payloadExample = exampleFromSchema(doc, message?.payload)
33
+ // SignalR hub methods take positional arguments; the payload is the (first) argument.
34
+ const args = payloadExample === null ? [] : [payloadExample]
35
+ messages.push({ name, title: message?.title, example: JSON.stringify(args, null, 2) })
36
+ }
37
+ return messages
38
+ }
39
+
40
+ function directionFor(op: AnyRecord, binding: AnyRecord | undefined): SignalRDirection {
41
+ const fromBinding = binding?.direction
42
+ if (fromBinding === 'clientToServer' || fromBinding === 'serverToClient') {
43
+ return fromBinding
44
+ }
45
+ // AsyncAPI 3 `action`: `send` = application -> channel, `receive` = channel -> application.
46
+ return op.action === 'receive' ? 'serverToClient' : 'clientToServer'
47
+ }
48
+
49
+ /** Extract every SignalR hub (channel + its operations) from a parsed AsyncAPI document. */
50
+ export function parseSignalRHubs(documentName: string, doc: AnyRecord): SignalRHubModel[] {
51
+ if (!doc || typeof doc !== 'object') {
52
+ return []
53
+ }
54
+
55
+ const baseUrl = resolveServerBaseUrl(firstServerHost(doc, SIGNALR))
56
+
57
+ // Extract security schemes from the document so the console can read them at connect time
58
+ // without re-fetching the document. Only a minimal subset is needed for auth resolution.
59
+ const securitySchemes = extractSecuritySchemes(doc)
60
+ const docSecuritySchemes = Object.keys(securitySchemes).length > 0 ? securitySchemes : undefined
61
+
62
+ const channels: AnyRecord = doc.channels ?? {}
63
+ const hubs = new Map<string, SignalRHubModel>()
64
+
65
+ for (const channelName of Object.keys(channels)) {
66
+ const channel: AnyRecord = channels[channelName] ?? {}
67
+ const binding: AnyRecord | undefined = channel.bindings?.[SIGNALR]
68
+ if (!binding) {
69
+ continue
70
+ }
71
+ hubs.set(channelName, {
72
+ documentName,
73
+ channelName,
74
+ hubPath: binding.hub ?? channel.address ?? `/${channelName}`,
75
+ baseUrl,
76
+ transports: binding.transports ?? [],
77
+ protocols: binding.protocols ?? [],
78
+ operations: [],
79
+ securitySchemes: docSecuritySchemes,
80
+ })
81
+ }
82
+
83
+ const operations: AnyRecord = doc.operations ?? {}
84
+ for (const opName of Object.keys(operations)) {
85
+ const op: AnyRecord = operations[opName] ?? {}
86
+ const channelName = refName(op.channel?.$ref)
87
+ const hub = channelName ? hubs.get(channelName) : undefined
88
+ if (!hub) {
89
+ continue
90
+ }
91
+ const binding: AnyRecord | undefined = op.bindings?.[SIGNALR]
92
+ const model: SignalROperationModel = {
93
+ id: opName,
94
+ target: binding?.target ?? opName,
95
+ direction: directionFor(op, binding),
96
+ callType: binding?.callType,
97
+ summary: op.summary ?? op.description,
98
+ messages: buildMessages(doc, op),
99
+ }
100
+ hub.operations.push(model)
101
+ }
102
+
103
+ return [...hubs.values()]
104
+ }
105
+
106
+ /**
107
+ * Resolve the configured documents (inline object or URL) and aggregate the SignalR hubs they
108
+ * declare. Only documents that are actually AsyncAPI are considered, so OpenAPI sources in the
109
+ * same Scalar configuration are ignored.
110
+ */
111
+ export async function loadSignalRHubs(documents: SignalRDocumentRef[]): Promise<SignalRHubModel[]> {
112
+ const loaded = await loadAsyncApiDocuments(documents)
113
+ return loaded.flatMap(({ name, doc }) => parseSignalRHubs(name, doc))
114
+ }
package/src/types.ts ADDED
@@ -0,0 +1,75 @@
1
+ /** Direction of a SignalR hub operation relative to the server. */
2
+ export type SignalRDirection = 'clientToServer' | 'serverToClient'
3
+
4
+ /** A reference to an AsyncAPI document — either by URL or as an already-parsed object. */
5
+ export type SignalRDocumentRef = {
6
+ /** Logical document name (matches the AsyncAPI/source title). */
7
+ name: string
8
+ /** URL the AsyncAPI JSON document is served from. */
9
+ url?: string
10
+ /** An inline, already-parsed document object (from a Scalar source's `content`). */
11
+ doc?: Record<string, any>
12
+ }
13
+
14
+ /** Configuration consumed by the SignalR console plugin. */
15
+ export type SignalRPluginConfig = {
16
+ /**
17
+ * AsyncAPI documents to scan for SignalR bindings. Normally derived automatically from the
18
+ * Scalar configuration's `sources`; only set explicitly to override that discovery.
19
+ */
20
+ documents: SignalRDocumentRef[]
21
+ }
22
+
23
+ /** A message an operation can carry, with a generated example payload. */
24
+ export type SignalRMessageModel = {
25
+ /** Message name/key (e.g. `chatMessage`). */
26
+ name: string
27
+ /** Human-friendly title, when the document provides one. */
28
+ title?: string
29
+ /**
30
+ * A ready-to-send example of the hub method arguments, as a pretty-printed JSON array string
31
+ * (e.g. `[{ "user": "string", … }]`), generated from the message payload schema.
32
+ */
33
+ example: string
34
+ }
35
+
36
+ /** A single hub operation (client-to-server method or server-to-client event). */
37
+ export type SignalROperationModel = {
38
+ /** AsyncAPI operation id. */
39
+ id: string
40
+ /** The hub method / event name actually used on the wire. */
41
+ target: string
42
+ direction: SignalRDirection
43
+ /** `invocation`, `streamInvocation` or `send`. */
44
+ callType?: string
45
+ summary?: string
46
+ /** Messages this operation carries, each with an example payload to prefill the editor. */
47
+ messages: SignalRMessageModel[]
48
+ }
49
+
50
+ /** A minimal representation of an AsyncAPI security scheme, extracted at document-parse time. */
51
+ export type SecuritySchemeModel = {
52
+ /** e.g. `'apiKey'`, `'http'`, `'oauth2'`, `'openIdConnect'`. */
53
+ type: string
54
+ /** For `apiKey`: `'query'`, `'header'`, or `'cookie'`. */
55
+ in?: string
56
+ /** For `apiKey`: the query-param or header field name. */
57
+ name?: string
58
+ /** For `http`: `'bearer'`, `'basic'`, etc. */
59
+ scheme?: string
60
+ }
61
+
62
+ /** A SignalR hub discovered from an AsyncAPI channel with a `signalr` binding. */
63
+ export type SignalRHubModel = {
64
+ documentName: string
65
+ channelName: string
66
+ /** Hub path, e.g. `/chatHub`. */
67
+ hubPath: string
68
+ /** Best-effort absolute base URL (scheme://host) resolved from the AsyncAPI servers. */
69
+ baseUrl: string
70
+ transports: string[]
71
+ protocols: string[]
72
+ operations: SignalROperationModel[]
73
+ /** Security schemes declared in this hub's AsyncAPI document, keyed by scheme name. */
74
+ securitySchemes?: Record<string, SecuritySchemeModel>
75
+ }