1use crate::session::{Session, SessionConfig};
45use crate::url::{UrlComponents, crack_url};
46use windows::core::Result;
47
48#[derive(Debug, Clone)]
79pub struct Body {
80 bytes: Vec<u8>,
81 content_type: Option<&'static str>,
82}
83
84impl Body {
85 #[cfg(feature = "json")]
108 pub fn json<T: serde::Serialize>(value: &T) -> Result<Self> {
109 let bytes = serde_json::to_vec(value).map_err(|err| {
110 windows::core::Error::new(
111 windows::Win32::Foundation::E_FAIL,
112 format!("JSON serialization failed: {err}"),
113 )
114 })?;
115 Ok(Self {
116 bytes,
117 content_type: Some("application/json"),
118 })
119 }
120
121 #[must_use]
123 pub fn as_bytes(&self) -> &[u8] {
124 &self.bytes
125 }
126
127 #[must_use]
129 pub fn content_type(&self) -> Option<&'static str> {
130 self.content_type
131 }
132}
133
134impl From<&[u8]> for Body {
135 fn from(bytes: &[u8]) -> Self {
136 Self {
137 bytes: bytes.to_vec(),
138 content_type: None,
139 }
140 }
141}
142
143impl<const N: usize> From<&[u8; N]> for Body {
144 fn from(bytes: &[u8; N]) -> Self {
145 Self {
146 bytes: bytes.to_vec(),
147 content_type: None,
148 }
149 }
150}
151
152impl From<Vec<u8>> for Body {
153 fn from(bytes: Vec<u8>) -> Self {
154 Self {
155 bytes,
156 content_type: None,
157 }
158 }
159}
160
161impl From<&str> for Body {
162 fn from(s: &str) -> Self {
163 Self {
164 bytes: s.as_bytes().to_vec(),
165 content_type: None,
166 }
167 }
168}
169
170impl From<String> for Body {
171 fn from(s: String) -> Self {
172 Self {
173 bytes: s.into_bytes(),
174 content_type: None,
175 }
176 }
177}
178
179fn body_headers(body: &Body) -> Vec<(String, String)> {
181 body.content_type
182 .map(|ct| vec![("Content-Type".to_string(), ct.to_string())])
183 .unwrap_or_default()
184}
185
186#[derive(Debug, Clone)]
192pub struct Response {
193 pub status: u16,
195 pub status_text: String,
197 pub headers: String,
199 pub body: Vec<u8>,
201}
202
203impl Response {
204 #[must_use]
206 pub fn text(&self) -> String {
207 String::from_utf8_lossy(&self.body).into_owned()
208 }
209
210 #[must_use]
212 pub fn is_success(&self) -> bool {
213 (200..300).contains(&self.status)
214 }
215
216 #[must_use]
218 pub fn is_redirect(&self) -> bool {
219 (300..400).contains(&self.status)
220 }
221
222 #[must_use]
224 pub fn is_client_error(&self) -> bool {
225 (400..500).contains(&self.status)
226 }
227
228 #[must_use]
230 pub fn is_server_error(&self) -> bool {
231 (500..600).contains(&self.status)
232 }
233
234 #[cfg(feature = "json")]
248 pub fn json<T: serde::de::DeserializeOwned>(
249 &self,
250 ) -> std::result::Result<T, serde_json::Error> {
251 serde_json::from_slice(&self.body)
252 }
253}
254
255pub struct ClientBuilder {
276 base_url: Option<String>,
277 user_agent: String,
278 connect_timeout_ms: u32,
279 send_timeout_ms: u32,
280 receive_timeout_ms: u32,
281}
282
283impl ClientBuilder {
284 #[must_use]
292 pub fn base_url(mut self, url: impl Into<String>) -> Self {
293 let mut url = url.into();
294 while url.ends_with('/') {
296 url.pop();
297 }
298 self.base_url = Some(url);
299 self
300 }
301
302 #[must_use]
304 pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
305 self.user_agent = agent.into();
306 self
307 }
308
309 #[must_use]
311 pub fn connect_timeout_ms(mut self, ms: u32) -> Self {
312 self.connect_timeout_ms = ms;
313 self
314 }
315
316 #[must_use]
318 pub fn send_timeout_ms(mut self, ms: u32) -> Self {
319 self.send_timeout_ms = ms;
320 self
321 }
322
323 #[must_use]
325 pub fn receive_timeout_ms(mut self, ms: u32) -> Self {
326 self.receive_timeout_ms = ms;
327 self
328 }
329
330 pub fn build(self) -> Result<Client> {
335 let base_components = match &self.base_url {
336 Some(url) => Some(crack_url(url)?),
337 None => None,
338 };
339
340 let config = SessionConfig {
341 user_agent: self.user_agent,
342 connect_timeout_ms: self.connect_timeout_ms,
343 send_timeout_ms: self.send_timeout_ms,
344 receive_timeout_ms: self.receive_timeout_ms,
345 };
346
347 let session = Session::with_config(config.clone())?;
348
349 #[cfg(feature = "async")]
350 let async_session = Session::with_config_async(config)?;
351
352 Ok(Client {
353 base_url: self.base_url,
354 base_components,
355 session,
356 #[cfg(feature = "async")]
357 async_session,
358 })
359 }
360}
361
362pub struct RequestHelper<'c> {
385 client: &'c Client,
386 method: String,
387 url: String,
388 headers: Vec<(String, String)>,
389 body: Option<Body>,
390}
391
392impl RequestHelper<'_> {
393 #[must_use]
395 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
396 self.headers.push((name.into(), value.into()));
397 self
398 }
399
400 #[must_use]
434 pub fn body(mut self, data: impl Into<Body>) -> Self {
435 self.body = Some(data.into());
436 self
437 }
438
439 pub fn send(self) -> Result<Response> {
441 let (body_headers, body_bytes) = split_body(self.body);
442 let mut all_headers = body_headers;
443 all_headers.extend(self.headers);
444 self.client
445 .execute(&self.method, &self.url, &all_headers, body_bytes.as_deref())
446 }
447
448 #[cfg(feature = "async")]
453 pub async fn send_async(self) -> Result<Response> {
454 let (body_headers, body_bytes) = split_body(self.body);
455 let mut all_headers = body_headers;
456 all_headers.extend(self.headers);
457 self.client
458 .execute_async(&self.method, &self.url, &all_headers, body_bytes)
459 .await
460 }
461}
462
463fn split_body(body: Option<Body>) -> (Vec<(String, String)>, Option<Vec<u8>>) {
465 match body {
466 Some(body) => {
467 let headers = body_headers(&body);
468 (headers, Some(body.bytes))
469 }
470 None => (Vec::new(), None),
471 }
472}
473
474pub struct Client {
500 base_url: Option<String>,
501 base_components: Option<UrlComponents>,
502 session: Session,
503 #[cfg(feature = "async")]
504 async_session: Session,
505}
506
507impl Client {
508 pub fn new() -> Result<Self> {
512 Self::builder().build()
513 }
514
515 #[must_use]
529 pub fn builder() -> ClientBuilder {
530 ClientBuilder {
531 base_url: None,
532 user_agent: "winhttp-rs/0.1.0".to_string(),
533 connect_timeout_ms: 60_000,
534 send_timeout_ms: 30_000,
535 receive_timeout_ms: 30_000,
536 }
537 }
538
539 #[must_use]
541 pub fn session(&self) -> &Session {
542 &self.session
543 }
544
545 #[cfg(feature = "async")]
547 #[must_use]
548 pub fn async_session(&self) -> &Session {
549 &self.async_session
550 }
551
552 #[must_use]
554 pub fn base_url(&self) -> Option<&str> {
555 self.base_url.as_deref()
556 }
557
558 fn resolve_url(&self, url: &str) -> Result<UrlComponents> {
566 if let Ok(components) = crack_url(url) {
568 return Ok(components);
569 }
570
571 let Some(base) = &self.base_components else {
573 return crack_url(url);
576 };
577
578 let mut components = base.clone();
579
580 let (path_part, extra_part) = match url.find(['?', '#']) {
582 Some(i) => (&url[..i], &url[i..]),
583 None => (url, ""),
584 };
585
586 if path_part.starts_with('/') {
587 components.path = path_part.to_string();
588 } else {
589 let base_path = components.path.trim_end_matches('/');
590 components.path = format!("{base_path}/{path_part}");
591 }
592 components.extra_info = extra_part.to_string();
593
594 Ok(components)
595 }
596
597 #[must_use]
617 pub fn request(&self, method: &str, url: &str) -> RequestHelper<'_> {
618 RequestHelper {
619 client: self,
620 method: method.to_string(),
621 url: url.to_string(),
622 headers: Vec::new(),
623 body: None,
624 }
625 }
626
627 pub fn get(&self, url: &str) -> Result<Response> {
633 self.execute("GET", url, &[], None)
634 }
635
636 pub fn post(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
643 let body = body.into();
644 let headers = body_headers(&body);
645 self.execute("POST", url, &headers, Some(&body.bytes))
646 }
647
648 pub fn put(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
654 let body = body.into();
655 let headers = body_headers(&body);
656 self.execute("PUT", url, &headers, Some(&body.bytes))
657 }
658
659 pub fn delete(&self, url: &str) -> Result<Response> {
663 self.execute("DELETE", url, &[], None)
664 }
665
666 pub fn patch(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
672 let body = body.into();
673 let headers = body_headers(&body);
674 self.execute("PATCH", url, &headers, Some(&body.bytes))
675 }
676
677 pub fn head(&self, url: &str) -> Result<Response> {
684 self.execute_head(url)
685 }
686
687 #[cfg(feature = "async")]
693 pub async fn async_get(&self, url: &str) -> Result<Response> {
694 self.execute_async("GET", url, &[], None).await
695 }
696
697 #[cfg(feature = "async")]
704 pub async fn async_post(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
705 let body = body.into();
706 let headers = body_headers(&body);
707 self.execute_async("POST", url, &headers, Some(body.bytes))
708 .await
709 }
710
711 #[cfg(feature = "async")]
717 pub async fn async_put(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
718 let body = body.into();
719 let headers = body_headers(&body);
720 self.execute_async("PUT", url, &headers, Some(body.bytes))
721 .await
722 }
723
724 #[cfg(feature = "async")]
728 pub async fn async_delete(&self, url: &str) -> Result<Response> {
729 self.execute_async("DELETE", url, &[], None).await
730 }
731
732 #[cfg(feature = "async")]
738 pub async fn async_patch(&self, url: &str, body: impl Into<Body>) -> Result<Response> {
739 let body = body.into();
740 let headers = body_headers(&body);
741 self.execute_async("PATCH", url, &headers, Some(body.bytes))
742 .await
743 }
744
745 #[cfg(feature = "async")]
749 pub async fn async_head(&self, url: &str) -> Result<Response> {
750 self.execute_async_head(url).await
751 }
752
753 fn execute(
756 &self,
757 method: &str,
758 url: &str,
759 headers: &[(String, String)],
760 body: Option<&[u8]>,
761 ) -> Result<Response> {
762 let components = self.resolve_url(url)?;
763 let secure = components.scheme.eq_ignore_ascii_case("https");
764 let path_and_query = if components.extra_info.is_empty() {
765 components.path.clone()
766 } else {
767 format!("{}{}", components.path, components.extra_info)
768 };
769
770 let connection = self.session.connect(&components.host, components.port)?;
771
772 let mut builder = connection.request(method, &path_and_query);
773 if secure {
774 builder = builder.secure();
775 }
776 for (name, value) in headers {
777 builder = builder.header(name, value);
778 }
779 let request = builder.build()?;
780
781 match body {
782 Some(data) if !data.is_empty() => request.send_with_body(data)?,
783 _ => request.send()?,
784 }
785 request.receive_response()?;
786
787 let status = request.status_code()?;
788 let status_text = request.status_text().unwrap_or_default();
789 let resp_headers = request.raw_headers().unwrap_or_default();
790 let resp_body = request.read_all()?;
791
792 Ok(Response {
793 status,
794 status_text,
795 headers: resp_headers,
796 body: resp_body,
797 })
798 }
799
800 fn execute_head(&self, url: &str) -> Result<Response> {
801 let components = self.resolve_url(url)?;
802 let secure = components.scheme.eq_ignore_ascii_case("https");
803 let path_and_query = if components.extra_info.is_empty() {
804 components.path.clone()
805 } else {
806 format!("{}{}", components.path, components.extra_info)
807 };
808
809 let connection = self.session.connect(&components.host, components.port)?;
810 let mut builder = connection.request("HEAD", &path_and_query);
811 if secure {
812 builder = builder.secure();
813 }
814 let request = builder.build()?;
815 request.send()?;
816 request.receive_response()?;
817
818 let status = request.status_code()?;
819 let status_text = request.status_text().unwrap_or_default();
820 let resp_headers = request.raw_headers().unwrap_or_default();
821
822 Ok(Response {
823 status,
824 status_text,
825 headers: resp_headers,
826 body: Vec::new(),
827 })
828 }
829
830 #[cfg(feature = "async")]
831 async fn execute_async(
832 &self,
833 method: &str,
834 url: &str,
835 headers: &[(String, String)],
836 body: Option<Vec<u8>>,
837 ) -> Result<Response> {
838 let components = self.resolve_url(url)?;
839 let secure = components.scheme.eq_ignore_ascii_case("https");
840 let path_and_query = if components.extra_info.is_empty() {
841 components.path.clone()
842 } else {
843 format!("{}{}", components.path, components.extra_info)
844 };
845
846 let connection = self
847 .async_session
848 .connect(&components.host, components.port)?;
849
850 let mut builder = connection.request(method, &path_and_query);
851 if secure {
852 builder = builder.secure();
853 }
854 for (name, value) in headers {
855 builder = builder.header(name, value);
856 }
857 let request = builder.build()?;
858 let async_request = request.into_async()?;
859
860 let response = match body {
861 Some(data) if !data.is_empty() => async_request.send_with_body(data).await?,
862 _ => async_request.send().await?,
863 };
864
865 let status = response.status_code()?;
866 let status_text = response.status_text().unwrap_or_default();
867 let resp_headers = response.raw_headers().unwrap_or_default();
868 let resp_body = response.read_all().await?;
869
870 Ok(Response {
871 status,
872 status_text,
873 headers: resp_headers,
874 body: resp_body,
875 })
876 }
877
878 #[cfg(feature = "async")]
879 async fn execute_async_head(&self, url: &str) -> Result<Response> {
880 let components = self.resolve_url(url)?;
881 let secure = components.scheme.eq_ignore_ascii_case("https");
882 let path_and_query = if components.extra_info.is_empty() {
883 components.path.clone()
884 } else {
885 format!("{}{}", components.path, components.extra_info)
886 };
887
888 let connection = self
889 .async_session
890 .connect(&components.host, components.port)?;
891 let mut builder = connection.request("HEAD", &path_and_query);
892 if secure {
893 builder = builder.secure();
894 }
895 let request = builder.build()?;
896 let async_request = request.into_async()?;
897 let response = async_request.send().await?;
898
899 let status = response.status_code()?;
900 let status_text = response.status_text().unwrap_or_default();
901 let resp_headers = response.raw_headers().unwrap_or_default();
902
903 Ok(Response {
904 status,
905 status_text,
906 headers: resp_headers,
907 body: Vec::new(),
908 })
909 }
910}
911
912pub fn get(url: &str) -> Result<Response> {
930 Client::new()?.get(url)
931}
932
933pub fn post(url: &str, body: impl Into<Body>) -> Result<Response> {
945 Client::new()?.post(url, body)
946}
947
948pub fn put(url: &str, body: impl Into<Body>) -> Result<Response> {
950 Client::new()?.put(url, body)
951}
952
953pub fn delete(url: &str) -> Result<Response> {
955 Client::new()?.delete(url)
956}
957
958pub fn patch(url: &str, body: impl Into<Body>) -> Result<Response> {
960 Client::new()?.patch(url, body)
961}
962
963pub fn head(url: &str) -> Result<Response> {
965 Client::new()?.head(url)
966}
967
968#[cfg(test)]
973mod tests {
974 use super::*;
975
976 #[test]
977 fn test_response_status_checks() {
978 let resp = Response {
979 status: 200,
980 status_text: "OK".to_string(),
981 headers: String::new(),
982 body: b"hello".to_vec(),
983 };
984 assert!(resp.is_success());
985 assert!(!resp.is_redirect());
986 assert!(!resp.is_client_error());
987 assert!(!resp.is_server_error());
988 assert_eq!(resp.text(), "hello");
989 }
990
991 #[test]
992 fn test_response_redirect() {
993 let resp = Response {
994 status: 301,
995 status_text: "Moved Permanently".to_string(),
996 headers: String::new(),
997 body: Vec::new(),
998 };
999 assert!(!resp.is_success());
1000 assert!(resp.is_redirect());
1001 }
1002
1003 #[test]
1004 fn test_response_client_error() {
1005 let resp = Response {
1006 status: 404,
1007 status_text: "Not Found".to_string(),
1008 headers: String::new(),
1009 body: Vec::new(),
1010 };
1011 assert!(resp.is_client_error());
1012 assert!(!resp.is_server_error());
1013 }
1014
1015 #[test]
1016 fn test_response_server_error() {
1017 let resp = Response {
1018 status: 500,
1019 status_text: "Internal Server Error".to_string(),
1020 headers: String::new(),
1021 body: Vec::new(),
1022 };
1023 assert!(resp.is_server_error());
1024 assert!(!resp.is_client_error());
1025 }
1026
1027 #[test]
1028 fn test_client_creation() {
1029 let client = Client::new();
1030 assert!(client.is_ok());
1031 }
1032
1033 #[test]
1034 fn test_client_with_builder() {
1035 let client = Client::builder()
1036 .user_agent("test-client/1.0")
1037 .connect_timeout_ms(5000)
1038 .send_timeout_ms(3000)
1039 .receive_timeout_ms(10_000)
1040 .build();
1041 assert!(client.is_ok());
1042 }
1043
1044 #[test]
1045 fn test_client_with_base_url() {
1046 let client = Client::builder().base_url("https://httpbin.org").build();
1047 assert!(client.is_ok());
1048 let client = client.unwrap();
1049 assert_eq!(client.base_url(), Some("https://httpbin.org"));
1050 }
1051
1052 #[test]
1053 fn test_client_base_url_trailing_slash_trimmed() {
1054 let client = Client::builder()
1055 .base_url("https://httpbin.org///")
1056 .build()
1057 .unwrap();
1058 assert_eq!(client.base_url(), Some("https://httpbin.org"));
1059 }
1060
1061 #[test]
1062 fn test_resolve_url_absolute_bypasses_base() {
1063 let client = Client::builder()
1064 .base_url("https://base.example.com")
1065 .build()
1066 .unwrap();
1067 let resolved = client.resolve_url("https://other.com/foo").unwrap();
1068 assert_eq!(resolved.host, "other.com");
1069 assert_eq!(resolved.path, "/foo");
1070 }
1071
1072 #[test]
1073 fn test_resolve_url_relative_path() {
1074 let client = Client::builder()
1075 .base_url("https://base.example.com")
1076 .build()
1077 .unwrap();
1078 let resolved = client.resolve_url("/api/users").unwrap();
1079 assert_eq!(resolved.host, "base.example.com");
1080 assert_eq!(resolved.path, "/api/users");
1081 }
1082
1083 #[test]
1084 fn test_resolve_url_relative_no_slash() {
1085 let client = Client::builder()
1086 .base_url("https://base.example.com")
1087 .build()
1088 .unwrap();
1089 let resolved = client.resolve_url("api/users").unwrap();
1090 assert_eq!(resolved.host, "base.example.com");
1091 assert_eq!(resolved.path, "/api/users");
1092 }
1093
1094 #[test]
1095 fn test_resolve_url_no_base() {
1096 let client = Client::new().unwrap();
1097 let resolved = client.resolve_url("https://example.com/test").unwrap();
1098 assert_eq!(resolved.host, "example.com");
1099 assert_eq!(resolved.path, "/test");
1100 }
1101
1102 #[test]
1103 fn test_resolve_url_relative_with_query() {
1104 let client = Client::builder()
1105 .base_url("https://base.example.com")
1106 .build()
1107 .unwrap();
1108 let resolved = client.resolve_url("/search?q=hello&page=2").unwrap();
1109 assert_eq!(resolved.host, "base.example.com");
1110 assert_eq!(resolved.path, "/search");
1111 assert_eq!(resolved.extra_info, "?q=hello&page=2");
1112 }
1113
1114 #[test]
1115 fn test_resolve_url_relative_fails_without_base() {
1116 let client = Client::new().unwrap();
1117 let result = client.resolve_url("/relative/path");
1118 assert!(
1119 result.is_err(),
1120 "relative path without base URL should fail"
1121 );
1122 }
1123
1124 #[test]
1127 fn test_body_from_byte_slice() {
1128 let body: Body = b"hello".as_slice().into();
1129 assert_eq!(body.as_bytes(), b"hello");
1130 assert!(body.content_type().is_none());
1131 }
1132
1133 #[test]
1134 fn test_body_from_vec() {
1135 let body: Body = vec![1, 2, 3].into();
1136 assert_eq!(body.as_bytes(), &[1, 2, 3]);
1137 assert!(body.content_type().is_none());
1138 }
1139
1140 #[test]
1141 fn test_body_from_str() {
1142 let body: Body = "hello".into();
1143 assert_eq!(body.as_bytes(), b"hello");
1144 assert!(body.content_type().is_none());
1145 }
1146
1147 #[test]
1148 fn test_body_from_string() {
1149 let body: Body = String::from("hello").into();
1150 assert_eq!(body.as_bytes(), b"hello");
1151 assert!(body.content_type().is_none());
1152 }
1153
1154 #[test]
1155 fn test_body_headers_raw() {
1156 let body: Body = b"raw".as_slice().into();
1157 let headers = body_headers(&body);
1158 assert!(headers.is_empty());
1159 }
1160
1161 #[cfg(feature = "json")]
1162 #[test]
1163 fn test_body_json() {
1164 use serde::Serialize;
1165
1166 #[derive(Serialize)]
1167 struct Payload {
1168 key: String,
1169 count: u32,
1170 }
1171
1172 let body = Body::json(&Payload {
1173 key: "hello".into(),
1174 count: 42,
1175 })
1176 .expect("serialization should succeed");
1177
1178 assert_eq!(body.content_type(), Some("application/json"));
1179
1180 let parsed: serde_json::Value =
1181 serde_json::from_slice(body.as_bytes()).expect("should be valid JSON");
1182 assert_eq!(parsed["key"], "hello");
1183 assert_eq!(parsed["count"], 42);
1184 }
1185
1186 #[cfg(feature = "json")]
1187 #[test]
1188 fn test_body_json_headers() {
1189 let body = Body::json(&serde_json::json!({"a": 1})).expect("serialization should succeed");
1190 let headers = body_headers(&body);
1191 assert_eq!(headers.len(), 1);
1192 assert_eq!(headers[0].0, "Content-Type");
1193 assert_eq!(headers[0].1, "application/json");
1194 }
1195
1196 #[cfg(feature = "json")]
1197 #[test]
1198 fn test_response_json() {
1199 use serde::Deserialize;
1200
1201 #[derive(Deserialize)]
1202 struct Data {
1203 key: String,
1204 count: u32,
1205 }
1206
1207 let resp = Response {
1208 status: 200,
1209 status_text: "OK".to_string(),
1210 headers: String::new(),
1211 body: br#"{"key":"hello","count":42}"#.to_vec(),
1212 };
1213
1214 let data: Data = resp.json().expect("Failed to parse JSON");
1215 assert_eq!(data.key, "hello");
1216 assert_eq!(data.count, 42);
1217 }
1218
1219 #[cfg(feature = "json")]
1220 #[test]
1221 fn test_response_json_error() {
1222 let resp = Response {
1223 status: 200,
1224 status_text: "OK".to_string(),
1225 headers: String::new(),
1226 body: b"not json".to_vec(),
1227 };
1228
1229 let result: std::result::Result<serde_json::Value, _> = resp.json();
1230 assert!(result.is_err());
1231 }
1232}