blume 1.5.0 → 1.5.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/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +449 -135
- package/dist/cli/index.js.map +24 -23
- package/dist/types/ai/ask-context.d.ts +78 -0
- package/dist/types/core/config-input.d.ts +54 -2
- package/dist/types/core/data.d.ts +19 -2
- package/dist/types/core/open-in-chat.d.ts +9 -0
- package/dist/types/core/schema.d.ts +48 -1
- package/dist/types/core/types.d.ts +10 -3
- package/dist/types/openapi/references.d.ts +9 -0
- package/dist/types/search/orama-index.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +67 -5
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/ai.mdx +35 -0
- package/docs/configuration/index.mdx +14 -2
- package/docs/configuration/search.mdx +4 -4
- package/docs/configuration/theming.mdx +4 -2
- package/docs/reference/cli.mdx +2 -2
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/ask-context.ts +51 -11
- package/src/ai/mcp/data.ts +3 -2
- package/src/ai/mcp/server.ts +3 -2
- package/src/assets/icon-dark.png +0 -0
- package/src/astro/generate.ts +172 -18
- package/src/astro/templates.ts +89 -15
- package/src/components/content/AccordionItem.astro +4 -0
- package/src/components/content/Update.astro +3 -0
- package/src/components/islands/AskAI.astro +6 -0
- package/src/components/islands/ask-ai.tsx +39 -9
- package/src/components/layout/Analytics.astro +9 -1
- package/src/components/layout/Favicon.astro +29 -8
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/PageActions.astro +120 -78
- package/src/components/layout/PageFeedback.astro +12 -3
- package/src/components/layout/PageLayout.astro +79 -5
- package/src/components/layout/ReferenceLayout.astro +12 -9
- package/src/components/layout/RootLayout.astro +153 -121
- package/src/components/layout/Search.astro +41 -26
- package/src/components/layout/drawer-inert.ts +10 -5
- package/src/components/layout/head-scripts.ts +34 -16
- package/src/components/layout/nav-utils.ts +34 -15
- package/src/components/layout/search/orama.ts +3 -2
- package/src/components/openapi/AsyncApiOperation.astro +22 -7
- package/src/components/openapi/MessageComposer.astro +238 -0
- package/src/components/openapi/Operation.astro +26 -12
- package/src/components/openapi/PanelTabs.astro +7 -0
- package/src/components/openapi/Playground.astro +320 -0
- package/src/components/openapi/RequestPanel.astro +1 -0
- package/src/components/openapi/async-snippets.ts +20 -7
- package/src/components/openapi/async.ts +13 -2
- package/src/components/openapi/message-composer.ts +242 -0
- package/src/components/openapi/message-model.ts +108 -0
- package/src/components/openapi/message.ts +153 -0
- package/src/components/openapi/operation-model.ts +260 -0
- package/src/components/openapi/playground-client.ts +486 -0
- package/src/components/openapi/playground-schema.ts +109 -0
- package/src/components/openapi/request.ts +287 -0
- package/src/components/openapi/security.ts +0 -56
- package/src/components/openapi/snippets.ts +23 -136
- package/src/components/openapi/validate-json.ts +144 -0
- package/src/components/openapi/ws-client.ts +194 -0
- package/src/core/config-input.ts +67 -1
- package/src/core/content-assets.ts +66 -15
- package/src/core/data.ts +16 -2
- package/src/core/last-modified.ts +76 -2
- package/src/core/links.ts +30 -4
- package/src/core/navigation.ts +26 -1
- package/src/core/open-in-chat.ts +17 -0
- package/src/core/project-graph.ts +11 -0
- package/src/core/schema.ts +60 -1
- package/src/core/server-features.ts +11 -0
- package/src/core/sources/normalize.ts +10 -2
- package/src/core/types.ts +10 -3
- package/src/deploy/vercel-negotiation.ts +34 -14
- package/src/og/card.ts +3 -1
- package/src/openapi/model.ts +7 -0
- package/src/openapi/proxy.ts +217 -0
- package/src/openapi/references.ts +8 -0
- package/src/openapi/source.ts +13 -0
- package/src/registry/eject.ts +4 -5
- package/src/search/orama-index.ts +109 -36
- package/src/theme/entry.ts +15 -2
- package/src/theme/fonts.ts +75 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AsyncApiAction,
|
|
3
|
+
AsyncApiServerObject,
|
|
4
|
+
} from "../../openapi/asyncapi.ts";
|
|
5
|
+
import type { AsyncApiMessageLike, NamedMessage } from "./async.ts";
|
|
6
|
+
import { payloadSchema } from "./async.ts";
|
|
7
|
+
import { exampleValue, toJson } from "./helpers.ts";
|
|
8
|
+
import type { ParameterLike, SchemaLike } from "./helpers.ts";
|
|
9
|
+
import type {
|
|
10
|
+
MessageModel,
|
|
11
|
+
MessageParam,
|
|
12
|
+
MessagePayload,
|
|
13
|
+
MessageServerOption,
|
|
14
|
+
} from "./message.ts";
|
|
15
|
+
import { inputValue, validationSchema } from "./playground-schema.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Server-side derivation of the event composer's message model — the AsyncAPI
|
|
19
|
+
* counterpart of `operation-model.ts`. The spec document, the sampler, and the
|
|
20
|
+
* channel/server resolution meet here once at build time; the resulting
|
|
21
|
+
* {@link MessageModel} is embedded as JSON on the operation page so the lazy
|
|
22
|
+
* client chunk carries none of it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** The protocols a docs page can actually open a live connection to. */
|
|
26
|
+
const LIVE_PROTOCOLS = { ws: true } satisfies Record<string, true>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Channel parameters as composer inputs. `channelParameters` has already
|
|
30
|
+
* lowered AsyncAPI's string-only parameters (enum, default, examples) into the
|
|
31
|
+
* shared parameter shape, so all that is left is picking a prefill: the
|
|
32
|
+
* declared default, first example, or first enum member, else nothing. Never
|
|
33
|
+
* the sampler — its `{type:"string"}` output is the literal word `string`,
|
|
34
|
+
* which would replace every `{name}` template in the address samples with
|
|
35
|
+
* junk like `user/string/signedup`.
|
|
36
|
+
*/
|
|
37
|
+
const messageParams = (parameters: ParameterLike[]): MessageParam[] =>
|
|
38
|
+
parameters.map((parameter) => ({
|
|
39
|
+
description: parameter.description,
|
|
40
|
+
enum: parameter.schema?.enum?.map(String),
|
|
41
|
+
name: parameter.name ?? "",
|
|
42
|
+
value: inputValue(
|
|
43
|
+
parameter.schema?.default ??
|
|
44
|
+
parameter.example ??
|
|
45
|
+
parameter.schema?.enum?.[0]
|
|
46
|
+
),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The payload editor's state for the operation's first message. Multi-message
|
|
51
|
+
* operations render every message's schema on the page, but the composer sends
|
|
52
|
+
* one payload — the first is the one the samples already show.
|
|
53
|
+
*/
|
|
54
|
+
const messagePayload = (
|
|
55
|
+
message: AsyncApiMessageLike | undefined,
|
|
56
|
+
schemas: Record<string, SchemaLike>
|
|
57
|
+
): MessagePayload => {
|
|
58
|
+
if (!message) {
|
|
59
|
+
return { example: "" };
|
|
60
|
+
}
|
|
61
|
+
const schema = payloadSchema(message);
|
|
62
|
+
// `undefined` means the message declares no example, `null` means it declares
|
|
63
|
+
// an empty one — only the former falls back to a schema sample. The sampler's
|
|
64
|
+
// own `null` is its no-schema/failure sentinel, not a value a reader typed:
|
|
65
|
+
// it degrades to an empty editor (which samples and sends `{}`) rather than
|
|
66
|
+
// prefilling the literal `null`.
|
|
67
|
+
const declared = message.examples?.[0]?.payload;
|
|
68
|
+
const example =
|
|
69
|
+
declared === undefined ? exampleValue(schema, schemas) : declared;
|
|
70
|
+
return {
|
|
71
|
+
contentType: message.contentType,
|
|
72
|
+
example:
|
|
73
|
+
declared === undefined && example === null ? "" : (toJson(example) ?? ""),
|
|
74
|
+
schema: validationSchema(schema, schemas),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A server's base URL for the picker. Falls back to the bare host when the
|
|
80
|
+
* spec omits `protocol` — a label is a label, and the snippet builders read
|
|
81
|
+
* the server object itself rather than this string.
|
|
82
|
+
*/
|
|
83
|
+
const serverOption = (server: AsyncApiServerObject): MessageServerOption => {
|
|
84
|
+
const host = `${server.host ?? ""}${server.pathname ?? ""}`;
|
|
85
|
+
return {
|
|
86
|
+
label: server.protocol ? `${server.protocol}://${host}` : host,
|
|
87
|
+
server,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Derive the composer's message model for one event operation, at build time. */
|
|
92
|
+
export const messageModel = (args: {
|
|
93
|
+
action: AsyncApiAction;
|
|
94
|
+
address: string;
|
|
95
|
+
messages: NamedMessage[];
|
|
96
|
+
parameters: ParameterLike[];
|
|
97
|
+
protocol?: string;
|
|
98
|
+
schemas: Record<string, SchemaLike>;
|
|
99
|
+
servers: AsyncApiServerObject[];
|
|
100
|
+
}): MessageModel => ({
|
|
101
|
+
action: args.action,
|
|
102
|
+
address: args.address,
|
|
103
|
+
connectable: Object.hasOwn(LIVE_PROTOCOLS, args.protocol ?? ""),
|
|
104
|
+
params: messageParams(args.parameters),
|
|
105
|
+
payload: messagePayload(args.messages[0]?.message, args.schemas),
|
|
106
|
+
protocol: args.protocol,
|
|
107
|
+
servers: args.servers.map(serverOption),
|
|
108
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AsyncApiAction,
|
|
3
|
+
AsyncApiServerObject,
|
|
4
|
+
} from "../../openapi/asyncapi.ts";
|
|
5
|
+
import type { MessageSample } from "./async-snippets.ts";
|
|
6
|
+
import type { ValidationSchema } from "./request.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Framework-free message model for the event ("Try it") composer — the
|
|
10
|
+
* AsyncAPI counterpart of `request.ts`. The composer form, the protocol-aware
|
|
11
|
+
* code samples, and the live WebSocket frame all derive from one
|
|
12
|
+
* {@link buildMessage} call, so a copied `wscat` command and a composed send
|
|
13
|
+
* carry the same address and the same payload bytes by construction.
|
|
14
|
+
*
|
|
15
|
+
* Ships to the browser inside the composer's lazy chunk: no spec parser, no
|
|
16
|
+
* sampler, no Astro imports.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One channel parameter as a composer input. Channel params are always required. */
|
|
20
|
+
export interface MessageParam {
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Declared `enum` members, when the parameter constrains its values. */
|
|
23
|
+
enum?: string[];
|
|
24
|
+
name: string;
|
|
25
|
+
/** Prefill from the parameter's `default`/`examples`; "" when none. */
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The composer's payload editor state. */
|
|
30
|
+
export interface MessagePayload {
|
|
31
|
+
/** The message's declared `contentType`, when it has one. */
|
|
32
|
+
contentType?: string;
|
|
33
|
+
/** Pretty-printed prefill: declared example, else a schema sample; "" when none. */
|
|
34
|
+
example: string;
|
|
35
|
+
/** Pruned payload schema the editor validates against, when derivable. */
|
|
36
|
+
schema?: ValidationSchema;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One server option in the picker: a display URL plus the spec object behind it. */
|
|
40
|
+
export interface MessageServerOption {
|
|
41
|
+
/** `protocol://host+pathname` — what the reader picks between. */
|
|
42
|
+
label: string;
|
|
43
|
+
/** The spec server, verbatim; the snippet builders read host/pathname/protocol. */
|
|
44
|
+
server: AsyncApiServerObject;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MessageModel {
|
|
48
|
+
action: AsyncApiAction;
|
|
49
|
+
/** Channel address with `{param}` templates intact, e.g. `user/{id}/signedup`. */
|
|
50
|
+
address: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the panel offers a live connection. Only WebSocket bindings can be
|
|
53
|
+
* driven from a docs page — Kafka, MQTT and friends get the composer and the
|
|
54
|
+
* CLI samples rather than faked connectivity.
|
|
55
|
+
*/
|
|
56
|
+
connectable: boolean;
|
|
57
|
+
params: MessageParam[];
|
|
58
|
+
payload: MessagePayload;
|
|
59
|
+
/** Normalized binding key (`ws`, `kafka`, `mqtt`, …) when the spec implies one. */
|
|
60
|
+
protocol?: string;
|
|
61
|
+
servers: MessageServerOption[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MessageValues {
|
|
65
|
+
/** Free-text server URL; overrides the picked server when non-empty. */
|
|
66
|
+
customUrl: string;
|
|
67
|
+
/** Key: parameter name. */
|
|
68
|
+
params: Record<string, string>;
|
|
69
|
+
/** Raw payload editor text. */
|
|
70
|
+
payload: string;
|
|
71
|
+
/** Index into `model.servers`; out of range falls back to the first. */
|
|
72
|
+
server: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Values pre-filled from the model's precomputed examples. */
|
|
76
|
+
export const defaultMessageValues = (model: MessageModel): MessageValues => ({
|
|
77
|
+
customUrl: "",
|
|
78
|
+
params: Object.fromEntries(
|
|
79
|
+
model.params.map((param) => [param.name, param.value])
|
|
80
|
+
),
|
|
81
|
+
payload: model.payload.example,
|
|
82
|
+
server: 0,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const PARAM_TEMPLATE = /\{(?<name>[^{}]+)\}/gu;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A free-text base URL lowered onto the server shape the snippet builders and
|
|
89
|
+
* the connect URL read. A full URL contributes its scheme, host, and path.
|
|
90
|
+
* Anything without an authority — a bare `host:port`, which is how broker
|
|
91
|
+
* tooling is usually addressed, and which `new URL` happily reads as a scheme
|
|
92
|
+
* plus path — is kept whole as the host, leaving the protocol to the spec.
|
|
93
|
+
*/
|
|
94
|
+
const customServer = (url: string): AsyncApiServerObject => {
|
|
95
|
+
let parsed: URL | undefined;
|
|
96
|
+
try {
|
|
97
|
+
parsed = new URL(url);
|
|
98
|
+
} catch {
|
|
99
|
+
parsed = undefined;
|
|
100
|
+
}
|
|
101
|
+
if (!parsed || parsed.host === "") {
|
|
102
|
+
return { host: url };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
host: parsed.host,
|
|
106
|
+
pathname: parsed.pathname === "/" ? "" : parsed.pathname,
|
|
107
|
+
protocol: parsed.protocol.replace(":", ""),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* THE one message builder: the composer's samples, its copy buttons, and the
|
|
113
|
+
* live WebSocket frame all consume its output. Parameter values are
|
|
114
|
+
* URL-encoded into the address; a parameter left blank keeps its `{name}`
|
|
115
|
+
* template so the sample still reads as a template rather than a broken
|
|
116
|
+
* address. Payload text that doesn't parse yields no payload — the editor is
|
|
117
|
+
* showing the reader a validation error at that moment, and neither a sample
|
|
118
|
+
* nor a send should invent a value.
|
|
119
|
+
*/
|
|
120
|
+
export const buildMessage = (
|
|
121
|
+
model: MessageModel,
|
|
122
|
+
values: MessageValues
|
|
123
|
+
): MessageSample => {
|
|
124
|
+
const custom = values.customUrl.trim();
|
|
125
|
+
const picked = model.servers[values.server] ?? model.servers[0];
|
|
126
|
+
let payload: unknown;
|
|
127
|
+
try {
|
|
128
|
+
// SAFETY: JSON.parse returns `any`; widening it to `unknown` claims
|
|
129
|
+
// nothing about the shape and forces consumers to narrow.
|
|
130
|
+
payload = JSON.parse(values.payload) as unknown;
|
|
131
|
+
} catch {
|
|
132
|
+
payload = undefined;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
action: model.action,
|
|
136
|
+
address: model.address.replaceAll(PARAM_TEMPLATE, (template, name) => {
|
|
137
|
+
const value = values.params[String(name)] ?? "";
|
|
138
|
+
return value === "" ? template : encodeURIComponent(value);
|
|
139
|
+
}),
|
|
140
|
+
payload,
|
|
141
|
+
server: custom === "" ? picked?.server : customServer(custom),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The exact bytes a live send transmits. Identical to the payload the `wscat`
|
|
147
|
+
* and browser-`WebSocket` samples embed, which is the whole point: what a
|
|
148
|
+
* reader copies is what Send puts on the wire. Only an absent payload
|
|
149
|
+
* (unparseable editor text) degrades to `{}` — a `null` payload is a value the
|
|
150
|
+
* reader typed, so it goes out as `null`.
|
|
151
|
+
*/
|
|
152
|
+
export const messageFrame = (sample: MessageSample): string =>
|
|
153
|
+
JSON.stringify(sample.payload === undefined ? {} : sample.payload);
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exampleValue,
|
|
3
|
+
objectProperties,
|
|
4
|
+
resolveSchema,
|
|
5
|
+
toJson,
|
|
6
|
+
} from "./helpers.ts";
|
|
7
|
+
import type { ParameterLike, SchemaLike, SpecValue } from "./helpers.ts";
|
|
8
|
+
import {
|
|
9
|
+
declaredTypes,
|
|
10
|
+
inputValue,
|
|
11
|
+
scalarType,
|
|
12
|
+
validationSchema,
|
|
13
|
+
} from "./playground-schema.ts";
|
|
14
|
+
import type {
|
|
15
|
+
PlaygroundAuthInput,
|
|
16
|
+
PlaygroundBody,
|
|
17
|
+
PlaygroundBodyField,
|
|
18
|
+
PlaygroundModel,
|
|
19
|
+
PlaygroundParam,
|
|
20
|
+
} from "./request.ts";
|
|
21
|
+
import { schemeLabel } from "./security.ts";
|
|
22
|
+
import type { OperationSecurity, ResolvedScheme } from "./security.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Server-side derivation of the playground's request model. This is where the
|
|
26
|
+
* spec document, openapi-sampler, and the security resolution meet — once, at
|
|
27
|
+
* build time. The resulting `PlaygroundModel` is embedded as JSON on the page,
|
|
28
|
+
* so the client (`request.ts`) never needs any of those dependencies.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
interface MediaTypeLike {
|
|
32
|
+
schema?: SchemaLike;
|
|
33
|
+
example?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Primitive types the flat-body fields UI can edit directly. */
|
|
37
|
+
const PRIMITIVE_TYPES = {
|
|
38
|
+
boolean: true,
|
|
39
|
+
integer: true,
|
|
40
|
+
number: true,
|
|
41
|
+
string: true,
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
/** Whether a spec example is a plain object usable for per-field defaults. */
|
|
45
|
+
const isExampleObject = (
|
|
46
|
+
value: SpecValue
|
|
47
|
+
): value is Record<string, SpecValue> =>
|
|
48
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Playground inputs for the operation's parameters. Cookie params are skipped
|
|
52
|
+
* (not supported in v1 — browsers won't let a page set arbitrary cookies).
|
|
53
|
+
* Path params are always required per spec, even when a lax document omits the
|
|
54
|
+
* flag. Only REQUIRED params get a precomputed example; optional ones default
|
|
55
|
+
* to "" so the default samples carry required params only — parity with the
|
|
56
|
+
* old static samples.
|
|
57
|
+
*/
|
|
58
|
+
const modelParams = (
|
|
59
|
+
parameters: ParameterLike[],
|
|
60
|
+
schemas: Record<string, SchemaLike>
|
|
61
|
+
): PlaygroundParam[] => {
|
|
62
|
+
const params: PlaygroundParam[] = [];
|
|
63
|
+
for (const param of parameters) {
|
|
64
|
+
const where = param.in;
|
|
65
|
+
if (
|
|
66
|
+
!param.name ||
|
|
67
|
+
(where !== "path" && where !== "query" && where !== "header")
|
|
68
|
+
) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const required = where === "path" ? true : param.required === true;
|
|
72
|
+
const schema = resolveSchema(schemas, param.schema);
|
|
73
|
+
params.push({
|
|
74
|
+
description: param.description,
|
|
75
|
+
enum: schema.enum?.map(String),
|
|
76
|
+
in: where,
|
|
77
|
+
name: param.name,
|
|
78
|
+
required,
|
|
79
|
+
type: scalarType(param.schema, schemas),
|
|
80
|
+
value: required
|
|
81
|
+
? inputValue(param.example ?? exampleValue(param.schema, schemas))
|
|
82
|
+
: "",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return params;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** The JSON-ish media entry: first whose type mentions json, else the first. */
|
|
89
|
+
const jsonContentType = (
|
|
90
|
+
content: Record<string, MediaTypeLike> | undefined
|
|
91
|
+
): [string, MediaTypeLike] | undefined => {
|
|
92
|
+
const entries = Object.entries(content ?? {});
|
|
93
|
+
return entries.find(([type]) => type.includes("json")) ?? entries[0];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Typed field inputs when the body schema is a flat object of primitives —
|
|
98
|
+
* anything nested (object/array properties) falls back to the raw JSON editor,
|
|
99
|
+
* where structure is easier to edit than in exploded form fields.
|
|
100
|
+
*/
|
|
101
|
+
const bodyFields = (
|
|
102
|
+
schema: SchemaLike | undefined,
|
|
103
|
+
schemas: Record<string, SchemaLike>,
|
|
104
|
+
example: SpecValue
|
|
105
|
+
): PlaygroundBodyField[] | undefined => {
|
|
106
|
+
const resolved = resolveSchema(schemas, schema);
|
|
107
|
+
if (declaredTypes(resolved.type).some((type) => type !== "object")) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const { properties, required } = objectProperties(resolved, schemas);
|
|
111
|
+
if (properties.length === 0) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const defaults = isExampleObject(example) ? example : undefined;
|
|
115
|
+
const fields: PlaygroundBodyField[] = [];
|
|
116
|
+
for (const [name, property] of properties) {
|
|
117
|
+
const propertySchema = resolveSchema(schemas, property);
|
|
118
|
+
const type = scalarType(property, schemas);
|
|
119
|
+
if (
|
|
120
|
+
!(type in PRIMITIVE_TYPES) ||
|
|
121
|
+
propertySchema.properties ||
|
|
122
|
+
propertySchema.items
|
|
123
|
+
) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
fields.push({
|
|
127
|
+
description: propertySchema.description,
|
|
128
|
+
enum: propertySchema.enum?.map(String),
|
|
129
|
+
name,
|
|
130
|
+
required: required.has(name),
|
|
131
|
+
type,
|
|
132
|
+
value: inputValue(defaults?.[name]),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return fields;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** The playground's body editor state, when the operation takes a request body. */
|
|
139
|
+
const modelBody = (
|
|
140
|
+
requestBody: { content?: Record<string, MediaTypeLike> } | undefined,
|
|
141
|
+
schemas: Record<string, SchemaLike>
|
|
142
|
+
): PlaygroundBody | undefined => {
|
|
143
|
+
const media = jsonContentType(requestBody?.content);
|
|
144
|
+
if (!media) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const [contentType, mediaType] = media;
|
|
148
|
+
// SAFETY: `example` comes from the parsed spec document (YAML/JSON), whose
|
|
149
|
+
// values are exactly the JSON-shaped tree `SpecValue` models.
|
|
150
|
+
const exampleData =
|
|
151
|
+
(mediaType.example as SpecValue) ?? exampleValue(mediaType.schema, schemas);
|
|
152
|
+
return {
|
|
153
|
+
contentType,
|
|
154
|
+
example: toJson(exampleData) ?? "",
|
|
155
|
+
fields: bodyFields(mediaType.schema, schemas, exampleData),
|
|
156
|
+
schema: validationSchema(mediaType.schema, schemas),
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const AUTHORIZATION_HEADER = { in: "header", name: "Authorization" } as const;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* One resolved security scheme -> the playground input that collects its
|
|
164
|
+
* credential. Mutual TLS travels outside the request and an unknown ref can't
|
|
165
|
+
* be guessed — both contribute nothing.
|
|
166
|
+
*/
|
|
167
|
+
const authInput = (
|
|
168
|
+
resolved: ResolvedScheme
|
|
169
|
+
): PlaygroundAuthInput | undefined => {
|
|
170
|
+
const { scheme } = resolved;
|
|
171
|
+
const label = schemeLabel(resolved);
|
|
172
|
+
switch (scheme?.type) {
|
|
173
|
+
case "http": {
|
|
174
|
+
const kind = (scheme.scheme ?? "bearer").toLowerCase();
|
|
175
|
+
if (kind === "basic") {
|
|
176
|
+
return {
|
|
177
|
+
carrier: AUTHORIZATION_HEADER,
|
|
178
|
+
id: resolved.key,
|
|
179
|
+
kind: "basic",
|
|
180
|
+
label,
|
|
181
|
+
placeholder: "YOUR_CREDENTIALS",
|
|
182
|
+
prefix: "Basic ",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (kind === "bearer") {
|
|
186
|
+
return {
|
|
187
|
+
carrier: AUTHORIZATION_HEADER,
|
|
188
|
+
id: resolved.key,
|
|
189
|
+
kind: "bearer",
|
|
190
|
+
label,
|
|
191
|
+
placeholder: "YOUR_TOKEN",
|
|
192
|
+
prefix: "Bearer ",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Digest and friends: a paste field like bearer, scheme-name prefix.
|
|
196
|
+
return {
|
|
197
|
+
carrier: AUTHORIZATION_HEADER,
|
|
198
|
+
id: resolved.key,
|
|
199
|
+
kind: "bearer",
|
|
200
|
+
label,
|
|
201
|
+
placeholder: "YOUR_CREDENTIALS",
|
|
202
|
+
prefix: `${kind.charAt(0).toUpperCase() + kind.slice(1)} `,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
case "oauth2":
|
|
206
|
+
case "openIdConnect": {
|
|
207
|
+
// Token paste, no flow — the playground doesn't run OAuth dances.
|
|
208
|
+
return {
|
|
209
|
+
carrier: AUTHORIZATION_HEADER,
|
|
210
|
+
id: resolved.key,
|
|
211
|
+
kind: "oauth2",
|
|
212
|
+
label,
|
|
213
|
+
placeholder: "YOUR_ACCESS_TOKEN",
|
|
214
|
+
prefix: "Bearer ",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
case "apiKey": {
|
|
218
|
+
const where = scheme.in;
|
|
219
|
+
return {
|
|
220
|
+
carrier: {
|
|
221
|
+
in: where === "query" || where === "cookie" ? where : "header",
|
|
222
|
+
name: scheme.name ?? resolved.key,
|
|
223
|
+
},
|
|
224
|
+
id: resolved.key,
|
|
225
|
+
kind: "apiKey",
|
|
226
|
+
label,
|
|
227
|
+
placeholder: "YOUR_API_KEY",
|
|
228
|
+
prefix: "",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
default: {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Derive the playground request model for one operation, at build time. */
|
|
238
|
+
export const operationModel = (args: {
|
|
239
|
+
method: string;
|
|
240
|
+
path: string;
|
|
241
|
+
/** Pre-merged/resolved (`mergeParameters` output). */
|
|
242
|
+
parameters: ParameterLike[];
|
|
243
|
+
requestBody?: { content?: Record<string, MediaTypeLike> };
|
|
244
|
+
servers: { url?: string }[];
|
|
245
|
+
schemas: Record<string, SchemaLike>;
|
|
246
|
+
security: OperationSecurity;
|
|
247
|
+
}): PlaygroundModel => ({
|
|
248
|
+
// First alternative only — the spec's preferred way to authorize, matching
|
|
249
|
+
// what the static samples always showed.
|
|
250
|
+
auth: (args.security.alternatives[0] ?? []).flatMap((resolved) => {
|
|
251
|
+
const input = authInput(resolved);
|
|
252
|
+
return input ? [input] : [];
|
|
253
|
+
}),
|
|
254
|
+
authOptional: args.security.optional,
|
|
255
|
+
body: modelBody(args.requestBody, args.schemas),
|
|
256
|
+
method: args.method.toUpperCase(),
|
|
257
|
+
params: modelParams(args.parameters, args.schemas),
|
|
258
|
+
path: args.path,
|
|
259
|
+
servers: args.servers.map((server) => server.url ?? ""),
|
|
260
|
+
});
|