rumtk_web/utils/form_data.rs
1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2025 Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025 Ethan Dixon
6 * Copyright (C) 2025 MedicalMasses L.L.C. <contact@medicalmasses.com>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21use rumtk_core::core::RUMResult;
22use rumtk_core::strings::{
23 rumtk_format, RUMArrayConversions, RUMString, RUMStringConversions, ToCompactString,
24};
25use rumtk_core::types::{RUMBuffer, RUMHashMap, RUMID};
26
27use crate::utils::defaults::*;
28use crate::{RUMWebData, RouterForm};
29
30pub type FormBuffer = RUMBuffer;
31
32#[derive(Default, Debug, PartialEq, Clone)]
33pub struct FormData {
34 pub form: RUMWebData,
35 pub files: RUMHashMap<RUMString, FormBuffer>,
36}
37
38pub type FormResult = RUMResult<FormData>;
39
40pub async fn get_type(content_type: &str) -> &'static str {
41 match content_type {
42 CONTENT_TYPE_PDF => FORM_DATA_TYPE_PDF,
43 _ => FORM_DATA_TYPE_DEFAULT,
44 }
45}
46
47///
48/// Converts the incoming form data with type [RouterForm] to [FormData] which is the preferred
49/// type in the library.
50///
51/// ## Examples
52///
53/// ### Plaintext only
54/// ```
55/// use axum::body::Body;
56/// use rumtk_core::{rumtk_spawn_task, rumtk_resolve_task};
57/// use rumtk_web::utils::testdata::TESTDATA_FORMDATA_REQUEST;
58/// use rumtk_web::utils::RouterForm;
59/// use rumtk_web::utils::form_data::compile_form_data;
60/// use rumtk_web::FormData;
61/// use axum::extract::{Request, FromRequest};
62/// use rumtk_core::core::RUMResult;
63/// use rumtk_core::types::RUMBuffer;
64/// use rumtk_web::form_data::FormResult;
65///
66/// let expected_form = FormData::default();
67///
68/// async fn create_form() -> FormResult {
69/// let mut raw_form = RouterForm::from_request(TESTDATA_FORMDATA_REQUEST(), &()).await.expect("Multipart form expected.");
70/// compile_form_data(&mut raw_form).await
71/// }
72///
73/// rumtk_resolve_task!(create_form());
74///
75/// ```
76///
77/// ## Note
78/// ```text
79/// Because anything that axum does not like could trigger a truncation of the incoming form, I
80/// could not even test this function without silencing the parsing error and returning any successful
81/// results so far. Axum would complain about an error parsing the multipart form when using a "mocked" Body buffer.
82/// Turns out, you can still properly parse a buffer. Also, for testing purposes, you cannot use byte
83/// literals as the input to a mocked Body but you can use a Vec<u8> and write!() to it then call
84/// into() on that buffer and everything then works despite still complaining about the error.
85/// Since we are ignoring anything past this point, I think this is technically safe while still
86/// allowing us to test this logic.
87/// ```
88///
89pub async fn compile_form_data(form: &mut RouterForm) -> FormResult {
90 let mut form_data = FormData::default();
91 while let field_result = form.next_field().await {
92 match field_result {
93 Ok(field_option) => match field_option {
94 Some(mut field) => {
95 let typ = match field.content_type() {
96 Some(content_type) => get_type(content_type).await,
97 None => FORM_DATA_TYPE_DEFAULT,
98 };
99 let name = field.name().unwrap_or_default().to_rumstring();
100
101 let data = match field.bytes().await {
102 Ok(bytes) => bytes,
103 Err(e) => {
104 return Err(rumtk_format!("Field data transfer failed because {}!", e))
105 }
106 };
107
108 if typ == FORM_DATA_TYPE_DEFAULT {
109 form_data.form.insert(name, data.to_vec().to_rumstring());
110 } else {
111 let file_id = RUMID::new_v4().to_compact_string();
112 &form_data.files.insert(file_id.clone(), data);
113 &form_data.form.insert(name, file_id);
114 }
115 }
116 _ => {}
117 },
118 Err(e) => {
119 // Just return what you got. This is tricky, because anything that axum does not like could
120 // trigger a truncation of the incoming form, but I could not even test this function without
121 // doing this because it would complain about an error parsing the multipart form. Turns out,
122 // you can still properly parse a buffer. Also, for testing purposes, you cannot use byte
123 // literals as the input to a mocked Body but you can use a Vec<u8> and write!() to it then
124 // call into() on that buffer and everything then works despite still complaining about the error.
125 // Since we are ignoring anything past this point, I think this is technically safe while still
126 // allowing us to test this logic.
127 return Ok(form_data);
128 }
129 }
130 }
131
132 Ok(form_data)
133}