1use rquickjs::atom::PredefinedAtom;
14use rquickjs::function::{Opt, This};
15use rquickjs::{Class, Ctx, Function, Object, Value, class::Trace};
16
17use crate::web::blob_bytes::{blob_parts, file_parts};
18use crate::web::js_iterator::live_iterator;
19
20#[derive(Clone)]
24pub enum FormEntry {
25 Text(String),
26 File {
27 bytes: Vec<u8>,
28 filename: String,
29 content_type: String,
30 },
31}
32
33#[derive(Trace, Default)]
34#[rquickjs::class(rename = "FormData")]
35pub struct FormDataJs {
36 #[qjs(skip_trace)]
37 entries: Vec<(String, FormEntry)>,
38}
39
40impl FormDataJs {
41 #[must_use]
44 pub fn from_entries(entries: Vec<(String, FormEntry)>) -> Self {
45 Self { entries }
46 }
47
48 #[must_use]
50 pub fn entries_slice(&self) -> &[(String, FormEntry)] {
51 &self.entries
52 }
53}
54
55#[allow(unsafe_code)]
56unsafe impl rquickjs::JsLifetime<'_> for FormDataJs {
57 type Changed<'to> = FormDataJs;
58}
59
60impl FormDataJs {
61 fn coerce(value: &Value<'_>, filename: Option<String>) -> FormEntry {
62 if let Some((bytes, ct, name)) = file_parts(value) {
65 return FormEntry::File {
66 bytes,
67 filename: filename.unwrap_or(name),
68 content_type: if ct.is_empty() {
69 "application/octet-stream".to_string()
70 } else {
71 ct
72 },
73 };
74 }
75 if let Some((bytes, ct)) = blob_parts(value) {
76 return FormEntry::File {
77 bytes,
78 filename: filename.unwrap_or_else(|| "blob".to_string()),
79 content_type: if ct.is_empty() {
80 "application/octet-stream".to_string()
81 } else {
82 ct
83 },
84 };
85 }
86 let s = value
87 .as_string()
88 .and_then(|s| s.to_string().ok())
89 .or_else(|| value.as_number().map(|n| n.to_string()))
90 .or_else(|| value.as_bool().map(|b| b.to_string()))
91 .unwrap_or_default();
92 FormEntry::Text(s)
93 }
94
95 fn entry_value<'js>(ctx: &Ctx<'js>, e: &FormEntry) -> rquickjs::Result<Value<'js>> {
98 match e {
99 FormEntry::Text(s) => Ok(rquickjs::String::from_str(ctx.clone(), s)?.into_value()),
100 FormEntry::File {
101 bytes,
102 content_type,
103 filename,
104 } => {
105 let file = Class::instance(
106 ctx.clone(),
107 crate::buffer::File::from_bytes(
108 ctx,
109 bytes.clone(),
110 filename.clone(),
111 Some(content_type.clone()),
112 )?,
113 )?;
114 Ok(file.into_value())
115 },
116 }
117 }
118
119 fn project_entry<'js>(
120 ctx: &Ctx<'js>,
121 parent: &Class<'js, Self>,
122 index: usize,
123 ) -> rquickjs::Result<Option<Value<'js>>> {
124 let Some((name, entry)) = parent.borrow().entries.get(index).cloned() else {
125 return Ok(None);
126 };
127 let pair = rquickjs::Array::new(ctx.clone())?;
128 pair.set(0, rquickjs::String::from_str(ctx.clone(), &name)?)?;
129 pair.set(1, Self::entry_value(ctx, &entry)?)?;
130 Ok(Some(pair.into_value()))
131 }
132
133 fn project_key<'js>(ctx: &Ctx<'js>, parent: &Class<'js, Self>, index: usize) -> rquickjs::Result<Option<Value<'js>>> {
134 let Some((name, _)) = parent.borrow().entries.get(index).cloned() else {
135 return Ok(None);
136 };
137 Ok(Some(rquickjs::String::from_str(ctx.clone(), &name)?.into_value()))
138 }
139
140 fn project_value<'js>(
141 ctx: &Ctx<'js>,
142 parent: &Class<'js, Self>,
143 index: usize,
144 ) -> rquickjs::Result<Option<Value<'js>>> {
145 let Some((_, entry)) = parent.borrow().entries.get(index).cloned() else {
146 return Ok(None);
147 };
148 Self::entry_value(ctx, &entry).map(Some)
149 }
150
151 pub fn from_urlencoded(body: &str) -> Self {
154 Self {
155 entries: url::form_urlencoded::parse(body.as_bytes())
156 .map(|(k, v)| (k.into_owned(), FormEntry::Text(v.into_owned())))
157 .collect(),
158 }
159 }
160}
161
162#[rquickjs::methods(rename_all = "camelCase")]
163impl FormDataJs {
164 #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
167 pub fn to_string_tag() -> &'static str {
168 "FormData"
169 }
170
171 #[qjs(constructor)]
172 pub fn new() -> Self {
173 Self::default()
174 }
175
176 #[qjs(rename = "append")]
177 pub fn append(&mut self, name: String, value: Value<'_>, filename: Opt<String>) {
178 self.entries.push((name, Self::coerce(&value, filename.0)));
179 }
180
181 #[qjs(rename = "set")]
182 pub fn set(&mut self, name: String, value: Value<'_>, filename: Opt<String>) {
183 let entry = Self::coerce(&value, filename.0);
184 if let Some(i) = self.entries.iter().position(|(k, _)| k == &name) {
187 self.entries[i].1 = entry;
188 let mut seen = false;
189 self.entries.retain(|(k, _)| {
190 if k == &name {
191 if seen {
192 return false;
193 }
194 seen = true;
195 }
196 true
197 });
198 } else {
199 self.entries.push((name, entry));
200 }
201 }
202
203 #[qjs(rename = "has")]
204 pub fn has(&self, name: String) -> bool {
205 self.entries.iter().any(|(k, _)| k == &name)
206 }
207
208 #[qjs(rename = "delete")]
209 pub fn delete(&mut self, name: String) {
210 self.entries.retain(|(k, _)| k != &name);
211 }
212
213 #[qjs(rename = "get")]
214 pub fn get<'js>(&self, ctx: Ctx<'js>, name: String) -> rquickjs::Result<Value<'js>> {
215 match self.entries.iter().find(|(k, _)| k == &name) {
216 Some((_, e)) => Self::entry_value(&ctx, e),
217 None => Ok(Value::new_null(ctx)),
218 }
219 }
220
221 #[qjs(rename = "getAll")]
222 pub fn get_all<'js>(&self, ctx: Ctx<'js>, name: String) -> rquickjs::Result<Vec<Value<'js>>> {
223 self
224 .entries
225 .iter()
226 .filter(|(k, _)| k == &name)
227 .map(|(_, e)| Self::entry_value(&ctx, e))
228 .collect()
229 }
230
231 #[qjs(rename = "keys")]
232 pub fn keys<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
233 live_iterator(&ctx, this.0, Self::project_key)
234 }
235
236 #[qjs(rename = "values")]
237 pub fn values<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
238 live_iterator(&ctx, this.0, Self::project_value)
239 }
240
241 #[qjs(rename = "entries")]
242 pub fn entries<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
243 live_iterator(&ctx, this.0, Self::project_entry)
244 }
245
246 #[qjs(rename = PredefinedAtom::SymbolIterator)]
247 pub fn js_iter<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
248 live_iterator(&ctx, this.0, Self::project_entry)
249 }
250
251 #[qjs(rename = "forEach")]
252 pub fn for_each<'js>(&self, ctx: Ctx<'js>, cb: Function<'js>) -> rquickjs::Result<()> {
253 for (k, e) in &self.entries {
254 let v = Self::entry_value(&ctx, e)?;
255 cb.call::<_, ()>((v, k.clone()))?;
256 }
257 Ok(())
258 }
259}