Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/skade

vendor/iceberg-catalog-rest-arrow59/src/request.rs

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

//! The request type REST catalog authentication works with.

use http::{HeaderMap, Method};
use iceberg::Result;
use reqwest::{Request, RequestBuilder};

/// An outgoing REST request being authenticated by an
/// [`AuthSession`](crate::AuthSession).
///
/// Wraps the request so a session mutates it through the stable
/// `http` crate types rather than the concrete request type the REST catalog
/// uses internally.
pub struct HttpRequest {
    inner: Request,
}

impl HttpRequest {
    /// Wraps a request, e.g. to unit-test a custom
    /// [`AuthSession`](crate::AuthSession).
    pub fn new(inner: Request) -> Self {
        Self { inner }
    }

    /// Builds the request `builder` describes.
    pub(crate) fn build(builder: RequestBuilder) -> Result<Self> {
        Ok(Self::new(builder.build()?))
    }

    /// The wrapped request, for the client that sends it.
    pub(crate) fn into_inner(self) -> Request {
        self.inner
    }

    /// The request method.
    pub fn method(&self) -> &Method {
        self.inner.method()
    }

    /// The request URL, as a string (scheme, host, path and query).
    pub fn url_str(&self) -> &str {
        self.inner.url().as_str()
    }

    /// The request headers.
    pub fn headers(&self) -> &HeaderMap {
        self.inner.headers()
    }

    /// The mutable request headers, e.g. to add an `Authorization` header.
    pub fn headers_mut(&mut self) -> &mut HeaderMap {
        self.inner.headers_mut()
    }

    /// The request body, distinguishing an absent body from a streaming one:
    /// signers can sign [`HttpRequestBody::Empty`] (empty-payload hash) and
    /// [`HttpRequestBody::Buffered`], but not [`HttpRequestBody::Streaming`].
    pub fn body(&self) -> HttpRequestBody<'_> {
        match self.inner.body() {
            None => HttpRequestBody::Empty,
            Some(body) => match body.as_bytes() {
                Some(bytes) => HttpRequestBody::Buffered(bytes),
                None => HttpRequestBody::Streaming,
            },
        }
    }
}

/// The body of an [`HttpRequest`], as seen by authentication.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpRequestBody<'a> {
    /// No body is set.
    Empty,
    /// An in-memory body.
    Buffered(&'a [u8]),
    /// A streaming body, whose bytes are not available for e.g. signing.
    Streaming,
}

impl<'a> HttpRequestBody<'a> {
    /// The signable bytes: empty for [`Self::Empty`], the buffer for
    /// [`Self::Buffered`], and `None` for [`Self::Streaming`].
    pub fn as_bytes(&self) -> Option<&'a [u8]> {
        match self {
            HttpRequestBody::Empty => Some(&[]),
            HttpRequestBody::Buffered(bytes) => Some(bytes),
            HttpRequestBody::Streaming => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use reqwest::Client;

    use super::*;

    #[test]
    fn test_http_request_body_states() {
        let client = Client::new();

        // No body at all.
        let req = client
            .get("https://rest.example.com/v1/config")
            .build()
            .unwrap();
        let http_req = HttpRequest::new(req);
        let body = http_req.body();
        assert_eq!(body, HttpRequestBody::Empty);
        assert_eq!(body.as_bytes(), Some(&[] as &[u8]));

        // An in-memory body.
        let req = client
            .post("https://rest.example.com/v1/namespaces")
            .body("{}")
            .build()
            .unwrap();
        let http_req = HttpRequest::new(req);
        let body = http_req.body();
        assert_eq!(body, HttpRequestBody::Buffered(b"{}"));
        assert_eq!(body.as_bytes(), Some(b"{}" as &[u8]));

        // A streaming body: bytes are unavailable, so it must not sign as empty.
        let req = client
            .post("https://rest.example.com/v1/namespaces")
            .body(reqwest::Body::wrap_stream(futures::stream::once(async {
                Ok::<_, std::io::Error>(bytes::Bytes::from_static(b"chunk"))
            })))
            .build()
            .unwrap();
        let http_req = HttpRequest::new(req);
        let body = http_req.body();
        assert_eq!(body, HttpRequestBody::Streaming);
        assert_eq!(body.as_bytes(), None);
    }
}