Skip to main content

iceberg_catalog_rest/
request.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! The request type REST catalog authentication works with.
19
20use http::{HeaderMap, Method};
21use iceberg::Result;
22use reqwest::{Request, RequestBuilder};
23
24/// An outgoing REST request being authenticated by an
25/// [`AuthSession`](crate::AuthSession).
26///
27/// Wraps the request so a session mutates it through the stable
28/// `http` crate types rather than the concrete request type the REST catalog
29/// uses internally.
30pub struct HttpRequest {
31    inner: Request,
32}
33
34impl HttpRequest {
35    /// Wraps a request, e.g. to unit-test a custom
36    /// [`AuthSession`](crate::AuthSession).
37    pub fn new(inner: Request) -> Self {
38        Self { inner }
39    }
40
41    /// Builds the request `builder` describes.
42    pub(crate) fn build(builder: RequestBuilder) -> Result<Self> {
43        Ok(Self::new(builder.build()?))
44    }
45
46    /// The wrapped request, for the client that sends it.
47    pub(crate) fn into_inner(self) -> Request {
48        self.inner
49    }
50
51    /// The request method.
52    pub fn method(&self) -> &Method {
53        self.inner.method()
54    }
55
56    /// The request URL, as a string (scheme, host, path and query).
57    pub fn url_str(&self) -> &str {
58        self.inner.url().as_str()
59    }
60
61    /// The request headers.
62    pub fn headers(&self) -> &HeaderMap {
63        self.inner.headers()
64    }
65
66    /// The mutable request headers, e.g. to add an `Authorization` header.
67    pub fn headers_mut(&mut self) -> &mut HeaderMap {
68        self.inner.headers_mut()
69    }
70
71    /// The request body, distinguishing an absent body from a streaming one:
72    /// signers can sign [`HttpRequestBody::Empty`] (empty-payload hash) and
73    /// [`HttpRequestBody::Buffered`], but not [`HttpRequestBody::Streaming`].
74    pub fn body(&self) -> HttpRequestBody<'_> {
75        match self.inner.body() {
76            None => HttpRequestBody::Empty,
77            Some(body) => match body.as_bytes() {
78                Some(bytes) => HttpRequestBody::Buffered(bytes),
79                None => HttpRequestBody::Streaming,
80            },
81        }
82    }
83}
84
85/// The body of an [`HttpRequest`], as seen by authentication.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum HttpRequestBody<'a> {
88    /// No body is set.
89    Empty,
90    /// An in-memory body.
91    Buffered(&'a [u8]),
92    /// A streaming body, whose bytes are not available for e.g. signing.
93    Streaming,
94}
95
96impl<'a> HttpRequestBody<'a> {
97    /// The signable bytes: empty for [`Self::Empty`], the buffer for
98    /// [`Self::Buffered`], and `None` for [`Self::Streaming`].
99    pub fn as_bytes(&self) -> Option<&'a [u8]> {
100        match self {
101            HttpRequestBody::Empty => Some(&[]),
102            HttpRequestBody::Buffered(bytes) => Some(bytes),
103            HttpRequestBody::Streaming => None,
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use reqwest::Client;
111
112    use super::*;
113
114    #[test]
115    fn test_http_request_body_states() {
116        let client = Client::new();
117
118        // No body at all.
119        let req = client
120            .get("https://rest.example.com/v1/config")
121            .build()
122            .unwrap();
123        let http_req = HttpRequest::new(req);
124        let body = http_req.body();
125        assert_eq!(body, HttpRequestBody::Empty);
126        assert_eq!(body.as_bytes(), Some(&[] as &[u8]));
127
128        // An in-memory body.
129        let req = client
130            .post("https://rest.example.com/v1/namespaces")
131            .body("{}")
132            .build()
133            .unwrap();
134        let http_req = HttpRequest::new(req);
135        let body = http_req.body();
136        assert_eq!(body, HttpRequestBody::Buffered(b"{}"));
137        assert_eq!(body.as_bytes(), Some(b"{}" as &[u8]));
138
139        // A streaming body: bytes are unavailable, so it must not sign as empty.
140        let req = client
141            .post("https://rest.example.com/v1/namespaces")
142            .body(reqwest::Body::wrap_stream(futures::stream::once(async {
143                Ok::<_, std::io::Error>(bytes::Bytes::from_static(b"chunk"))
144            })))
145            .build()
146            .unwrap();
147        let http_req = HttpRequest::new(req);
148        let body = http_req.body();
149        assert_eq!(body, HttpRequestBody::Streaming);
150        assert_eq!(body.as_bytes(), None);
151    }
152}