Skip to main content

hackerone_api/
transport.rs

1//! Transport abstraction.
2//!
3//! The client depends on the [`Transport`] trait, not on a specific HTTP
4//! stack. [`UreqTransport`] is the default (`ureq`, blocking, TLS via
5//! rustls/native-tls depending on features), but embedders can supply their
6//! own — or a mock — by implementing the trait. Nothing else in the crate
7//! knows how bytes reach the network.
8
9use std::io::Read;
10use std::time::Duration;
11
12use serde::Serialize;
13
14use crate::error::{Error, Result};
15
16/// HTTP method.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Method {
19    /// `GET`
20    Get,
21    /// `POST`
22    Post,
23    /// `PUT`
24    Put,
25    /// `PATCH`
26    Patch,
27    /// `DELETE`
28    Delete,
29}
30
31impl Method {
32    /// The wire form of the method.
33    pub fn as_str(self) -> &'static str {
34        match self {
35            Method::Get => "GET",
36            Method::Post => "POST",
37            Method::Put => "PUT",
38            Method::Patch => "PATCH",
39            Method::Delete => "DELETE",
40        }
41    }
42}
43
44/// A fully-built request handed to a [`Transport`].
45#[derive(Debug, Clone)]
46pub struct Request {
47    /// HTTP method.
48    pub method: Method,
49    /// Absolute URL.
50    pub url: String,
51    /// Header name/value pairs.
52    pub headers: Vec<(String, String)>,
53    /// Serialized JSON body, if any.
54    pub body: Option<Vec<u8>>,
55}
56
57impl Request {
58    /// Start a request.
59    pub fn new(method: Method, url: impl Into<String>) -> Self {
60        Self {
61            method,
62            url: url.into(),
63            headers: Vec::new(),
64            body: None,
65        }
66    }
67
68    /// Add a header (builder style).
69    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
70        self.headers.push((name.into(), value.into()));
71        self
72    }
73
74    /// Set a JSON body (builder style).
75    pub fn body_json<T: Serialize>(mut self, value: &T) -> Result<Self> {
76        let bytes = serde_json::to_vec(value)
77            .map_err(|e| Error::Invalid(format!("serialize request body: {e}")))?;
78        self.body = Some(bytes);
79        Ok(self)
80    }
81
82    /// Look up a header value (case-insensitive).
83    pub fn header_value(&self, name: &str) -> Option<&str> {
84        self.headers
85            .iter()
86            .find(|(k, _)| k.eq_ignore_ascii_case(name))
87            .map(|(_, v)| v.as_str())
88    }
89}
90
91/// A raw HTTP response.
92#[derive(Debug, Clone)]
93pub struct Response {
94    /// Status code.
95    pub status: u16,
96    /// Raw response body.
97    pub body: Vec<u8>,
98}
99
100impl Response {
101    /// Construct a response (useful for tests).
102    pub fn new(status: u16, body: impl Into<Vec<u8>>) -> Self {
103        Self {
104            status,
105            body: body.into(),
106        }
107    }
108
109    /// The body as lossy UTF-8.
110    pub fn text(&self) -> String {
111        String::from_utf8_lossy(&self.body).into_owned()
112    }
113
114    /// Parse the body as JSON (empty bodies become `Value::Null`).
115    pub fn json(&self) -> Result<serde_json::Value> {
116        if self.body.is_empty() {
117            return Ok(serde_json::Value::Null);
118        }
119        serde_json::from_slice(&self.body)
120            .map_err(|e| Error::Decode(format!("invalid JSON response: {e}")))
121    }
122}
123
124/// Anything that can turn a [`Request`] into a [`Response`].
125pub trait Transport: Send + Sync {
126    /// Execute the request.
127    fn send(&self, request: &Request) -> Result<Response>;
128}
129
130/// Default transport backed by blocking `ureq`.
131pub struct UreqTransport {
132    agent: ureq::Agent,
133}
134
135impl UreqTransport {
136    /// A transport with the default 30s timeout.
137    pub fn new() -> Self {
138        Self::with_timeout(Duration::from_secs(30))
139    }
140
141    /// A transport with a custom timeout.
142    pub fn with_timeout(timeout: Duration) -> Self {
143        Self {
144            agent: ureq::AgentBuilder::new().timeout(timeout).build(),
145        }
146    }
147}
148
149impl Default for UreqTransport {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155impl Transport for UreqTransport {
156    fn send(&self, request: &Request) -> Result<Response> {
157        let mut req = self.agent.request(request.method.as_str(), &request.url);
158        for (name, value) in &request.headers {
159            req = req.set(name, value);
160        }
161
162        let result = match &request.body {
163            Some(bytes) => req.send_bytes(bytes),
164            None => req.call(),
165        };
166
167        match result {
168            Ok(resp) => {
169                let status = resp.status();
170                let mut body = Vec::new();
171                resp.into_reader()
172                    .read_to_end(&mut body)
173                    .map_err(|e| Error::Transport(e.to_string()))?;
174                Ok(Response { status, body })
175            }
176            // Non-2xx is a normal HTTP response, not a transport failure.
177            Err(ureq::Error::Status(code, resp)) => {
178                let mut body = Vec::new();
179                let _ = resp.into_reader().read_to_end(&mut body);
180                Ok(Response { status: code, body })
181            }
182            Err(ureq::Error::Transport(t)) => Err(Error::Transport(t.to_string())),
183        }
184    }
185}