@scalar/snippetz 0.9.11 → 0.9.13

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.
Files changed (34) hide show
  1. package/dist/httpsnippet-lite/helpers/shell.d.ts +0 -1
  2. package/dist/httpsnippet-lite/helpers/shell.d.ts.map +1 -1
  3. package/dist/httpsnippet-lite/helpers/shell.js +0 -1
  4. package/dist/plugins/clojure/clj_http/clj_http.d.ts.map +1 -1
  5. package/dist/plugins/clojure/clj_http/clj_http.js +197 -5
  6. package/dist/plugins/csharp/restsharp/restsharp.d.ts.map +1 -1
  7. package/dist/plugins/csharp/restsharp/restsharp.js +134 -5
  8. package/dist/plugins/kotlin/okhttp/okhttp.d.ts.map +1 -1
  9. package/dist/plugins/kotlin/okhttp/okhttp.js +65 -5
  10. package/dist/plugins/objc/nsurlsession/nsurlsession.d.ts.map +1 -1
  11. package/dist/plugins/objc/nsurlsession/nsurlsession.js +124 -5
  12. package/dist/plugins/shell/curl/curl.d.ts.map +1 -1
  13. package/dist/plugins/shell/curl/curl.js +8 -5
  14. package/dist/plugins/shell/wget/wget.d.ts.map +1 -1
  15. package/dist/plugins/shell/wget/wget.js +103 -5
  16. package/package.json +3 -3
  17. package/dist/httpsnippet-lite/targets/clojure/clj_http/client.d.ts +0 -12
  18. package/dist/httpsnippet-lite/targets/clojure/clj_http/client.d.ts.map +0 -1
  19. package/dist/httpsnippet-lite/targets/clojure/clj_http/client.js +0 -186
  20. package/dist/httpsnippet-lite/targets/csharp/restsharp/client.d.ts +0 -3
  21. package/dist/httpsnippet-lite/targets/csharp/restsharp/client.d.ts.map +0 -1
  22. package/dist/httpsnippet-lite/targets/csharp/restsharp/client.js +0 -36
  23. package/dist/httpsnippet-lite/targets/kotlin/okhttp/client.d.ts +0 -12
  24. package/dist/httpsnippet-lite/targets/kotlin/okhttp/client.d.ts.map +0 -1
  25. package/dist/httpsnippet-lite/targets/kotlin/okhttp/client.js +0 -87
  26. package/dist/httpsnippet-lite/targets/objc/helpers.d.ts +0 -21
  27. package/dist/httpsnippet-lite/targets/objc/helpers.d.ts.map +0 -1
  28. package/dist/httpsnippet-lite/targets/objc/helpers.js +0 -57
  29. package/dist/httpsnippet-lite/targets/objc/nsurlsession/client.d.ts +0 -12
  30. package/dist/httpsnippet-lite/targets/objc/nsurlsession/client.d.ts.map +0 -1
  31. package/dist/httpsnippet-lite/targets/objc/nsurlsession/client.js +0 -125
  32. package/dist/httpsnippet-lite/targets/shell/wget/client.d.ts +0 -12
  33. package/dist/httpsnippet-lite/targets/shell/wget/client.d.ts.map +0 -1
  34. package/dist/httpsnippet-lite/targets/shell/wget/client.js +0 -49
@@ -3,5 +3,4 @@
3
3
  * see: http://wiki.bash-hackers.org/syntax/quoting#strong_quoting
4
4
  */
5
5
  export declare const shellQuote: (value?: string) => string;
6
- export declare const shellEscape: (value: string) => string;
7
6
  //# sourceMappingURL=shell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../../src/httpsnippet-lite/helpers/shell.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,cAAU,KAAG,MAQvC,CAAA;AACD,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAA2D,CAAA"}
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../../src/httpsnippet-lite/helpers/shell.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,cAAU,KAAG,MAQvC,CAAA"}
@@ -11,4 +11,3 @@ export const shellQuote = (value = '') => {
11
11
  // if the value is not shell safe, then quote it
12
12
  return `'${value.replace(/'/g, "'\\''")}'`;
13
13
  };
14
- export const shellEscape = (value) => value.replace(/\r/g, '\\r').replace(/\n/g, '\\n');
@@ -1 +1 @@
1
- {"version":3,"file":"clj_http.d.ts","sourceRoot":"","sources":["../../../../src/plugins/clojure/clj_http/clj_http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAKpD;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAQ5B,CAAA"}
1
+ {"version":3,"file":"clj_http.d.ts","sourceRoot":"","sources":["../../../../src/plugins/clojure/clj_http/clj_http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAsGpD;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAiI5B,CAAA"}
@@ -1,5 +1,96 @@
1
- import { clj_http } from '../../../httpsnippet-lite/targets/clojure/clj_http/client.js';
2
- import { convertWithHttpSnippetLite } from '../../../utils/convertWithHttpSnippetLite.js';
1
+ import { normalizeMethod, reduceQueryParams } from '../../../libs/http.js';
2
+ /**
3
+ * Escapes a string so it stays a valid EDN string literal. Backslashes are
4
+ * escaped first, then double quotes, so the two passes do not interfere.
5
+ */
6
+ const escapeEdnString = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
7
+ /**
8
+ * A Clojure keyword (e.g. `:json`) rendered verbatim in EDN.
9
+ */
10
+ class Keyword {
11
+ name;
12
+ constructor(name) {
13
+ this.name = name;
14
+ }
15
+ toString() {
16
+ return `:${this.name}`;
17
+ }
18
+ }
19
+ /**
20
+ * A reference to a file on disk, rendered as a `clojure.java.io/file` call.
21
+ */
22
+ class File {
23
+ path;
24
+ constructor(path) {
25
+ this.path = path;
26
+ }
27
+ toString() {
28
+ return `(clojure.java.io/file "${escapeEdnString(this.path)}")`;
29
+ }
30
+ }
31
+ /** True when the value is a plain object with no own keys. */
32
+ const isEmptyObject = (input) => typeof input === 'object' &&
33
+ input !== null &&
34
+ !Array.isArray(input) &&
35
+ !(input instanceof Keyword) &&
36
+ !(input instanceof File) &&
37
+ Object.keys(input).length === 0;
38
+ /**
39
+ * Drops keys whose values are empty objects so we do not emit things like
40
+ * `:headers {}` for a request without headers.
41
+ */
42
+ const filterEmpty = (input) => {
43
+ for (const key of Object.keys(input)) {
44
+ if (isEmptyObject(input[key])) {
45
+ delete input[key];
46
+ }
47
+ }
48
+ return input;
49
+ };
50
+ /** Indents every line after the first by `padSize` spaces. */
51
+ const padBlock = (padSize, input) => input.replace(/\n/g, `\n${' '.repeat(padSize)}`);
52
+ /**
53
+ * Renders a JavaScript value as an EDN literal, matching clj-http conventions:
54
+ * maps are laid out vertically and vectors horizontally.
55
+ */
56
+ const jsToEdn = (value) => {
57
+ if (value === null || value === undefined) {
58
+ return 'nil';
59
+ }
60
+ if (value instanceof Keyword || value instanceof File) {
61
+ return value.toString();
62
+ }
63
+ if (typeof value === 'string') {
64
+ return `"${escapeEdnString(value)}"`;
65
+ }
66
+ if (Array.isArray(value)) {
67
+ // Simple horizontal format.
68
+ const body = value.reduce((accumulator, item) => `${accumulator} ${jsToEdn(item)}`, '').trim();
69
+ return `[${padBlock(1, body)}]`;
70
+ }
71
+ if (typeof value === 'object') {
72
+ // Simple vertical format, one key per line.
73
+ const body = Object.keys(value)
74
+ .reduce((accumulator, key) => {
75
+ const rendered = padBlock(key.length + 2, jsToEdn(value[key]));
76
+ return `${accumulator}:${key} ${rendered}\n `;
77
+ }, '')
78
+ .trim();
79
+ return `{${padBlock(1, body)}}`;
80
+ }
81
+ // number, boolean
82
+ return String(value);
83
+ };
84
+ /** Case-insensitive lookup of a header name as it was originally cased. */
85
+ const findHeaderName = (headers, name) => Object.keys(headers).find((header) => header.toLowerCase() === name.toLowerCase());
86
+ /** Removes a header (case-insensitive) from the headers map, if present. */
87
+ const deleteHeader = (headers, name) => {
88
+ const header = findHeaderName(headers, name);
89
+ if (header) {
90
+ delete headers[header];
91
+ }
92
+ };
93
+ const SUPPORTED_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
3
94
  /**
4
95
  * clojure/clj_http
5
96
  */
@@ -7,8 +98,109 @@ export const clojureCljhttp = {
7
98
  target: 'clojure',
8
99
  client: 'clj_http',
9
100
  title: 'clj-http',
10
- generate(request) {
11
- // TODO: Write an own converter
12
- return convertWithHttpSnippetLite(clj_http, request);
101
+ generate(request, configuration) {
102
+ const method = normalizeMethod(request?.method).toLowerCase();
103
+ if (!SUPPORTED_METHODS.includes(method)) {
104
+ return 'Method not supported';
105
+ }
106
+ // Parse the URL so we can lift any query string into `:query-params`.
107
+ const urlObject = new URL(request?.url ?? '');
108
+ let url = urlObject.pathname === '/' ? urlObject.origin : urlObject.toString();
109
+ // Collect query parameters from both the URL and the explicit list.
110
+ const queryObj = reduceQueryParams([
111
+ ...Array.from(urlObject.searchParams.entries()).map(([name, value]) => ({ name, value })),
112
+ ...(request?.queryString ?? []),
113
+ ]);
114
+ if (Object.keys(queryObj).length > 0) {
115
+ // clj-http takes care of encoding the query string for us.
116
+ url = url.split('?')[0] ?? url;
117
+ }
118
+ // Reduce headers into a plain object (last value wins for duplicates).
119
+ const headers = (request?.headers ?? []).reduce((accumulator, header) => {
120
+ accumulator[header.name] = header.value ?? '';
121
+ return accumulator;
122
+ }, {});
123
+ // clj-http has no dedicated cookie option, so fold cookies into a single
124
+ // Cookie header, mirroring what the request would send on the wire.
125
+ if (request?.cookies?.length) {
126
+ headers.Cookie = request.cookies
127
+ .map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`)
128
+ .join('; ');
129
+ }
130
+ const params = {
131
+ 'headers': headers,
132
+ 'query-params': queryObj,
133
+ };
134
+ // Basic authentication maps to clj-http's `:basic-auth ["user" "pass"]`.
135
+ if (configuration?.auth?.username && configuration?.auth?.password) {
136
+ params['basic-auth'] = [configuration.auth.username, configuration.auth.password];
137
+ }
138
+ const postData = request?.postData;
139
+ switch (postData?.mimeType) {
140
+ case 'application/json': {
141
+ params['content-type'] = new Keyword('json');
142
+ if (postData.text) {
143
+ try {
144
+ params['form-params'] = JSON.parse(postData.text);
145
+ }
146
+ catch {
147
+ // Preserve the original payload as a raw body when it is not valid JSON.
148
+ params.body = postData.text;
149
+ }
150
+ }
151
+ deleteHeader(headers, 'content-type');
152
+ break;
153
+ }
154
+ case 'application/x-www-form-urlencoded': {
155
+ params['form-params'] = (postData.params ?? []).reduce((accumulator, param) => {
156
+ if (param.name && param.value !== undefined) {
157
+ accumulator[param.name] = param.value;
158
+ }
159
+ return accumulator;
160
+ }, {});
161
+ deleteHeader(headers, 'content-type');
162
+ break;
163
+ }
164
+ case 'multipart/form-data': {
165
+ if (postData.params) {
166
+ params.multipart = postData.params.map((param) =>
167
+ // Reference a file when there is a string fileName and no inline
168
+ // body. A part carrying an actual value (a common HAR file part
169
+ // with body bytes) keeps that value as the content, while an
170
+ // empty or null value still references the file path.
171
+ typeof param.fileName === 'string' && !param.value
172
+ ? { name: param.name, content: new File(param.fileName) }
173
+ : { name: param.name, content: param.value });
174
+ }
175
+ deleteHeader(headers, 'content-type');
176
+ break;
177
+ }
178
+ default: {
179
+ // Everything else (text/plain, octet-stream, …) goes through as a raw body.
180
+ if (postData?.text) {
181
+ params.body = postData.text;
182
+ deleteHeader(headers, 'content-type');
183
+ }
184
+ }
185
+ }
186
+ // clj-http exposes `:accept :json` instead of an Accept header for JSON.
187
+ const acceptHeader = findHeaderName(headers, 'accept');
188
+ if (acceptHeader && headers[acceptHeader] === 'application/json') {
189
+ params.accept = new Keyword('json');
190
+ delete headers[acceptHeader];
191
+ }
192
+ const filteredParams = filterEmpty(params);
193
+ const require = "(require '[clj-http.client :as client])\n";
194
+ // Escape the URL like every other EDN string so backslashes or quotes
195
+ // cannot break out of the literal.
196
+ const escapedUrl = escapeEdnString(url);
197
+ if (isEmptyObject(filteredParams)) {
198
+ return `${require}\n(client/${method} "${escapedUrl}")`;
199
+ }
200
+ // Align the option map under the opening of the call. The padding uses the
201
+ // escaped URL length so the columns line up with what is actually rendered.
202
+ const padding = 11 + method.length + escapedUrl.length;
203
+ const formattedParams = padBlock(padding, jsToEdn(filteredParams));
204
+ return `${require}\n(client/${method} "${escapedUrl}" ${formattedParams})`;
13
205
  },
14
206
  };
@@ -1 +1 @@
1
- {"version":3,"file":"restsharp.d.ts","sourceRoot":"","sources":["../../../../src/plugins/csharp/restsharp/restsharp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAKpD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAQ7B,CAAA"}
1
+ {"version":3,"file":"restsharp.d.ts","sourceRoot":"","sources":["../../../../src/plugins/csharp/restsharp/restsharp.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAmDpD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAkH7B,CAAA"}
@@ -1,5 +1,47 @@
1
- import { restsharp } from '../../../httpsnippet-lite/targets/csharp/restsharp/client.js';
2
- import { convertWithHttpSnippetLite } from '../../../utils/convertWithHttpSnippetLite.js';
1
+ import { parseMimeType } from '@scalar/helpers/http/mime-type';
2
+ import { encode } from 'js-base64';
3
+ import { joinUrlAndQuery } from '../../../libs/http.js';
4
+ /**
5
+ * True for `application/json`, any RFC 6839 `+json` structured-syntax suffix
6
+ * (e.g. `application/vnd.api+json`), and parameterized variants
7
+ * (e.g. `application/json;charset=utf-8`). Case-insensitive.
8
+ */
9
+ const isJsonContentType = (value) => {
10
+ if (!value) {
11
+ return false;
12
+ }
13
+ const { subtype } = parseMimeType(value);
14
+ return subtype === 'json' || subtype.endsWith('+json');
15
+ };
16
+ /**
17
+ * Maps an HTTP method to a RestSharp `Method` enum member. The enum uses
18
+ * PascalCase members (`Method.Get`, `Method.Post`, ...), so we title-case the
19
+ * method name to cover both the well-known verbs and any custom ones.
20
+ */
21
+ const getMethod = (method) => {
22
+ const titleCased = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
23
+ return `Method.${titleCased}`;
24
+ };
25
+ /**
26
+ * Escapes a value for use inside a regular C# double-quoted string literal.
27
+ * Backslashes and double quotes are escaped, and newlines, carriage returns,
28
+ * and tabs are turned into their escape sequences so values stay on a single
29
+ * line and the generated snippet remains valid C#.
30
+ */
31
+ const escapeCSharpString = (text) => text.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
32
+ /**
33
+ * Wraps text in a C# raw string literal (`"""`), growing the delimiter when the
34
+ * payload itself contains a run of quotes. This keeps multi-line JSON bodies
35
+ * readable without escaping every quote.
36
+ */
37
+ const createRawStringLiteral = (text) => {
38
+ let quoteCount = 3;
39
+ while (text.includes('"'.repeat(quoteCount))) {
40
+ quoteCount++;
41
+ }
42
+ const quotes = '"'.repeat(quoteCount);
43
+ return `${quotes}\n${text}\n${quotes}`;
44
+ };
3
45
  /**
4
46
  * csharp/restsharp
5
47
  */
@@ -7,8 +49,95 @@ export const csharpRestsharp = {
7
49
  target: 'csharp',
8
50
  client: 'restsharp',
9
51
  title: 'RestSharp',
10
- generate(request) {
11
- // TODO: Write an own converter
12
- return convertWithHttpSnippetLite(restsharp, request);
52
+ generate(request, configuration) {
53
+ // Defaults
54
+ const normalizedRequest = {
55
+ method: 'GET',
56
+ url: '',
57
+ ...request,
58
+ };
59
+ // Normalization
60
+ normalizedRequest.method = normalizedRequest.method.toUpperCase();
61
+ // Build the full URL, appending the query string with the correct separator
62
+ // (joinUrlAndQuery uses `&` when the URL already carries a query string)
63
+ const url = joinUrlAndQuery(normalizedRequest.url, normalizedRequest.queryString);
64
+ // Derive the host so cookies can be scoped to it (RestSharp requires a domain)
65
+ let host = '';
66
+ try {
67
+ host = new URL(url).host;
68
+ }
69
+ catch {
70
+ // Leave the host empty when the URL cannot be parsed
71
+ }
72
+ const lines = [];
73
+ // Client and request
74
+ lines.push(`var client = new RestClient("${escapeCSharpString(url)}");`);
75
+ lines.push(`var request = new RestRequest("", ${getMethod(normalizedRequest.method)});`);
76
+ // Basic Auth (added as an Authorization header so the client stays request-scoped)
77
+ const { username, password } = configuration?.auth ?? {};
78
+ const hasBasicAuth = Boolean(username && password);
79
+ if (hasBasicAuth) {
80
+ const credentials = encode(`${username}:${password}`);
81
+ lines.push(`request.AddHeader("Authorization", "Basic ${credentials}");`);
82
+ }
83
+ // Headers (skip an existing Authorization header when Basic auth from config takes precedence)
84
+ normalizedRequest.headers?.forEach((header) => {
85
+ if (hasBasicAuth && header.name.toLowerCase() === 'authorization') {
86
+ return;
87
+ }
88
+ lines.push(`request.AddHeader("${escapeCSharpString(header.name)}", "${escapeCSharpString(header.value)}");`);
89
+ });
90
+ // Cookies
91
+ normalizedRequest.cookies?.forEach((cookie) => {
92
+ lines.push(`request.AddCookie("${escapeCSharpString(cookie.name)}", "${escapeCSharpString(cookie.value)}", "/", "${escapeCSharpString(host)}");`);
93
+ });
94
+ // Body
95
+ if (normalizedRequest.postData) {
96
+ const { mimeType, text, params } = normalizedRequest.postData;
97
+ // Compare against the essence so parameterized values (e.g. a `boundary` or
98
+ // `charset`) still match the form, multipart, and octet-stream branches.
99
+ const essence = mimeType ? parseMimeType(mimeType).essence : undefined;
100
+ if (isJsonContentType(mimeType)) {
101
+ if (text) {
102
+ let body = text;
103
+ try {
104
+ body = JSON.stringify(JSON.parse(text), null, 2);
105
+ }
106
+ catch {
107
+ // Fall back to the raw text if it is not valid JSON
108
+ }
109
+ lines.push(`request.AddStringBody(${createRawStringLiteral(body)}, ContentType.Json);`);
110
+ }
111
+ }
112
+ else if (essence === 'application/x-www-form-urlencoded' && params) {
113
+ params.forEach((param) => {
114
+ lines.push(`request.AddParameter("${escapeCSharpString(param.name)}", "${escapeCSharpString(param.value ?? '')}");`);
115
+ });
116
+ }
117
+ else if (essence === 'multipart/form-data' && params) {
118
+ params.forEach((param) => {
119
+ if (param.fileName !== undefined) {
120
+ if (param.contentType) {
121
+ lines.push(`request.AddFile("${escapeCSharpString(param.name)}", "${escapeCSharpString(param.fileName)}", "${escapeCSharpString(param.contentType)}");`);
122
+ }
123
+ else {
124
+ lines.push(`request.AddFile("${escapeCSharpString(param.name)}", "${escapeCSharpString(param.fileName)}");`);
125
+ }
126
+ }
127
+ else {
128
+ lines.push(`request.AddParameter("${escapeCSharpString(param.name)}", "${escapeCSharpString(param.value ?? '')}");`);
129
+ }
130
+ });
131
+ }
132
+ else if (essence === 'application/octet-stream' && text) {
133
+ lines.push(`request.AddParameter("application/octet-stream", "${escapeCSharpString(text)}", ParameterType.RequestBody);`);
134
+ }
135
+ else if (text) {
136
+ lines.push(`request.AddParameter("${escapeCSharpString(mimeType ?? '')}", "${escapeCSharpString(text)}", ParameterType.RequestBody);`);
137
+ }
138
+ }
139
+ // Execute
140
+ lines.push('var response = await client.ExecuteAsync(request);');
141
+ return lines.join('\n');
13
142
  },
14
143
  };
@@ -1 +1 @@
1
- {"version":3,"file":"okhttp.d.ts","sourceRoot":"","sources":["../../../../src/plugins/kotlin/okhttp/okhttp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAKpD;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAQ1B,CAAA"}
1
+ {"version":3,"file":"okhttp.d.ts","sourceRoot":"","sources":["../../../../src/plugins/kotlin/okhttp/okhttp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAcpD;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAqE1B,CAAA"}
@@ -1,5 +1,11 @@
1
- import { okhttp } from '../../../httpsnippet-lite/targets/kotlin/okhttp/client.js';
2
- import { convertWithHttpSnippetLite } from '../../../utils/convertWithHttpSnippetLite.js';
1
+ import { escapeForDoubleQuotes } from '../../../httpsnippet-lite/helpers/escape.js';
2
+ import { collectHeaders, joinUrlAndQuery, normalizeMethod, normalizeUrl } from '../../../libs/http.js';
3
+ /** Methods OkHttp exposes as dedicated builder calls (anything else uses `.method(...)`). */
4
+ const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'];
5
+ /** Methods that accept a request body through their dedicated builder call. */
6
+ const METHODS_WITH_BODY = ['POST', 'PUT', 'DELETE', 'PATCH'];
7
+ /** Wrap a value in double quotes, escaping anything that would break the Kotlin string. */
8
+ const quote = (value) => `"${escapeForDoubleQuotes(value)}"`;
3
9
  /**
4
10
  * kotlin/okhttp
5
11
  */
@@ -7,8 +13,62 @@ export const kotlinOkhttp = {
7
13
  target: 'kotlin',
8
14
  client: 'okhttp',
9
15
  title: 'OkHttp',
10
- generate(request) {
11
- // TODO: Write an own converter
12
- return convertWithHttpSnippetLite(okhttp, request);
16
+ generate(request, configuration) {
17
+ const method = normalizeMethod(request?.method);
18
+ const url = normalizeUrl(joinUrlAndQuery(request?.url ?? '', request?.queryString));
19
+ const postData = request?.postData;
20
+ const lines = ['val client = OkHttpClient()', ''];
21
+ // Body. Whenever a `val body` is emitted below, `hasBody` flips so the request
22
+ // builder passes it through for both dedicated and custom method calls.
23
+ let hasBody = false;
24
+ if (postData?.mimeType === 'application/x-www-form-urlencoded' && postData.params) {
25
+ lines.push('val body = FormBody.Builder()');
26
+ postData.params.forEach((param) => {
27
+ lines.push(` .addEncoded(${quote(param.name ?? '')}, ${quote(param.value ?? '')})`);
28
+ });
29
+ lines.push(' .build()', '');
30
+ hasBody = true;
31
+ }
32
+ else if (postData?.mimeType === 'multipart/form-data' && postData.params) {
33
+ lines.push('val body = MultipartBody.Builder()', ' .setType(MultipartBody.FORM)');
34
+ postData.params.forEach((param) => {
35
+ if (param.fileName !== undefined) {
36
+ lines.push(` .addFormDataPart(${quote(param.name ?? '')}, ${quote(param.fileName)}, RequestBody.create(MediaType.parse("application/octet-stream"), File(${quote(param.fileName)})))`);
37
+ }
38
+ else if (param.value !== undefined) {
39
+ lines.push(` .addFormDataPart(${quote(param.name ?? '')}, ${quote(param.value)})`);
40
+ }
41
+ });
42
+ lines.push(' .build()', '');
43
+ hasBody = true;
44
+ }
45
+ else if (postData) {
46
+ lines.push(`val mediaType = MediaType.parse(${quote(postData.mimeType ?? '')})`);
47
+ lines.push(`val body = RequestBody.create(mediaType, ${JSON.stringify(postData.text ?? '')})`);
48
+ hasBody = true;
49
+ }
50
+ // Request builder
51
+ lines.push('val request = Request.Builder()', ` .url(${quote(url)})`);
52
+ // Method, mirroring OkHttp's dedicated builder calls and the generic `.method(...)` fallback
53
+ const bodyArg = hasBody ? 'body' : 'null';
54
+ if (!METHODS.includes(method)) {
55
+ lines.push(` .method(${quote(method)}, ${bodyArg})`);
56
+ }
57
+ else if (METHODS_WITH_BODY.includes(method)) {
58
+ lines.push(` .${method.toLowerCase()}(${bodyArg})`);
59
+ }
60
+ else {
61
+ lines.push(` .${method.toLowerCase()}()`);
62
+ }
63
+ // Basic auth, expressed through OkHttp's Credentials helper
64
+ if (configuration?.auth?.username && configuration?.auth?.password) {
65
+ lines.push(` .addHeader("Authorization", Credentials.basic(${quote(configuration.auth.username)}, ${quote(configuration.auth.password)}))`);
66
+ }
67
+ // Headers, including cookies folded into a single Cookie header
68
+ collectHeaders(request?.headers, request?.cookies).forEach((header) => {
69
+ lines.push(` .addHeader(${quote(header.name)}, ${quote(header.value)})`);
70
+ });
71
+ lines.push(' .build()', '', 'val response = client.newCall(request).execute()');
72
+ return lines.join('\n');
13
73
  },
14
74
  };
@@ -1 +1 @@
1
- {"version":3,"file":"nsurlsession.d.ts","sourceRoot":"","sources":["../../../../src/plugins/objc/nsurlsession/nsurlsession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAKpD;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAQ9B,CAAA"}
1
+ {"version":3,"file":"nsurlsession.d.ts","sourceRoot":"","sources":["../../../../src/plugins/objc/nsurlsession/nsurlsession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAmEpD;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAsI9B,CAAA"}
@@ -1,5 +1,59 @@
1
- import { nsurlsession } from '../../../httpsnippet-lite/targets/objc/nsurlsession/client.js';
2
- import { convertWithHttpSnippetLite } from '../../../utils/convertWithHttpSnippetLite.js';
1
+ import { collectHeaders, joinUrlAndQuery, normalizeMethod, normalizeUrl } from '../../../libs/http.js';
2
+ /**
3
+ * Boundary used for multipart bodies. NSURLSession does not provide one, so we
4
+ * emit a stable placeholder the user can replace as needed.
5
+ */
6
+ const MULTIPART_BOUNDARY = '---011000010111000001101001';
7
+ /**
8
+ * Escapes a string so it can be safely embedded inside an Objective-C `@"..."`
9
+ * string literal.
10
+ */
11
+ const objcStringLiteral = (value) => {
12
+ const escaped = value
13
+ .replace(/\\/g, '\\\\')
14
+ .replace(/"/g, '\\"')
15
+ .replace(/\n/g, '\\n')
16
+ .replace(/\r/g, '\\r')
17
+ .replace(/\t/g, '\\t');
18
+ return `@"${escaped}"`;
19
+ };
20
+ /**
21
+ * Renders a JavaScript value as an Objective-C object literal (NSDictionary,
22
+ * NSArray, NSNumber, ...). When `indentation` is set, nested key/value pairs are
23
+ * aligned under the opening brace for readability.
24
+ */
25
+ const literalRepresentation = (value, indentation) => {
26
+ const join = indentation === undefined ? ', ' : `,\n ${' '.repeat(indentation)}`;
27
+ switch (Object.prototype.toString.call(value)) {
28
+ case '[object Number]':
29
+ return `@${value}`;
30
+ case '[object Array]': {
31
+ const values = value.map((item) => literalRepresentation(item));
32
+ return `@[ ${values.join(join)} ]`;
33
+ }
34
+ case '[object Object]': {
35
+ const entries = Object.entries(value).map(([key, item]) => `@"${key}": ${literalRepresentation(item)}`);
36
+ return `@{ ${entries.join(join)} }`;
37
+ }
38
+ case '[object Boolean]':
39
+ return value ? '@YES' : '@NO';
40
+ default:
41
+ // Map JSON null/undefined to NSNull so NSJSONSerialization emits a real
42
+ // JSON `null` instead of an empty string.
43
+ if (value === null || value === undefined) {
44
+ return '[NSNull null]';
45
+ }
46
+ return objcStringLiteral(value.toString());
47
+ }
48
+ };
49
+ /**
50
+ * Declares and initializes an Objective-C object literal, for example
51
+ * `NSDictionary *headers = @{ @"a": @"b" };`
52
+ */
53
+ const nsDeclaration = (nsClass, name, value) => {
54
+ const opening = `${nsClass} *${name} = `;
55
+ return `${opening}${literalRepresentation(value, opening.length)};`;
56
+ };
3
57
  /**
4
58
  * objc/nsurlsession
5
59
  */
@@ -7,8 +61,73 @@ export const objcNsurlsession = {
7
61
  target: 'objc',
8
62
  client: 'nsurlsession',
9
63
  title: 'NSURLSession',
10
- generate(request) {
11
- // TODO: Write an own converter
12
- return convertWithHttpSnippetLite(nsurlsession, request);
64
+ generate(request, configuration) {
65
+ if (!request) {
66
+ return '';
67
+ }
68
+ const method = normalizeMethod(request.method);
69
+ const url = normalizeUrl(joinUrlAndQuery(request.url ?? '', request.queryString));
70
+ const headers = collectHeaders(request.headers, request.cookies);
71
+ const lines = ['#import <Foundation/Foundation.h>'];
72
+ // Headers (cookies are folded into a single Cookie header by collectHeaders)
73
+ const hasHeaders = headers.length > 0;
74
+ if (hasHeaders) {
75
+ const headersObject = Object.fromEntries(headers.map((header) => [header.name, header.value]));
76
+ lines.push('', nsDeclaration('NSDictionary', 'headers', headersObject));
77
+ }
78
+ // Body
79
+ let hasBody = false;
80
+ if (request.postData) {
81
+ const { mimeType, text, params } = request.postData;
82
+ if (mimeType === 'application/json' && text !== undefined) {
83
+ try {
84
+ const parsed = JSON.parse(text);
85
+ lines.push('', nsDeclaration('NSDictionary', 'parameters', parsed));
86
+ lines.push('', 'NSData *postData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];');
87
+ hasBody = true;
88
+ }
89
+ catch {
90
+ // Fall back to the raw text when the payload is not valid JSON.
91
+ lines.push('', `NSData *postData = [${objcStringLiteral(text)} dataUsingEncoding:NSUTF8StringEncoding];`);
92
+ hasBody = true;
93
+ }
94
+ }
95
+ else if (mimeType === 'application/x-www-form-urlencoded' && params?.length) {
96
+ hasBody = true;
97
+ const [head, ...tail] = params;
98
+ lines.push('', `NSMutableData *postData = [[NSMutableData alloc] initWithData:[${objcStringLiteral(`${encodeURIComponent(head.name)}=${encodeURIComponent(head.value ?? '')}`)} dataUsingEncoding:NSUTF8StringEncoding]];`);
99
+ tail.forEach((param) => {
100
+ lines.push(`[postData appendData:[${objcStringLiteral(`&${encodeURIComponent(param.name)}=${encodeURIComponent(param.value ?? '')}`)} dataUsingEncoding:NSUTF8StringEncoding]];`);
101
+ });
102
+ }
103
+ else if (mimeType === 'multipart/form-data' && params?.length) {
104
+ hasBody = true;
105
+ lines.push('', nsDeclaration('NSArray', 'parameters', params), `NSString *boundary = @"${MULTIPART_BOUNDARY}";`, '', 'NSError *error;', 'NSMutableString *body = [NSMutableString string];', 'for (NSDictionary *param in parameters) {', ' [body appendFormat:@"--%@\\r\\n", boundary];', ' if (param[@"fileName"]) {', ' [body appendFormat:@"Content-Disposition:form-data; name=\\"%@\\"; filename=\\"%@\\"\\r\\n", param[@"name"], param[@"fileName"]];', ' [body appendFormat:@"Content-Type: %@\\r\\n\\r\\n", param[@"contentType"]];', ' [body appendFormat:@"%@", [NSString stringWithContentsOfFile:param[@"fileName"] encoding:NSUTF8StringEncoding error:&error]];', ' if (error) {', ' NSLog(@"%@", error);', ' }', ' } else {', ' [body appendFormat:@"Content-Disposition:form-data; name=\\"%@\\"\\r\\n\\r\\n", param[@"name"]];', ' [body appendFormat:@"%@", param[@"value"]];', ' }', '}', '[body appendFormat:@"\\r\\n--%@--\\r\\n", boundary];', 'NSData *postData = [body dataUsingEncoding:NSUTF8StringEncoding];');
106
+ }
107
+ else if (mimeType === 'application/octet-stream') {
108
+ hasBody = true;
109
+ lines.push('', `NSData *postData = [${objcStringLiteral(text ?? '')} dataUsingEncoding:NSUTF8StringEncoding];`);
110
+ }
111
+ else if (text !== undefined) {
112
+ hasBody = true;
113
+ lines.push('', `NSData *postData = [${objcStringLiteral(text)} dataUsingEncoding:NSUTF8StringEncoding];`);
114
+ }
115
+ }
116
+ // Request
117
+ lines.push('', `NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:${objcStringLiteral(url)}]`, ' cachePolicy:NSURLRequestUseProtocolCachePolicy', ' timeoutInterval:10.0];', `[request setHTTPMethod:@"${method}"];`);
118
+ if (hasHeaders) {
119
+ lines.push('[request setAllHTTPHeaderFields:headers];');
120
+ }
121
+ if (hasBody) {
122
+ lines.push('[request setHTTPBody:postData];');
123
+ }
124
+ // Basic auth
125
+ if (configuration?.auth?.username && configuration?.auth?.password) {
126
+ const credentials = objcStringLiteral(`${configuration.auth.username}:${configuration.auth.password}`);
127
+ lines.push(`NSData *authData = [${credentials} dataUsingEncoding:NSUTF8StringEncoding];`, 'NSString *authValue = [NSString stringWithFormat:@"Basic %@", [authData base64EncodedStringWithOptions:0]];', '[request setValue:authValue forHTTPHeaderField:@"Authorization"];');
128
+ }
129
+ // Session
130
+ lines.push('', 'NSURLSession *session = [NSURLSession sharedSession];', 'NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request', 'completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {', ' if (error) {', ' NSLog(@"%@", error);', ' } else {', ' NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;', ' NSLog(@"%@", httpResponse);', ' }', '}];', '[dataTask resume];');
131
+ return lines.join('\n');
13
132
  },
14
133
  };
@@ -1 +1 @@
1
- {"version":3,"file":"curl.d.ts","sourceRoot":"","sources":["../../../../src/plugins/shell/curl/curl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAiBpD;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,MA8IvB,CAAA"}
1
+ {"version":3,"file":"curl.d.ts","sourceRoot":"","sources":["../../../../src/plugins/shell/curl/curl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAiBpD;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,MAiJvB,CAAA"}
@@ -29,9 +29,11 @@ export const shellCurl = {
29
29
  normalizedRequest.method = normalizedRequest.method.toUpperCase();
30
30
  // Build curl command parts
31
31
  const parts = ['curl'];
32
- // URL (quote if has query parameters or special characters)
32
+ // Build the URL, joining extra query parameters with `&` when the URL already carries a query string
33
+ const baseUrl = normalizedRequest.url ?? '';
34
+ const separator = baseUrl.includes('?') ? '&' : '?';
33
35
  const queryString = normalizedRequest.queryString?.length
34
- ? '?' +
36
+ ? separator +
35
37
  normalizedRequest.queryString
36
38
  .map((param) => {
37
39
  // Ensure both name and value are fully URI encoded
@@ -39,9 +41,10 @@ export const shellCurl = {
39
41
  })
40
42
  .join('&')
41
43
  : '';
42
- const url = `${normalizedRequest.url}${queryString}`;
43
- const hasSpecialChars = /[\s<>[\]{}|\\^%$]/.test(url);
44
- const urlPart = queryString || hasSpecialChars ? `'${url}'` : url;
44
+ const url = `${baseUrl}${queryString}`;
45
+ // Quote the URL whenever it contains anything the shell could interpret (spaces, query separators, globs, …)
46
+ const isShellSafe = /^[A-Za-z0-9._~:/%@+,=-]*$/.test(url);
47
+ const urlPart = isShellSafe ? url : `'${escapeSingleQuotes(url)}'`;
45
48
  parts[0] = `curl ${urlPart}`;
46
49
  // Method
47
50
  if (normalizedRequest.method !== 'GET') {