@yaakdoc/snippets 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +43 -2
- package/dist/index.d.ts +50 -0
- package/dist/index.js +492 -0
- package/package.json +56 -3
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
|
-
#
|
|
1
|
+
# @yaakdoc/snippets
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.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.2.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
|
}
|