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