1use std::collections::BTreeMap;
5
6use crate::{
7 DefaultValue, Dispatch, MethodSig, PackageDeclaration, Param, PropertySig, Span, Type,
8 TypeKind, TypeSymbol, ValueKind, ValueSymbol,
9};
10
11use super::MODULE_NAME;
12
13pub fn package_declaration() -> PackageDeclaration {
14 let mut defs = PackageDeclaration::with_package(MODULE_NAME);
15
16 defs.types.insert(
17 "Headers".to_string(),
18 TypeSymbol {
19 name: "Headers".to_string(),
20 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Headers"),
21 declaration_span: Span::at(crate::FileId::HTTP),
22 kind: TypeKind::Alias {
23 generics: Vec::new(),
24 ty: Type::InterfaceRef { mangled: crate::mangle::prelude("Map"),
25 package: crate::Package::prelude(),
26 name: "Map".to_string(),
27 args: vec![Type::String, Type::String],
28 },
29 doc: crate::doc(crate::FileId::HTTP,
30 "/** HTTP header bag: `Map<string, string>` with names lowercased on the wire. Future slice may add case-insensitive lookup. */",
31 ),
32 },
33 },
34 );
35
36 insert_response_interface(&mut defs);
37 insert_download_options_interface(&mut defs);
38 insert_download_result_interface(&mut defs);
39
40 for verb in &["get", "delete", "head", "options"] {
42 insert_verb_fn(&mut defs, verb, false);
43 }
44 for verb in &["post", "put", "patch"] {
45 insert_verb_fn(&mut defs, verb, true);
46 }
47
48 defs.values.insert(
49 "download".to_string(),
50 ValueSymbol {
51 name: "download".to_string(),
52 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "download"),
53 declaration_span: Span::at(crate::FileId::HTTP),
54 kind: ValueKind::Function {
55 generics: Vec::new(),
56 params: vec![
57 Param::new("url", Type::String),
58 Param::new("path", Type::String),
59 Param::with_default(
60 "options",
61 optional_download_options_type(),
62 DefaultValue::Undefined,
63 ),
64 ],
65 ret: download_result_type(),
66 type_predicate: None,
67 doc: crate::doc(crate::FileId::HTTP,
68 "/**\n * Download a remote file directly to the VFS — wget-style. Streams the response body to disk so memory stays bounded regardless of file size. Atomic: a crash mid-download leaves a `.tmp` sibling rather than a half-written final file. Traps on transport failure (DNS, connect, TLS, timeout), oversize response, refuse-on-exists, or VFS path escape.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param path Destination VFS path (e.g. `\"/workspace/dataset.csv\"`). The parent directory must exist — call `fs.mkdir` first if needed.\n * @param options Optional `DownloadOptions` bag: `overwrite` (default `false`), `maxBytes` (default and upper bound: tier-configured), `headers`, `timeout` (default and upper bound: 60_000 ms, or a lower operator limit), `decompress` (default `false`). Omit or pass `undefined` to take every default.\n * @capability http.download { host: $url.host, url_path: $url.path, vfs_path: $path, max_bytes: number, overwrite: boolean, decompress: boolean }\n * @capability fs.write { path, max_bytes: number }\n */",
69 ),
70 },
71 },
72 );
73
74 defs.values.insert(
75 "request".to_string(),
76 ValueSymbol {
77 name: "request".to_string(),
78 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "request"),
79 declaration_span: Span::at(crate::FileId::HTTP),
80 kind: ValueKind::Function {
81 generics: Vec::new(),
82 params: vec![
83 Param::new("method", Type::String),
84 Param::new("url", Type::String),
85 Param::with_default(
86 "body",
87 optional_body_type(),
88 DefaultValue::Undefined,
89 ),
90 Param::with_default(
91 "headers",
92 optional_headers_type(),
93 DefaultValue::Undefined,
94 ),
95 ],
96 ret: response_type(),
97 type_predicate: None,
98 doc: crate::doc(crate::FileId::HTTP,
99 "/**\n * Issue an HTTP request with a runtime-chosen verb. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param method HTTP method (case-insensitive). Common verbs: `\"GET\"` / `\"POST\"` / `\"PUT\"` / `\"PATCH\"` / `\"DELETE\"` / `\"HEAD\"` / `\"OPTIONS\"`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param body Optional request body. `string` is UTF-8 encoded and auto-tagged `Content-Type: text/plain; charset=utf-8` unless the caller overrides; a structural object or array is JSON-encoded and auto-tagged `Content-Type: application/json` unless the caller overrides; `Uint8Array` sends the bytes verbatim with no defaulted Content-Type; `null` sends the JSON payload `null` with default `Content-Type: application/json`; `undefined` (or omitted) sends an empty body.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n * @capability http.get { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.post { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.put { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.patch { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.delete { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.head { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n * @capability http.options { host: $url.host, path: $url.path, body_size: number, timeout_ms: number }\n */",
100 ),
101 },
102 },
103 );
104
105 defs
106}
107
108fn insert_verb_fn(defs: &mut PackageDeclaration, verb: &str, has_body: bool) {
109 let upper = verb.to_uppercase();
110 let doc = if has_body {
111 format!(
112 "/**\n * Issue a synchronous HTTP {upper}. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param body Optional request body. `string` is UTF-8 encoded and auto-tagged `Content-Type: text/plain; charset=utf-8` unless the caller overrides; a structural object or array is JSON-encoded and auto-tagged `Content-Type: application/json` unless the caller overrides; `Uint8Array` sends the bytes verbatim with no defaulted Content-Type; `null` sends the JSON payload `null` with default `Content-Type: application/json`; `undefined` (or omitted) sends an empty body.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n */",
113 )
114 } else {
115 format!(
116 "/**\n * Issue a synchronous HTTP {upper}. Returns the full response (status, headers, UTF-8-decoded body). Traps on transport failure (DNS, connect, TLS, timeout) — surfaced as a guest `Error`.\n * @param url Absolute URL. A path with a `.` or `..` segment, in any spelling such as `%2E%2E`, throws `TypeError` and sends nothing.\n * @param headers Optional request headers. Omit or pass `undefined` to send no extra headers.\n */",
117 )
118 };
119 let capability = format!(
120 " * @capability http.{verb} {{ host: $url.host, path: $url.path, body_size: number, timeout_ms: number }}\n */"
121 );
122 let doc = doc.replace(" */", &capability);
123 let mut params = vec![Param::new("url", Type::String)];
124 if has_body {
125 params.push(Param::with_default(
126 "body",
127 optional_body_type(),
128 DefaultValue::Undefined,
129 ));
130 }
131 params.push(Param::with_default(
132 "headers",
133 optional_headers_type(),
134 DefaultValue::Undefined,
135 ));
136 defs.values.insert(
137 verb.to_string(),
138 ValueSymbol {
139 name: verb.to_string(),
140 mangled_name: crate::mangle::package_symbol(MODULE_NAME, verb),
141 declaration_span: Span::at(crate::FileId::HTTP),
142 kind: ValueKind::Function {
143 generics: Vec::new(),
144 params,
145 ret: response_type(),
146 type_predicate: None,
147 doc: crate::doc(crate::FileId::HTTP, &doc),
148 },
149 },
150 );
151}
152
153fn optional_headers_type() -> Type {
154 Type::Union(vec![headers_type(), Type::Undefined])
155}
156
157fn optional_body_type() -> Type {
158 Type::Union(vec![
159 Type::String,
160 Type::Uint8Array,
161 Type::any_object(),
163 Type::Array(Box::new(Type::Unknown)),
164 Type::Null,
165 Type::Undefined,
166 ])
167}
168
169fn response_type() -> Type {
170 Type::InterfaceRef {
171 mangled: crate::mangle::package_symbol(MODULE_NAME, "Response"),
172 package: crate::Package(MODULE_NAME.to_string()),
173 name: "Response".to_string(),
174 args: Vec::new(),
175 }
176}
177
178fn download_result_type() -> Type {
179 Type::InterfaceRef {
180 mangled: crate::mangle::package_symbol(MODULE_NAME, "DownloadResult"),
181 package: crate::Package(MODULE_NAME.to_string()),
182 name: "DownloadResult".to_string(),
183 args: Vec::new(),
184 }
185}
186
187fn download_options_type() -> Type {
188 Type::InterfaceRef {
189 mangled: crate::mangle::package_symbol(MODULE_NAME, "DownloadOptions"),
190 package: crate::Package(MODULE_NAME.to_string()),
191 name: "DownloadOptions".to_string(),
192 args: Vec::new(),
193 }
194}
195
196fn optional_download_options_type() -> Type {
197 Type::Union(vec![download_options_type(), Type::Undefined])
198}
199
200fn headers_type() -> Type {
201 Type::Alias {
202 mangled: crate::mangle::package_symbol(MODULE_NAME, "Headers"),
203 package: crate::Package(MODULE_NAME.to_string()),
204 name: "Headers".to_string(),
205 args: Vec::new(),
206 ty: Box::new(Type::InterfaceRef {
207 mangled: crate::mangle::prelude("Map"),
208 package: crate::Package::prelude(),
209 name: "Map".to_string(),
210 args: vec![Type::String, Type::String],
211 }),
212 }
213}
214
215fn insert_response_interface(defs: &mut PackageDeclaration) {
216 let mut properties = BTreeMap::new();
217 insert_property(
218 &mut properties,
219 "body",
220 Type::String,
221 "/** Response body, decoded as UTF-8. For binary responses, a future `bytes()` method will return `Uint8Array` without lossy decoding. */",
222 );
223 insert_property(
224 &mut properties,
225 "headers",
226 headers_type(),
227 "/** Response headers. Keys lowercased on the wire so `headers.get(\"content-type\")` works regardless of how the server cased them. */",
228 );
229 insert_property(
230 &mut properties,
231 "ok",
232 Type::Boolean,
233 "/** `true` iff `status` is in the 2xx range. */",
234 );
235 insert_property(
236 &mut properties,
237 "status",
238 Type::Number,
239 "/** HTTP status code (e.g. `200`, `404`). */",
240 );
241 insert_property(
242 &mut properties,
243 "statusText",
244 Type::String,
245 "/** Reason phrase (e.g. `\"OK\"`, `\"Not Found\"`). Empty when the server doesn't supply one. */",
246 );
247 insert_property(
248 &mut properties,
249 "url",
250 Type::String,
251 "/** Final URL after any redirects. Equals the original URL if there were no redirects. */",
252 );
253
254 let mut methods = BTreeMap::new();
255 methods.insert(
256 "throwForStatus".to_string(),
257 MethodSig {
258 optional: false,
259 generics: Vec::new(),
260 params: Vec::new(),
261 ret: Type::Void,
262 predicate: None,
263 doc: crate::doc(crate::FileId::HTTP,
264 "/** Throw if `ok` is `false`. The error message includes the status code, reason phrase, and final URL; catch it with `try`/`catch` like any other `Error`. */",
265 ),
266 },
267 );
268 methods.insert(
269 "toString".to_string(),
270 MethodSig {
271 optional: false,
272 generics: Vec::new(),
273 params: Vec::new(),
274 ret: Type::String,
275 predicate: None,
276 doc: crate::doc(crate::FileId::HTTP,
277 "/** Concise description of the response — `\"Response(200 OK, https://...)\"`. Built host-side for efficiency. */",
278 ),
279 },
280 );
281 methods.insert(
285 "json".to_string(),
286 MethodSig {
287 optional: false,
288 generics: Vec::new(),
289 params: Vec::new(),
290 ret: Type::Unknown,
291 predicate: None,
292 doc: crate::doc(crate::FileId::HTTP,
293 "/** Parse the response body as JSON into `unknown`. Validate with `as`, for example `r.json() as User`. */",
294 ),
295 },
296 );
297
298 defs.types.insert(
299 "Response".to_string(),
300 TypeSymbol {
301 name: "Response".to_string(),
302 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Response"),
303 declaration_span: Span::at(crate::FileId::HTTP),
304 kind: TypeKind::Interface { index: None,
305 generics: Vec::new(),
306 methods,
307 properties,
308 dispatch: Dispatch::Direct,
309 doc: crate::doc(crate::FileId::HTTP,
310 "/** Result of an HTTP call — status, headers, body, and the final URL after any redirects. Constructed only by the verb-form helpers; user code reads fields via property access and calls `throwForStatus()` / `toString()`. */",
311 ),
312 },
313 },
314 );
315}
316
317fn insert_download_options_interface(defs: &mut PackageDeclaration) {
318 let mut properties = BTreeMap::new();
319 insert_optional_property(
320 &mut properties,
321 "overwrite",
322 Type::Boolean,
323 "/** When `true`, clobber an existing file at `path`. Default `false` — refuse with a trap, since LLMs retry operations and silent overwrites cause data loss. */",
324 );
325 insert_optional_property(
326 &mut properties,
327 "maxBytes",
328 Type::Number,
329 "/** Maximum bytes to write to disk (post-decompression when `decompress` is true). Default: the tier-configured global limit. The download traps with a `response too large` error if exceeded. */",
330 );
331 insert_optional_property(
332 &mut properties,
333 "headers",
334 optional_headers_type(),
335 "/** Request headers — typically used for auth (`Authorization: Bearer …`). */",
336 );
337 insert_optional_property(
338 &mut properties,
339 "timeout",
340 Type::Number,
341 "/** Per-request timeout in milliseconds. Default 60_000 (vs 30_000 for the verb-form helpers — downloads are usually larger). */",
342 );
343 insert_optional_property(
344 &mut properties,
345 "decompress",
346 Type::Boolean,
347 "/** When `true`, transparently gunzip / unzstd the response if the `Content-Encoding` header (or URL suffix `.gz` / `.zst`) advertises a compressed stream. Default `false`: downloading `data.csv.gz` saves the `.gz` file as-is. */",
348 );
349
350 defs.types.insert(
351 "DownloadOptions".to_string(),
352 TypeSymbol {
353 name: "DownloadOptions".to_string(),
354 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DownloadOptions"),
355 declaration_span: Span::at(crate::FileId::HTTP),
356 kind: TypeKind::Interface { index: None,
357 generics: Vec::new(),
358 methods: BTreeMap::new(),
359 properties,
360 dispatch: Dispatch::VTable,
362 doc: crate::doc(crate::FileId::HTTP,
363 "/** Options bag for [`download`]. All fields optional. Pass `undefined` (or omit the param) to take every default. */",
364 ),
365 },
366 },
367 );
368}
369
370fn insert_optional_property(
371 properties: &mut BTreeMap<String, PropertySig>,
372 name: &str,
373 ty: Type,
374 doc: &str,
375) {
376 properties.insert(
377 name.to_string(),
378 PropertySig {
379 ty,
380 readonly: false,
381 intrinsic: false,
382 optional: true,
383 doc: crate::doc(crate::FileId::HTTP, doc),
384 },
385 );
386}
387
388fn insert_download_result_interface(defs: &mut PackageDeclaration) {
389 let mut properties = BTreeMap::new();
390 insert_property(
391 &mut properties,
392 "bytesWritten",
393 Type::Number,
394 "/** Bytes actually written to disk (post-decompression when `decompress` was set). */",
395 );
396 insert_property(
397 &mut properties,
398 "contentType",
399 Type::String,
400 "/** `Content-Type` header from the response, lowercased. Empty string when the server didn't send one. */",
401 );
402 insert_property(
403 &mut properties,
404 "duration_ms",
405 Type::Number,
406 "/** Wall-clock duration of the download in milliseconds, measured from temp-file open through atomic rename. */",
407 );
408 insert_property(
409 &mut properties,
410 "finalUrl",
411 Type::String,
412 "/** Final URL after any redirects. Equals the original URL if there were no redirects. */",
413 );
414 insert_property(
415 &mut properties,
416 "path",
417 Type::String,
418 "/** The VFS path the file was written to — echoes the `path` argument. */",
419 );
420 insert_property(
421 &mut properties,
422 "status",
423 Type::Number,
424 "/** HTTP status code (e.g. `200`). The download does not trap on non-2xx — a 404 still writes the response body to disk, on the theory that an HTML error page is sometimes what an agent wants. Check `status` to discriminate. */",
425 );
426
427 let mut methods = BTreeMap::new();
428 methods.insert(
429 "toString".to_string(),
430 MethodSig {
431 optional: false,
432 generics: Vec::new(),
433 params: Vec::new(),
434 ret: Type::String,
435 predicate: None,
436 doc: crate::doc(crate::FileId::HTTP,
437 "/** Concise description — `\"Download(200, 1024 bytes -> /workspace/file.csv)\"`. */",
438 ),
439 },
440 );
441
442 defs.types.insert(
443 "DownloadResult".to_string(),
444 TypeSymbol {
445 name: "DownloadResult".to_string(),
446 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DownloadResult"),
447 declaration_span: Span::at(crate::FileId::HTTP),
448 kind: TypeKind::Interface { index: None,
449 generics: Vec::new(),
450 methods,
451 properties,
452 dispatch: Dispatch::Direct,
453 doc: crate::doc(crate::FileId::HTTP,
454 "/** Result of [`download`] — what was written, where, and the response metadata. */",
455 ),
456 },
457 },
458 );
459}
460
461fn insert_property(
462 properties: &mut BTreeMap<String, PropertySig>,
463 name: &str,
464 ty: Type,
465 doc: &str,
466) {
467 properties.insert(
468 name.to_string(),
469 PropertySig {
470 ty,
471 readonly: true,
472 intrinsic: false,
473 optional: false,
474 doc: crate::doc(crate::FileId::HTTP, doc),
475 },
476 );
477}