sl_map_web/
usb_notecard.rs1use axum::extract::Multipart;
5use sl_types::map::USBNotecard;
6use uuid::Uuid;
7
8use crate::error::Error;
9
10#[derive(Debug, Default)]
12pub struct NotecardForm {
13 pub notecard: Option<USBNotecard>,
16 pub notecard_id: Option<Uuid>,
20 pub border_regions: Option<u16>,
23 pub border_north: Option<u16>,
25 pub border_south: Option<u16>,
27 pub border_east: Option<u16>,
29 pub border_west: Option<u16>,
31}
32
33impl NotecardForm {
34 #[must_use]
37 pub fn borders(&self) -> (u16, u16, u16, u16) {
38 if let Some(b) = self.border_regions {
39 (b, b, b, b)
40 } else {
41 (
42 self.border_north.unwrap_or(0),
43 self.border_south.unwrap_or(0),
44 self.border_east.unwrap_or(0),
45 self.border_west.unwrap_or(0),
46 )
47 }
48 }
49}
50
51pub async fn parse_notecard_form(mut multipart: Multipart) -> Result<NotecardForm, Error> {
66 let mut form = NotecardForm::default();
67 let mut notecard_text: Option<String> = None;
68 let mut notecard_file: Option<String> = None;
69 let mut notecard_id: Option<Uuid> = None;
70 while let Some(field) = multipart.next_field().await? {
71 let Some(name) = field.name().map(str::to_owned) else {
72 continue;
73 };
74 match name.as_str() {
75 "notecard" => {
76 let bytes = field.bytes().await?;
77 if !bytes.is_empty() {
78 let text = String::from_utf8(bytes.to_vec())
79 .map_err(|e| Error::BadRequest(format!("notecard is not UTF-8: {e}")))?;
80 notecard_file = Some(text);
81 }
82 }
83 "notecard_text" => {
84 let text = field.text().await?;
85 if !text.trim().is_empty() {
86 notecard_text = Some(text);
87 }
88 }
89 "notecard_id" => {
90 let raw = field.text().await?;
91 let trimmed = raw.trim();
92 if !trimmed.is_empty() {
93 notecard_id = Some(
94 Uuid::parse_str(trimmed)
95 .map_err(|e| Error::BadRequest(format!("invalid notecard_id: {e}")))?,
96 );
97 }
98 }
99 "border_regions" => form.border_regions = parse_optional_u16(field.text().await?)?,
100 "border_north" => form.border_north = parse_optional_u16(field.text().await?)?,
101 "border_south" => form.border_south = parse_optional_u16(field.text().await?)?,
102 "border_east" => form.border_east = parse_optional_u16(field.text().await?)?,
103 "border_west" => form.border_west = parse_optional_u16(field.text().await?)?,
104 _ => {}
105 }
106 }
107 let inline = notecard_file.or(notecard_text);
108 match (inline, notecard_id) {
109 (Some(raw), None) => {
110 form.notecard = Some(raw.parse()?);
111 }
112 (None, Some(id)) => {
113 form.notecard_id = Some(id);
114 }
115 (Some(_), Some(_)) => {
116 return Err(Error::BadRequest(
117 "supply either `notecard_id` or notecard text/file, not both".to_owned(),
118 ));
119 }
120 (None, None) => {
121 return Err(Error::BadRequest(
122 "supply a `notecard_id`, `notecard` file upload, or `notecard_text` field"
123 .to_owned(),
124 ));
125 }
126 }
127 Ok(form)
128}
129
130fn parse_optional_u16(s: String) -> Result<Option<u16>, Error> {
133 let trimmed = s.trim();
134 if trimmed.is_empty() {
135 return Ok(None);
136 }
137 trimmed
138 .parse::<u16>()
139 .map(Some)
140 .map_err(|e| Error::BadRequest(format!("invalid u16 `{trimmed}`: {e}")))
141}