Skip to main content

surrealdb_core/fnc/script/fetch/classes/
form_data.rs

1//! FormData class implementation
2
3use std::collections::HashMap;
4use std::string::String as StdString;
5
6use js::class::{Class, Trace};
7use js::function::{Opt, Rest};
8use js::prelude::Coerced;
9use js::{Ctx, Exception, FromJs, JsLifetime, Result, String, Value};
10use reqwest::multipart::{Form, Part};
11
12use crate::fnc::script::fetch::classes::Blob;
13
14#[derive(Clone, JsLifetime)]
15pub enum FormDataValue<'js> {
16	String(String<'js>),
17	Blob {
18		data: Class<'js, Blob>,
19		filename: Option<String<'js>>,
20	},
21}
22
23impl<'js> FormDataValue<'js> {
24	fn from_arguments(
25		ctx: &Ctx<'js>,
26		value: Value<'js>,
27		filename: Opt<Coerced<String<'js>>>,
28		error: &'static str,
29	) -> Result<Self> {
30		if let Some(blob) = value.as_object().and_then(Class::<Blob>::from_object) {
31			let filename = filename.into_inner().map(|x| x.0);
32
33			Ok(FormDataValue::Blob {
34				data: blob,
35				filename,
36			})
37		} else if filename.into_inner().is_some() {
38			return Err(Exception::throw_type(ctx, error));
39		} else {
40			let value = Coerced::<String>::from_js(ctx, value)?;
41			Ok(FormDataValue::String(value.0))
42		}
43	}
44}
45
46#[js::class]
47#[derive(Clone, Trace, JsLifetime)]
48pub struct FormData<'js> {
49	#[qjs(skip_trace)]
50	pub(crate) values: HashMap<StdString, Vec<FormDataValue<'js>>>,
51}
52
53#[js::methods]
54impl<'js> FormData<'js> {
55	// ------------------------------
56	// Constructor
57	// ------------------------------
58
59	// FormData spec states that FormDa takes two html elements as arguments
60	// which does not make sense implementing fetch outside a browser.
61	// So we ignore those arguments.
62	#[qjs(constructor)]
63	pub fn new(ctx: Ctx<'js>, args: Rest<()>) -> Result<Self> {
64		if !args.is_empty() {
65			return Err(Exception::throw_internal(
66				&ctx,
67				"Cant call FormData with arguments as the dom elements required are not available",
68			));
69		}
70		Ok(FormData {
71			values: HashMap::new(),
72		})
73	}
74
75	pub fn append(
76		&mut self,
77		ctx: Ctx<'js>,
78		name: Coerced<StdString>,
79		value: Value<'js>,
80		filename: Opt<Coerced<String<'js>>>,
81	) -> Result<()> {
82		let value = FormDataValue::from_arguments(
83			&ctx,
84			value,
85			filename,
86			"Can't call `append` on `FormData` with a filename when value isn't of type `Blob`",
87		)?;
88
89		self.values.entry(name.0).or_default().push(value);
90
91		Ok(())
92	}
93
94	pub fn set(
95		&mut self,
96		ctx: Ctx<'js>,
97		name: Coerced<StdString>,
98		value: Value<'js>,
99		filename: Opt<Coerced<String<'js>>>,
100	) -> Result<()> {
101		let value = FormDataValue::from_arguments(
102			&ctx,
103			value,
104			filename,
105			"Can't call `set` on `FormData` with a filename when value isn't of type `Blob`",
106		)?;
107
108		self.values.insert(name.0, vec![value]);
109
110		Ok(())
111	}
112
113	pub fn has(&self, name: Coerced<StdString>) -> bool {
114		self.values.contains_key(&name.0)
115	}
116
117	pub fn delete(&mut self, name: Coerced<StdString>) {
118		self.values.remove(&name.0);
119	}
120
121	#[qjs(skip)]
122	pub fn to_form(&self) -> Result<Form> {
123		let mut res = Form::new();
124		for (k, v) in self.values.iter() {
125			for v in v {
126				match v {
127					FormDataValue::String(x) => {
128						res = res.text(k.clone(), x.to_string()?);
129					}
130					FormDataValue::Blob {
131						data,
132						filename,
133					} => {
134						let mut part = Part::bytes(data.borrow().data.to_vec());
135						if let Some(filename) = filename {
136							let filename = filename.to_string()?;
137							part = part.file_name(filename);
138						}
139						res = res.part(k.clone(), part);
140					}
141				}
142			}
143		}
144		Ok(res)
145	}
146}