@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.
- package/dist/httpsnippet-lite/helpers/shell.d.ts +0 -1
- package/dist/httpsnippet-lite/helpers/shell.d.ts.map +1 -1
- package/dist/httpsnippet-lite/helpers/shell.js +0 -1
- package/dist/plugins/clojure/clj_http/clj_http.d.ts.map +1 -1
- package/dist/plugins/clojure/clj_http/clj_http.js +197 -5
- package/dist/plugins/csharp/restsharp/restsharp.d.ts.map +1 -1
- package/dist/plugins/csharp/restsharp/restsharp.js +134 -5
- package/dist/plugins/kotlin/okhttp/okhttp.d.ts.map +1 -1
- package/dist/plugins/kotlin/okhttp/okhttp.js +65 -5
- package/dist/plugins/objc/nsurlsession/nsurlsession.d.ts.map +1 -1
- package/dist/plugins/objc/nsurlsession/nsurlsession.js +124 -5
- package/dist/plugins/shell/curl/curl.d.ts.map +1 -1
- package/dist/plugins/shell/curl/curl.js +8 -5
- package/dist/plugins/shell/wget/wget.d.ts.map +1 -1
- package/dist/plugins/shell/wget/wget.js +103 -5
- package/package.json +3 -3
- package/dist/httpsnippet-lite/targets/clojure/clj_http/client.d.ts +0 -12
- package/dist/httpsnippet-lite/targets/clojure/clj_http/client.d.ts.map +0 -1
- package/dist/httpsnippet-lite/targets/clojure/clj_http/client.js +0 -186
- package/dist/httpsnippet-lite/targets/csharp/restsharp/client.d.ts +0 -3
- package/dist/httpsnippet-lite/targets/csharp/restsharp/client.d.ts.map +0 -1
- package/dist/httpsnippet-lite/targets/csharp/restsharp/client.js +0 -36
- package/dist/httpsnippet-lite/targets/kotlin/okhttp/client.d.ts +0 -12
- package/dist/httpsnippet-lite/targets/kotlin/okhttp/client.d.ts.map +0 -1
- package/dist/httpsnippet-lite/targets/kotlin/okhttp/client.js +0 -87
- package/dist/httpsnippet-lite/targets/objc/helpers.d.ts +0 -21
- package/dist/httpsnippet-lite/targets/objc/helpers.d.ts.map +0 -1
- package/dist/httpsnippet-lite/targets/objc/helpers.js +0 -57
- package/dist/httpsnippet-lite/targets/objc/nsurlsession/client.d.ts +0 -12
- package/dist/httpsnippet-lite/targets/objc/nsurlsession/client.d.ts.map +0 -1
- package/dist/httpsnippet-lite/targets/objc/nsurlsession/client.js +0 -125
- package/dist/httpsnippet-lite/targets/shell/wget/client.d.ts +0 -12
- package/dist/httpsnippet-lite/targets/shell/wget/client.d.ts.map +0 -1
- package/dist/httpsnippet-lite/targets/shell/wget/client.js +0 -49
|
@@ -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
|
|
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"}
|
|
@@ -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;
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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":"
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
//
|
|
12
|
-
|
|
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;
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
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;
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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,
|
|
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
|
|
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 = `${
|
|
43
|
-
|
|
44
|
-
const
|
|
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') {
|