@yaakdoc/snippets 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 József Sallai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,44 @@
1
- # Temporary Holding Version
1
+ # @yaakdoc/snippets
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Code example generators for [yaakdoc](https://github.com/jozsefsallai/yaakdoc#readme).
4
+
5
+ | Id | Language | Request kinds |
6
+ | --- | --- | --- |
7
+ | `curl` | bash | HTTP |
8
+ | `javascript` | JavaScript (`fetch`, `WebSocket`) | HTTP, WebSocket |
9
+ | `python` | Python (`requests`) | HTTP |
10
+ | `go` | Go (`net/http`) | HTTP |
11
+ | `grpcurl` | bash | gRPC |
12
+ | `wscat` | bash | WebSocket |
13
+
14
+ The generators handle JSON, text, form, multipart, GraphQL and binary bodies, headers, path and query parameters, and authentication: basic, bearer, API key (header or query), plus placeholders for OAuth and JWT.
15
+
16
+ ## Custom snippets
17
+
18
+ ```ts
19
+ // snippets/httpie.ts
20
+ import { applyAuth } from "@yaakdoc/core";
21
+ import { defineSnippet } from "@yaakdoc/snippets";
22
+
23
+ export default defineSnippet({
24
+ id: "httpie",
25
+ label: "HTTPie",
26
+ language: "bash",
27
+ supports: ["http"],
28
+ generate(request) {
29
+ if (request.kind !== "http") {
30
+ return "";
31
+ }
32
+
33
+ const req = applyAuth(request);
34
+
35
+ return [`http ${req.method} '${req.url}'`, ...req.headers.map((h) => `'${h.name}:${h.value}'`)].join(" \\\n ");
36
+ },
37
+ });
38
+ ```
39
+
40
+ ```json
41
+ { "snippets": ["curl", "./snippets/httpie.ts", "python"] }
42
+ ```
43
+
44
+ The order of `snippets` is the order of the tabs. Use `generateExamples(resolvedRequest, snippets)` to generate examples programmatically.
@@ -0,0 +1,50 @@
1
+ import { RequestKind, ResolvedRequest } from "@yaakdoc/core";
2
+ //#region src/generators/curl.d.ts
3
+ export declare const curl: Snippet;
4
+ //#endregion
5
+ //#region src/generators/go.d.ts
6
+ export declare const go: Snippet;
7
+ //#endregion
8
+ //#region src/generators/grpcurl.d.ts
9
+ export declare const grpcurl: Snippet;
10
+ //#endregion
11
+ //#region src/generators/javascript.d.ts
12
+ export declare const javascript: Snippet;
13
+ //#endregion
14
+ //#region src/generators/python.d.ts
15
+ /** Serializes a JSON value as a Python literal. */
16
+ export declare function toPython(value: unknown, level?: number): string;
17
+ export declare const python: Snippet;
18
+ //#endregion
19
+ //#region src/generators/wscat.d.ts
20
+ export declare const wscat: Snippet;
21
+ //#endregion
22
+ //#region src/types.d.ts
23
+ type Snippet = {
24
+ /** Unique id, referenced from `snippets` in the yaakdoc config. */
25
+ id: string;
26
+ /** Human readable label, e.g. "cURL". */
27
+ label: string;
28
+ /** Shiki language id used for syntax highlighting. */
29
+ language: string;
30
+ /** Request kinds this snippet can generate code for. */
31
+ supports: readonly RequestKind[];
32
+ generate(request: ResolvedRequest): string;
33
+ };
34
+ type GeneratedExample = {
35
+ id: string;
36
+ label: string;
37
+ language: string;
38
+ code: string;
39
+ };
40
+ //#endregion
41
+ //#region src/define.d.ts
42
+ /** Defines a code example generator. Identity function for type inference. */
43
+ export declare function defineSnippet(snippet: Snippet): Snippet;
44
+ //#endregion
45
+ //#region src/index.d.ts
46
+ export declare const builtinSnippets: Readonly<Record<string, Snippet>>;
47
+ /** Generates code examples for a request with every applicable snippet. */
48
+ export declare function generateExamples(request: ResolvedRequest, snippets: readonly Snippet[]): GeneratedExample[];
49
+ //#endregion
50
+ export type { GeneratedExample, Snippet };
package/dist/index.js ADDED
@@ -0,0 +1,492 @@
1
+ import { applyAuth, grpcMethodPath, tryParseJson } from "@yaakdoc/core";
2
+ //#region src/define.ts
3
+ /** Defines a code example generator. Identity function for type inference. */
4
+ function defineSnippet(snippet) {
5
+ return snippet;
6
+ }
7
+ //#endregion
8
+ //#region src/utils.ts
9
+ /** Quotes a string for POSIX shells using single quotes. */
10
+ function shellQuote(value) {
11
+ if (value !== "" && /^[\w@%+=:,./-]+$/.test(value)) return value;
12
+ return `'${value.replace(/'/g, `'\\''`)}'`;
13
+ }
14
+ /** Returns the content type implied by a body, if any. */
15
+ function bodyContentType(body) {
16
+ return body.type === "none" || body.type === "multipart" ? null : body.contentType;
17
+ }
18
+ /** Adds a Content-Type header for the body unless one is already present. */
19
+ function withContentType(headers, body) {
20
+ const contentType = bodyContentType(body);
21
+ if (!contentType || headers.some((h) => h.name.toLowerCase() === "content-type")) return headers;
22
+ return [...headers, {
23
+ name: "Content-Type",
24
+ value: contentType
25
+ }];
26
+ }
27
+ /** GraphQL bodies are sent as JSON `{ query, variables, operationName }`. */
28
+ function graphqlPayload(body, query = graphqlQuery(body)) {
29
+ const variables = graphqlVariables(body);
30
+ return {
31
+ query,
32
+ ...variables !== void 0 ? { variables } : {},
33
+ ...body.operationName ? { operationName: body.operationName } : {}
34
+ };
35
+ }
36
+ /** Variables as a value (parsed JSON), the raw text when it isn't JSON, or `undefined`. */
37
+ function graphqlVariables(body) {
38
+ if (!body.variables?.trim()) return;
39
+ const parsed = tryParseJson(body.variables);
40
+ return parsed.ok ? parsed.value : body.variables;
41
+ }
42
+ /** The query with common indentation and surrounding blank lines removed. */
43
+ function graphqlQuery(body) {
44
+ const lines = body.query.replace(/\r\n?/g, "\n").split("\n");
45
+ while (lines.length > 0 && lines[0].trim() === "") lines.shift();
46
+ while (lines.length > 0 && lines.at(-1).trim() === "") lines.pop();
47
+ const indents = lines.filter((line) => line.trim()).map((line) => line.match(/^\s*/)[0].length);
48
+ const common = indents.length > 0 ? Math.min(...indents) : 0;
49
+ return lines.map((line) => line.slice(common).trimEnd()).join("\n");
50
+ }
51
+ /**
52
+ * Collapses a GraphQL document onto one readable line (for JSON bodies in shell commands):
53
+ * comments are removed and whitespace outside strings is collapsed, e.g.
54
+ * `mutation x($id: ID!) { node(id: $id) { id } }`.
55
+ */
56
+ function minifyGraphql(query) {
57
+ let out = "";
58
+ let inString = false;
59
+ for (let i = 0; i < query.length; i++) {
60
+ const char = query[i];
61
+ if (inString) {
62
+ out += char;
63
+ if (char === "\\") out += query[++i] ?? "";
64
+ else if (char === "\"") inString = false;
65
+ } else if (char === "#") while (i + 1 < query.length && query[i + 1] !== "\n") i++;
66
+ else if (/\s/.test(char)) {
67
+ if (!out.endsWith(" ")) out += " ";
68
+ } else {
69
+ if (char === "\"") inString = true;
70
+ out += char;
71
+ }
72
+ }
73
+ return out.replace(/\( /g, "(").replace(/ \)/g, ")").trim();
74
+ }
75
+ /** Body as a single string, for bodies that can be represented as one. */
76
+ function bodyText(body) {
77
+ switch (body.type) {
78
+ case "json":
79
+ case "text": return body.text;
80
+ case "form-urlencoded": return body.fields.map((field) => `${encodeURIComponent(field.name)}=${encodeURIComponent(field.value)}`).join("&");
81
+ default: return null;
82
+ }
83
+ }
84
+ function indent(text, prefix) {
85
+ return text.split("\n").map((line, index) => index === 0 ? line : prefix + line).join("\n");
86
+ }
87
+ /** Prefixes every non-empty line. */
88
+ function indentLines(text, prefix) {
89
+ return text.split("\n").map((line) => line ? prefix + line : line).join("\n");
90
+ }
91
+ //#endregion
92
+ //#region src/generators/curl.ts
93
+ const curl = defineSnippet({
94
+ id: "curl",
95
+ label: "cURL",
96
+ language: "bash",
97
+ supports: ["http"],
98
+ generate(request) {
99
+ if (request.kind !== "http") return "";
100
+ const basic = request.auth?.type === "basic" ? request.auth : null;
101
+ const req = basic ? {
102
+ ...request,
103
+ auth: null
104
+ } : applyAuth(request);
105
+ const parts = [];
106
+ parts.push(req.method === "GET" && req.body.type === "none" ? `curl ${shellQuote(req.url)}` : `curl -X ${req.method} ${shellQuote(req.url)}`);
107
+ for (const header of withContentType(req.headers, req.body)) parts.push(`-H ${shellQuote(`${header.name}: ${header.value}`)}`);
108
+ if (basic) parts.push(`-u ${shellQuote(`${basic.username}:${basic.password}`)}`);
109
+ const { body } = req;
110
+ switch (body.type) {
111
+ case "json":
112
+ case "text":
113
+ if (body.text) parts.push(`--data-raw ${shellQuote(body.text)}`);
114
+ break;
115
+ case "graphql": {
116
+ const payload = graphqlPayload(body, minifyGraphql(graphqlQuery(body)));
117
+ parts.push(`--data-raw ${shellQuote(JSON.stringify(payload, null, 2))}`);
118
+ break;
119
+ }
120
+ case "form-urlencoded":
121
+ for (const field of body.fields) parts.push(`--data-urlencode ${shellQuote(`${field.name}=${field.value}`)}`);
122
+ break;
123
+ case "multipart":
124
+ for (const field of body.fields) {
125
+ const value = field.file ? `@${field.file}${field.contentType ? `;type=${field.contentType}` : ""}` : field.value;
126
+ parts.push(`-F ${shellQuote(`${field.name}=${value}`)}`);
127
+ }
128
+ break;
129
+ case "binary": parts.push(`--data-binary ${shellQuote(`@${body.filePath ?? "file"}`)}`);
130
+ }
131
+ return parts.join(" \\\n ");
132
+ }
133
+ });
134
+ //#endregion
135
+ //#region src/generators/go.ts
136
+ const goStr = (value) => value.includes("`") || !value.includes("\n") ? JSON.stringify(value) : `\`${value}\``;
137
+ const go = defineSnippet({
138
+ id: "go",
139
+ label: "Go",
140
+ language: "go",
141
+ supports: ["http"],
142
+ generate(request) {
143
+ if (request.kind !== "http") return "";
144
+ const basic = request.auth?.type === "basic" ? request.auth : null;
145
+ const req = basic ? {
146
+ ...request,
147
+ auth: null
148
+ } : applyAuth(request);
149
+ const imports = /* @__PURE__ */ new Set([
150
+ "fmt",
151
+ "io",
152
+ "net/http"
153
+ ]);
154
+ const setup = [];
155
+ let bodyVar = "nil";
156
+ const headers = withContentType(req.headers, req.body);
157
+ const { body } = req;
158
+ if (body.type === "multipart") {
159
+ imports.add("bytes");
160
+ imports.add("mime/multipart");
161
+ setup.push(" body := &bytes.Buffer{}", " writer := multipart.NewWriter(body)");
162
+ for (const field of body.fields) if (field.file) {
163
+ imports.add("os");
164
+ setup.push(`\tfile, _ := os.Open(${goStr(field.file)})`, " defer file.Close()", `\tpart, _ := writer.CreateFormFile(${goStr(field.name)}, ${goStr(field.file.split("/").at(-1) ?? field.file)})`, " io.Copy(part, file)");
165
+ } else setup.push(`\twriter.WriteField(${goStr(field.name)}, ${goStr(field.value)})`);
166
+ setup.push(" writer.Close()", "");
167
+ bodyVar = "body";
168
+ headers.push({
169
+ name: "Content-Type",
170
+ value: "__MULTIPART__"
171
+ });
172
+ } else if (body.type === "binary") {
173
+ imports.add("os");
174
+ setup.push(`\tbody, _ := os.Open(${goStr(body.filePath ?? "file")})`, " defer body.Close()", "");
175
+ bodyVar = "body";
176
+ } else if (body.type === "graphql") {
177
+ imports.add("bytes");
178
+ imports.add("encoding/json");
179
+ const query = graphqlQuery(body);
180
+ const variables = graphqlVariables(body);
181
+ const fields = [["query", "query"]];
182
+ setup.push(query.includes("`") ? `\tquery := ${JSON.stringify(query)}` : `\tquery := \`\n${indentLines(query, " ")}\n\t\``);
183
+ if (variables !== void 0) {
184
+ const json = JSON.stringify(variables, null, " ");
185
+ setup.push(json.includes("`") ? `\tvariables := json.RawMessage(${JSON.stringify(json)})` : `\tvariables := json.RawMessage(\`${indent(json, " ")}\`)`);
186
+ fields.push(["variables", "variables"]);
187
+ }
188
+ if (body.operationName) fields.push(["operationName", goStr(body.operationName)]);
189
+ const width = Math.max(...fields.map(([key]) => key.length)) + 3;
190
+ setup.push("", " payload, _ := json.Marshal(map[string]any{", ...fields.map(([key, value]) => `\t\t${`"${key}":`.padEnd(width)} ${value},`), " })", "");
191
+ bodyVar = "bytes.NewReader(payload)";
192
+ } else {
193
+ const text = bodyText(body);
194
+ if (text) {
195
+ imports.add("strings");
196
+ setup.push(`\tbody := strings.NewReader(${goStr(text)})`, "");
197
+ bodyVar = "body";
198
+ }
199
+ }
200
+ return [
201
+ "package main",
202
+ "",
203
+ `import (\n${[...imports].sort().map((i) => `\t"${i}"`).join("\n")}\n)`,
204
+ "",
205
+ "func main() {",
206
+ ...setup,
207
+ `\treq, err := http.NewRequest(${goStr(req.method)}, ${goStr(req.url)}, ${bodyVar})`,
208
+ " if err != nil {",
209
+ " panic(err)",
210
+ " }",
211
+ ...headers.map((h) => h.value === "__MULTIPART__" ? " req.Header.Set(\"Content-Type\", writer.FormDataContentType())" : `\treq.Header.Set(${goStr(h.name)}, ${goStr(h.value)})`),
212
+ ...basic ? [`\treq.SetBasicAuth(${goStr(basic.username)}, ${goStr(basic.password)})`] : [],
213
+ "",
214
+ " res, err := http.DefaultClient.Do(req)",
215
+ " if err != nil {",
216
+ " panic(err)",
217
+ " }",
218
+ " defer res.Body.Close()",
219
+ "",
220
+ " data, _ := io.ReadAll(res.Body)",
221
+ " fmt.Println(string(data))",
222
+ "}"
223
+ ].join("\n");
224
+ }
225
+ });
226
+ //#endregion
227
+ //#region src/generators/grpcurl.ts
228
+ const grpcurl = defineSnippet({
229
+ id: "grpcurl",
230
+ label: "grpcurl",
231
+ language: "bash",
232
+ supports: ["grpc"],
233
+ generate(request) {
234
+ if (request.kind !== "grpc") return "";
235
+ const plaintext = request.url.startsWith("http://") || request.url.startsWith("grpc://");
236
+ const address = request.url.replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "") || "localhost:443";
237
+ const parts = ["grpcurl"];
238
+ if (plaintext) parts.push("-plaintext");
239
+ const metadata = [...request.metadata];
240
+ const { auth } = request;
241
+ if (auth?.type === "bearer") metadata.push({
242
+ name: "authorization",
243
+ value: `${auth.prefix} ${auth.token}`
244
+ });
245
+ else if (auth?.type === "apikey") metadata.push({
246
+ name: auth.key,
247
+ value: auth.value
248
+ });
249
+ else if (auth?.type === "other" && auth.header) metadata.push(auth.header);
250
+ for (const entry of metadata) parts.push(`-H ${shellQuote(`${entry.name}: ${entry.value}`)}`);
251
+ if (request.message.trim()) parts.push(`-d ${shellQuote(request.message.trim())}`);
252
+ parts.push(address);
253
+ const method = grpcMethodPath(request);
254
+ if (method) parts.push(method);
255
+ return parts.join(" \\\n ");
256
+ }
257
+ });
258
+ //#endregion
259
+ //#region src/generators/javascript.ts
260
+ const str = (value) => JSON.stringify(value);
261
+ const objectLiteral = (entries, pad) => entries.length === 0 ? "{}" : `{\n${entries.map(([k, v]) => `${pad} ${str(k)}: ${str(v)},`).join("\n")}\n${pad}}`;
262
+ const javascript = defineSnippet({
263
+ id: "javascript",
264
+ label: "JavaScript",
265
+ language: "javascript",
266
+ supports: ["http", "websocket"],
267
+ generate(request) {
268
+ if (request.kind === "websocket") {
269
+ const req = applyAuth(request);
270
+ const lines = [`const socket = new WebSocket(${str(req.url)});`, ""];
271
+ if (req.headers.length > 0) lines.unshift("// Note: browsers don't support custom WebSocket headers. In Node.js, use the `ws` package:", `// new WebSocket(url, { headers: ${JSON.stringify(Object.fromEntries(req.headers.map((h) => [h.name, h.value])))} })`);
272
+ lines.push("socket.addEventListener(\"open\", () => {", req.message ? ` socket.send(${templateString(req.message)});` : " // Connected", "});", "", "socket.addEventListener(\"message\", (event) => {", " console.log(event.data);", "});");
273
+ return lines.join("\n");
274
+ }
275
+ if (request.kind !== "http") return "";
276
+ if (request.auth?.type === "basic") {
277
+ const { username, password } = request.auth;
278
+ const credentials = str(`${username}:${password}`);
279
+ return fetchSnippet({
280
+ ...request,
281
+ auth: null
282
+ }, credentials);
283
+ }
284
+ return fetchSnippet(applyAuth(request));
285
+ }
286
+ });
287
+ function fetchSnippet(req, basicCredentials) {
288
+ const pre = [];
289
+ const options = [];
290
+ const headerEntries = withContentType(req.headers, req.body).map((h) => ` ${str(h.name)}: ${str(h.value)},`);
291
+ if (basicCredentials) headerEntries.push(` Authorization: \`Basic \${btoa(${basicCredentials})}\`,`);
292
+ if (req.method !== "GET") options.push(`method: ${str(req.method)},`);
293
+ if (headerEntries.length > 0) options.push(`headers: {\n${headerEntries.join("\n")}\n },`);
294
+ const { body } = req;
295
+ switch (body.type) {
296
+ case "json": {
297
+ const parsed = tryParseJson(body.text);
298
+ if (parsed.ok) options.push(`body: JSON.stringify(${indent(JSON.stringify(parsed.value, null, 2), " ")}),`);
299
+ else if (body.text) options.push(`body: ${templateString(body.text)},`);
300
+ break;
301
+ }
302
+ case "text":
303
+ if (body.text) options.push(`body: ${templateString(body.text)},`);
304
+ break;
305
+ case "graphql": {
306
+ pre.push(`const query = /* GraphQL */ \`\n${indentLines(escapeTemplate(graphqlQuery(body)), " ")}\n\`;`, "");
307
+ const variables = graphqlVariables(body);
308
+ const fields = [
309
+ " query,",
310
+ ...variables !== void 0 ? [` variables: ${indent(JSON.stringify(variables, null, 2), " ")},`] : [],
311
+ ...body.operationName ? [` operationName: ${str(body.operationName)},`] : []
312
+ ];
313
+ options.push(`body: JSON.stringify({\n${fields.join("\n")}\n }),`);
314
+ break;
315
+ }
316
+ case "form-urlencoded":
317
+ options.push(`body: new URLSearchParams(${objectLiteral(body.fields.map((f) => [f.name, f.value]), " ")}),`);
318
+ break;
319
+ case "multipart":
320
+ pre.push("const form = new FormData();");
321
+ for (const field of body.fields) pre.push(field.file ? `form.append(${str(field.name)}, fileInput.files[0]); // ${field.file}` : `form.append(${str(field.name)}, ${str(field.value)});`);
322
+ pre.push("");
323
+ options.push("body: form,");
324
+ break;
325
+ case "binary": options.push(`body: file, // ${body.filePath ?? "binary file"}`);
326
+ }
327
+ const call = options.length === 0 ? `const response = await fetch(${str(req.url)});` : `const response = await fetch(${str(req.url)}, {\n${options.map((o) => ` ${o}`).join("\n")}\n});`;
328
+ return [
329
+ ...pre,
330
+ call,
331
+ "",
332
+ "const data = await response.json();",
333
+ "console.log(data);"
334
+ ].join("\n");
335
+ }
336
+ function templateString(value) {
337
+ if (!value.includes("\n")) return str(value);
338
+ return `\`${escapeTemplate(value)}\``;
339
+ }
340
+ /** Escapes text for a JavaScript template literal. */
341
+ function escapeTemplate(value) {
342
+ return value.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
343
+ }
344
+ //#endregion
345
+ //#region src/generators/python.ts
346
+ const pyStr = (value) => JSON.stringify(value);
347
+ /** Serializes a JSON value as a Python literal. */
348
+ function toPython(value, level = 0) {
349
+ const pad = " ".repeat(level);
350
+ const inner = " ".repeat(level + 1);
351
+ if (value === null || value === void 0) return "None";
352
+ if (value === true) return "True";
353
+ if (value === false) return "False";
354
+ if (typeof value === "number") return String(value);
355
+ if (typeof value === "string") return pyStr(value);
356
+ if (Array.isArray(value)) {
357
+ if (value.length === 0) return "[]";
358
+ return `[\n${value.map((item) => `${inner}${toPython(item, level + 1)},`).join("\n")}\n${pad}]`;
359
+ }
360
+ const entries = Object.entries(value);
361
+ if (entries.length === 0) return "{}";
362
+ return `{\n${entries.map(([k, v]) => `${inner}${pyStr(k)}: ${toPython(v, level + 1)},`).join("\n")}\n${pad}}`;
363
+ }
364
+ const python = defineSnippet({
365
+ id: "python",
366
+ label: "Python",
367
+ language: "python",
368
+ supports: ["http"],
369
+ generate(request) {
370
+ if (request.kind !== "http") return "";
371
+ const basic = request.auth?.type === "basic" ? request.auth : null;
372
+ const req = basic ? {
373
+ ...request,
374
+ auth: null
375
+ } : applyAuth(request);
376
+ const lines = [
377
+ "import requests",
378
+ "",
379
+ `url = ${pyStr(req.url)}`
380
+ ];
381
+ const args = ["url"];
382
+ const { body } = req;
383
+ const headers = withContentType(req.headers, body).filter((h) => !((body.type === "json" || body.type === "graphql") && h.name.toLowerCase() === "content-type" && h.value === "application/json"));
384
+ if (headers.length > 0) {
385
+ lines.push(`headers = ${toPython(Object.fromEntries(headers.map((h) => [h.name, h.value])))}`);
386
+ args.push("headers=headers");
387
+ }
388
+ switch (body.type) {
389
+ case "json": {
390
+ const parsed = tryParseJson(body.text);
391
+ if (parsed.ok) {
392
+ lines.push(`payload = ${toPython(parsed.value)}`);
393
+ args.push("json=payload");
394
+ } else if (body.text) {
395
+ lines.push(`payload = ${pyStr(body.text)}`);
396
+ args.push("data=payload");
397
+ }
398
+ break;
399
+ }
400
+ case "graphql": {
401
+ const variables = graphqlVariables(body);
402
+ const query = graphqlQuery(body).replace(/\\/g, "\\\\").replace(/"""/g, "\\\"\\\"\\\"");
403
+ lines.push("", `query = """\n${query}\n"""`, "", [
404
+ "payload = {",
405
+ " \"query\": query,",
406
+ ...variables !== void 0 ? [` "variables": ${toPython(variables, 1)},`] : [],
407
+ ...body.operationName ? [` "operationName": ${pyStr(body.operationName)},`] : [],
408
+ "}"
409
+ ].join("\n"));
410
+ args.push("json=payload");
411
+ break;
412
+ }
413
+ case "text":
414
+ if (body.text) {
415
+ lines.push(`payload = ${pyStr(body.text)}`);
416
+ args.push("data=payload");
417
+ }
418
+ break;
419
+ case "form-urlencoded":
420
+ lines.push(`payload = ${toPython(Object.fromEntries(body.fields.map((f) => [f.name, f.value])))}`);
421
+ args.push("data=payload");
422
+ break;
423
+ case "multipart": {
424
+ const files = body.fields.filter((f) => f.file);
425
+ const data = body.fields.filter((f) => !f.file);
426
+ if (data.length > 0) {
427
+ lines.push(`payload = ${toPython(Object.fromEntries(data.map((f) => [f.name, f.value])))}`);
428
+ args.push("data=payload");
429
+ }
430
+ if (files.length > 0) {
431
+ lines.push(`files = {\n${files.map((f) => ` ${pyStr(f.name)}: open(${pyStr(f.file)}, "rb"),`).join("\n")}\n}`);
432
+ args.push("files=files");
433
+ }
434
+ break;
435
+ }
436
+ case "binary":
437
+ lines.push(`payload = open(${pyStr(body.filePath ?? "file")}, "rb")`);
438
+ args.push("data=payload");
439
+ }
440
+ if (basic) args.push(`auth=(${pyStr(basic.username)}, ${pyStr(basic.password)})`);
441
+ const method = req.method.toLowerCase();
442
+ const call = [
443
+ "get",
444
+ "post",
445
+ "put",
446
+ "patch",
447
+ "delete",
448
+ "head",
449
+ "options"
450
+ ].includes(method) ? `requests.${method}(${args.join(", ")})` : `requests.request(${pyStr(req.method)}, ${args.join(", ")})`;
451
+ lines.push("", `response = ${call}`, "print(response.json())");
452
+ return lines.join("\n");
453
+ }
454
+ });
455
+ //#endregion
456
+ //#region src/generators/wscat.ts
457
+ const wscat = defineSnippet({
458
+ id: "wscat",
459
+ label: "wscat",
460
+ language: "bash",
461
+ supports: ["websocket"],
462
+ generate(request) {
463
+ if (request.kind !== "websocket") return "";
464
+ const req = applyAuth(request);
465
+ const parts = [`wscat -c ${shellQuote(req.url)}`];
466
+ for (const header of req.headers) parts.push(`-H ${shellQuote(`${header.name}: ${header.value}`)}`);
467
+ const command = parts.join(" \\\n ");
468
+ if (!req.message) return command;
469
+ return `${command}\n\n# Then send:\n# ${req.message.split("\n").join("\n# ")}`;
470
+ }
471
+ });
472
+ //#endregion
473
+ //#region src/index.ts
474
+ const builtinSnippets = Object.freeze({
475
+ curl,
476
+ javascript,
477
+ python,
478
+ go,
479
+ grpcurl,
480
+ wscat
481
+ });
482
+ /** Generates code examples for a request with every applicable snippet. */
483
+ function generateExamples(request, snippets) {
484
+ return snippets.filter((snippet) => snippet.supports.includes(request.kind)).map((snippet) => ({
485
+ id: snippet.id,
486
+ label: snippet.label,
487
+ language: snippet.language,
488
+ code: snippet.generate(request)
489
+ })).filter((example) => example.code.trim() !== "");
490
+ }
491
+ //#endregion
492
+ export { builtinSnippets, curl, defineSnippet, generateExamples, go, grpcurl, javascript, python, toPython, wscat };
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@yaakdoc/snippets",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Code example generators (cURL, fetch, Python, Go, grpcurl, wscat) for yaakdoc.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "@yaakdoc/source": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "sideEffects": false,
17
+ "keywords": [
18
+ "yaak",
19
+ "yaakdoc",
20
+ "code",
21
+ "snippets",
22
+ "curl",
23
+ "examples"
24
+ ],
25
+ "author": "József Sallai @jozsefsallai",
26
+ "license": "MIT",
27
+ "files": [
28
+ "/dist",
29
+ "/README.md",
30
+ "/LICENSE"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/jozsefsallai/yaakdoc.git",
35
+ "directory": "packages/snippets"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/jozsefsallai/yaakdoc/issues"
39
+ },
40
+ "homepage": "https://github.com/jozsefsallai/yaakdoc#readme",
41
+ "engines": {
42
+ "node": ">=20.19"
43
+ },
44
+ "devDependencies": {
45
+ "@repo/tsconfig": "0.0.0"
46
+ },
47
+ "dependencies": {
48
+ "@yaakdoc/core": "^0.1.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsdown",
52
+ "dev": "tsdown --watch --no-clean",
53
+ "test": "vitest run",
54
+ "check-types": "tsc",
55
+ "lint": "biome lint .",
56
+ "lint:fix": "biome lint . --write",
57
+ "format": "biome format . --write"
58
+ }
6
59
  }