1pub mod dom;
8
9pub use dom::{FilterOption, Row, RowActions, field_error, filter_options, row_actions, rows};
10use http_body_util::BodyExt;
11use topcoat::router::Body;
12
13pub fn cookie_header<'a>(cookies: impl IntoIterator<Item = (&'a str, &'a str)>) -> Option<String> {
15 let mut jar = String::new();
16 for (name, value) in cookies {
17 if !jar.is_empty() {
18 jar.push_str("; ");
19 }
20 jar.push_str(name);
21 jar.push('=');
22 jar.push_str(value);
23 }
24 (!jar.is_empty()).then_some(jar)
25}
26
27pub async fn body_bytes(response: http::Response<Body>) -> Vec<u8> {
29 response
30 .into_body()
31 .collect()
32 .await
33 .expect("collect body")
34 .to_bytes()
35 .to_vec()
36}
37
38pub async fn body_string(response: http::Response<Body>) -> String {
40 String::from_utf8_lossy(&body_bytes(response).await).into_owned()
41}
42
43pub fn form_body(pairs: &[(&str, &str)]) -> String {
45 let mut serializer = form_urlencoded::Serializer::new(String::new());
46 for (key, value) in pairs {
47 serializer.append_pair(key, value);
48 }
49 serializer.finish()
50}
51
52pub fn multipart_body(boundary: &str, parts: &[(&str, Option<&str>, &str)]) -> String {
55 let mut body = String::new();
56 for (name, filename, content) in parts {
57 body.push_str(&format!(
58 "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\""
59 ));
60 if let Some(filename) = filename {
61 body.push_str(&format!("; filename=\"{filename}\""));
62 }
63 body.push_str("\r\n\r\n");
64 body.push_str(content);
65 body.push_str("\r\n");
66 }
67 body.push_str(&format!("--{boundary}--\r\n"));
68 body
69}
70
71pub fn input_value(html: &str, name: &str) -> Option<String> {
74 let double = format!("name=\"{name}\"");
75 let single = format!("name='{name}'");
76 for tag in html.split('<').skip(1) {
77 if !tag.contains(&double) && !tag.contains(&single) {
78 continue;
79 }
80 let attrs = &tag[..tag.find('>')?];
81 for (prefix, term) in [("value=\"", '"'), ("value='", '\'')] {
82 if let Some(start) = attrs.find(prefix) {
83 let rest = &attrs[start + prefix.len()..];
84 if let Some(end) = rest.find(term) {
85 let raw = &rest[..end];
87 return Some(raw.replace(""", "\"").replace("'", "'"));
88 }
89 }
90 }
91 }
92 None
93}
94
95pub const SESSION_COOKIE: &str = "__Host-session";
97
98pub fn response_cookies(response: &http::Response<Body>) -> Vec<(String, String)> {
100 response
101 .headers()
102 .get_all(http::header::SET_COOKIE)
103 .iter()
104 .filter_map(|value| value.to_str().ok())
105 .filter_map(|value| value.split(';').next())
106 .filter_map(|pair| pair.split_once('='))
107 .map(|(name, value)| (name.trim().to_string(), value.trim().to_string()))
108 .collect()
109}
110
111pub fn set_cookie_header(response: &http::Response<Body>, name: &str) -> Option<String> {
113 response
114 .headers()
115 .get_all(http::header::SET_COOKIE)
116 .iter()
117 .filter_map(|value| value.to_str().ok())
118 .find(|value| value.starts_with(&format!("{name}=")))
119 .map(str::to_string)
120}
121
122pub fn session_cookie_value(response: &http::Response<Body>) -> Option<String> {
124 response_cookies(response)
125 .into_iter()
126 .find(|(name, _)| name == SESSION_COOKIE)
127 .map(|(_, value)| value)
128}