@tishlang/tish-lint 2.36.2 → 2.37.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/bin/tish-lint CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang_lint"
3
- version = "2.36.2"
3
+ version = "2.37.0"
4
4
  edition = "2021"
5
5
  description = "AST-based linter for Tish"
6
6
  license-file = { workspace = true }
@@ -79,7 +79,7 @@ ws = [
79
79
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
80
80
  tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
81
81
  tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "time", "sync"], optional = true }
82
- reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream"], optional = true }
82
+ reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream", "multipart"], optional = true }
83
83
  futures = { version = "0.3.32", optional = true }
84
84
  bytes = { version = "1", optional = true }
85
85
  tiny_http = { version = "0.12.0", optional = true }
@@ -111,6 +111,57 @@ pub(crate) fn extract_body(options: Option<&Value>) -> Option<String> {
111
111
  })
112
112
  }
113
113
 
114
+ /// A single `multipart/form-data` part extracted from a `fetch` options object's `multipart` array.
115
+ pub(crate) enum MultipartPart {
116
+ /// A text field: `{ name, value }`.
117
+ Text { name: String, value: String },
118
+ /// A file field read from disk (binary-safe): `{ name, path, filename?, contentType? }`.
119
+ File {
120
+ name: String,
121
+ path: String,
122
+ filename: Option<String>,
123
+ content_type: Option<String>,
124
+ },
125
+ }
126
+
127
+ /// Extract the `multipart` option — an array of part descriptors — from a `fetch` options object.
128
+ /// Each part is `{ name, value }` (text) or `{ name, path, filename?, contentType? }` (file, read as
129
+ /// bytes so binary uploads work). Returns `None` when there is no usable `multipart` array, so callers
130
+ /// fall back to `body`. A part missing `name`, or with neither `path` nor `value`, is skipped.
131
+ pub(crate) fn extract_multipart(options: Option<&Value>) -> Option<Vec<MultipartPart>> {
132
+ let field = options.and_then(|v| match v {
133
+ Value::Object(obj) => obj.borrow().strings.get("multipart").cloned(),
134
+ _ => None,
135
+ })?;
136
+ let items = match field {
137
+ Value::Array(a) => a.borrow().clone(),
138
+ _ => return None,
139
+ };
140
+ let mut parts = Vec::with_capacity(items.len());
141
+ for item in items {
142
+ let obj = match &item {
143
+ Value::Object(o) => o.borrow(),
144
+ _ => continue,
145
+ };
146
+ let name = match obj.strings.get("name") {
147
+ Some(v) => v.to_display_string(),
148
+ None => continue,
149
+ };
150
+ let filename = obj.strings.get("filename").map(|v| v.to_display_string());
151
+ let content_type = obj.strings.get("contentType").map(|v| v.to_display_string());
152
+ if let Some(path) = obj.strings.get("path").map(|v| v.to_display_string()) {
153
+ parts.push(MultipartPart::File { name, path, filename, content_type });
154
+ } else if let Some(value) = obj.strings.get("value").map(|v| v.to_display_string()) {
155
+ parts.push(MultipartPart::Text { name, value });
156
+ }
157
+ }
158
+ if parts.is_empty() {
159
+ None
160
+ } else {
161
+ Some(parts)
162
+ }
163
+ }
164
+
114
165
  pub(crate) fn build_error_response(error: &str) -> Value {
115
166
  let mut obj: ObjectMap = ObjectMap::with_capacity(2);
116
167
  obj.insert(Arc::from("error"), Value::String(error.into()));
@@ -10,7 +10,10 @@ use futures::Stream;
10
10
  use futures::StreamExt;
11
11
  use tishlang_core::{NativeFn, ObjectMap, TishOpaque, TishPromise, Value};
12
12
 
13
- use crate::http::{build_error_response, extract_body, extract_headers, extract_method};
13
+ use crate::http::{
14
+ build_error_response, extract_body, extract_headers, extract_method, extract_multipart,
15
+ MultipartPart,
16
+ };
14
17
 
15
18
  // --- Promises (Send payloads only; Value built on awaiting thread) ---
16
19
 
@@ -479,6 +482,7 @@ async fn send_request_parts(
479
482
  method: String,
480
483
  headers: Vec<(String, String)>,
481
484
  body: Option<String>,
485
+ multipart: Option<Vec<MultipartPart>>,
482
486
  ) -> Result<reqwest::Response, String> {
483
487
  if fetch_block_private() {
484
488
  ssrf_preflight(&url).await?;
@@ -496,7 +500,39 @@ async fn send_request_parts(
496
500
  for (key, value) in headers {
497
501
  req = req.header(key, value);
498
502
  }
499
- if let Some(body) = body {
503
+ // A `multipart` option takes precedence over `body` — it builds a multipart/form-data request
504
+ // (reqwest sets the Content-Type + boundary). File parts are read as bytes, so binary uploads work.
505
+ if let Some(parts) = multipart {
506
+ let mut form = reqwest::multipart::Form::new();
507
+ for part in parts {
508
+ match part {
509
+ MultipartPart::Text { name, value } => {
510
+ form = form.text(name, value);
511
+ }
512
+ MultipartPart::File {
513
+ name,
514
+ path,
515
+ filename,
516
+ content_type,
517
+ } => {
518
+ let bytes = std::fs::read(&path)
519
+ .map_err(|e| format!("multipart file '{path}': {e}"))?;
520
+ let fname = filename.unwrap_or_else(|| {
521
+ std::path::Path::new(&path)
522
+ .file_name()
523
+ .map(|s| s.to_string_lossy().into_owned())
524
+ .unwrap_or_else(|| name.clone())
525
+ });
526
+ let mut p = reqwest::multipart::Part::bytes(bytes).file_name(fname);
527
+ if let Some(ct) = content_type {
528
+ p = p.mime_str(&ct).map_err(|e| e.to_string())?;
529
+ }
530
+ form = form.part(name, p);
531
+ }
532
+ }
533
+ }
534
+ req = req.multipart(form);
535
+ } else if let Some(body) = body {
500
536
  req = req.body(body);
501
537
  }
502
538
  req.send().await.map_err(|e| e.to_string())
@@ -517,10 +553,11 @@ pub fn fetch_promise_from_args(args: Vec<Value>) -> Value {
517
553
  let method = extract_method(args.get(1));
518
554
  let headers = extract_headers(args.get(1));
519
555
  let body = extract_body(args.get(1));
556
+ let multipart = extract_multipart(args.get(1));
520
557
  let (tx, rx) = tokio::sync::oneshot::channel();
521
558
  crate::http::RUNTIME.with(|rt| {
522
559
  rt.spawn(async move {
523
- let r = send_request_parts(url, method, headers, body).await;
560
+ let r = send_request_parts(url, method, headers, body, multipart).await;
524
561
  let _ = tx.send(r);
525
562
  });
526
563
  });
@@ -541,7 +578,13 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
541
578
  }
542
579
  };
543
580
  #[allow(clippy::type_complexity)]
544
- let mut parts: Vec<(String, String, Vec<(String, String)>, Option<String>)> = Vec::new();
581
+ let mut parts: Vec<(
582
+ String,
583
+ String,
584
+ Vec<(String, String)>,
585
+ Option<String>,
586
+ Option<Vec<MultipartPart>>,
587
+ )> = Vec::new();
545
588
  for req in requests {
546
589
  let (url, opt) = match &req {
547
590
  Value::String(s) => (s.to_string(), None),
@@ -576,14 +619,15 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
576
619
  let method = extract_method(opt.as_ref());
577
620
  let headers = extract_headers(opt.as_ref());
578
621
  let body = extract_body(opt.as_ref());
579
- parts.push((url, method, headers, body));
622
+ let multipart = extract_multipart(opt.as_ref());
623
+ parts.push((url, method, headers, body, multipart));
580
624
  }
581
625
  let (tx, rx) = tokio::sync::oneshot::channel();
582
626
  crate::http::RUNTIME.with(|rt| {
583
627
  rt.spawn(async move {
584
628
  let futs: Vec<_> = parts
585
629
  .into_iter()
586
- .map(|(url, m, h, b)| send_request_parts(url, m, h, b))
630
+ .map(|(url, m, h, b, mp)| send_request_parts(url, m, h, b, mp))
587
631
  .collect();
588
632
  let results = futures::future::join_all(futs).await;
589
633
  let mapped: Vec<Result<reqwest::Response, String>> = results.into_iter().collect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-lint",
3
- "version": "2.36.2",
3
+ "version": "2.37.0",
4
4
  "description": "Tish code linter",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file