Skip to content

Feature Request: Support Iterator<Item=Cow<'static, str>> #508

Open

Description

@NfNitLoop

Somewhat related to #491, but not exactly the same.

Streaming output is nice, but there's an easy middle-ground that works well when you're rendering templates that are mostly static, but have some dynamic parts. If you can shove everything into a Vec<Cow<'static, str>>, you only have to keep the dynamic parts in memory, and the rest can be references to &'static str fragments.

I'm using this in Axum with a custom type that implements IntoResponse, and it's working well. (I'll attach the code in a comment below in case it's useful to anyone.)

It would be great if maud's Render had a way to get an Iterator<Item=Cow<'static, str>> which would let me easily slot it into that system.

Not a blocker for me at all, though. Maud's great, thanks for this library! <3

Activity

  1. NfNitLoop commented on Oct 22, 2026

    @NfNitLoop
    Author
    Example of using Cow<'static, str> in an Axum Response.
    use std::{borrow::Cow, convert::Infallible};
    
    use axum::{
        body::{Body, Bytes},
        http::{
            HeaderMap, HeaderValue,
            header::{CONTENT_LENGTH, CONTENT_TYPE},
        },
        response::{IntoResponse, Response},
    };
    use tokio_stream::StreamExt as _;
    
    /// Efficient body that concatenates string parts as they are output to the client.
    /// (vs. requiring string concatenation in memory.)
    ///
    /// Sets Content-Length based on the length of all string parts.
    /// Sets Conent-Type depending on which constructor was used.
    #[derive(Debug)]
    pub struct BodyParts {
        existing_headers: Option<HeaderMap>,
        content_type: Option<HeaderValue>,
        parts: Vec<CowStr>,
    }
    
    impl BodyParts {
        /// Init & set the Content-Type to `text/html; charset=UTF-8`
        pub fn html() -> Self {
            Self {
                existing_headers: None,
                content_type: Some(HeaderValue::from_static("text/html;charset=UTF-8")),
                parts: Vec::new(),
            }
        }
    
        /// Add some pre-existing headers to our output Response.
        /// Content-Type and -Length will be overwritten.
        pub fn with_existing_headers(mut self, headers: HeaderMap) -> Self {
            self.existing_headers = Some(headers);
            self
        }
    
        /// Add some string output.
        pub fn push(&mut self, value: impl Into<Cow<'static, str>>) {
            self.parts.push(value.into().into());
        }
    }
    
    impl IntoResponse for BodyParts {
        fn into_response(self) -> Response {
            let Self {
                existing_headers,
                content_type,
                parts,
            } = self;
    
            let content_length: usize = parts.iter().map(|it| it.inner.as_bytes().len()).sum();
    
            let parts = tokio_stream::iter(parts).map(always_ok);
            let body = Body::from_stream(parts);
            let mut response = Response::new(body);
    
            if let Some(headers) = existing_headers {
                *response.headers_mut() = headers;
            }
    
            if let Some(content_type) = content_type {
                response.headers_mut().insert(CONTENT_TYPE, content_type);
            }
            response
                .headers_mut()
                .insert(CONTENT_LENGTH, content_length.into());
    
            response
        }
    }
    
    fn always_ok<T>(value: T) -> Result<T, Infallible> {
        Ok(value)
    }
    
    // Axum requires that Stream::Item impl Into<Bytes>.
    // Bytes impls From<'static str> and From<String> but not From<Cow<'static, str>>,
    // so we have to write our own.
    // See: https://github.com/tokio-rs/bytes/issues/861
    #[derive(Debug)]
    struct CowStr {
        inner: Cow<'static, str>,
    }
    
    impl From<Cow<'static, str>> for CowStr {
        fn from(value: Cow<'static, str>) -> Self {
            Self { inner: value }
        }
    }
    
    impl Into<Bytes> for CowStr {
        fn into(self) -> Bytes {
            match self.inner {
                Cow::Borrowed(static_str) => Bytes::from(static_str),
                Cow::Owned(owned_string) => Bytes::from(owned_string),
            }
        }
    }

    Relevant to the other issue I mentioned, under the hood this does turn things into a Stream. But, it's all handled by the IntoResponse implementation. It's not as efficient as rolling your own, since we DO have to keep all of the dynamic bits in memory. But it's a nice compromise for usability.

    🤔 I suppose it might be even more efficient if we let it take Box<dyn Display>, then even more formatting could be postponed. But you get the gist, I hope. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions