predawn 0.9.0

Predawn is a Rust web framework like Spring Boot.
Documentation
use std::{
    collections::BTreeMap,
    marker::PhantomData,
    pin::Pin,
    task::{Context, Poll},
};

use bytes::Bytes;
use futures_core::{Stream, TryStream};
use futures_util::TryStreamExt;
use http::{
    header::{CACHE_CONTROL, CONTENT_TYPE},
    StatusCode,
};
use pin_project_lite::pin_project;
use predawn_core::{
    api_response::ApiResponse,
    body::ResponseBody,
    error::BoxError,
    into_response::IntoResponse,
    media_type::{MediaType, MultiResponseMediaType, ResponseMediaType, SingleMediaType},
    openapi::{self, AnySchema, ReferenceOr, Schema, SchemaKind},
    response::{MultiResponse, Response, SingleResponse},
};
use predawn_schema::ToSchema;
use serde::Serialize;

use super::{event::Event, keep_alive::KeepAlive};
use crate::{response::sse::keep_alive::KeepAliveStream, response_error::EventStreamError};

pub struct EventStream<T> {
    result: Result<Response, EventStreamError>,
    _marker: PhantomData<T>,
}

impl<T> EventStream<T> {
    pub fn new<S>(stream: S) -> Self
    where
        T: Serialize + Send + 'static,
        S: TryStream<Ok = T> + Send + 'static,
        S::Error: Into<BoxError>,
    {
        Self::builder().build(stream)
    }

    pub fn builder() -> EventStreamBuilder<DefaultOnCreateEvent<T>> {
        EventStreamBuilder {
            keep_alive: None,
            _marker: PhantomData,
        }
    }
}

impl<T> IntoResponse for EventStream<T> {
    type Error = EventStreamError;

    fn into_response(self) -> Result<Response, Self::Error> {
        self.result
    }
}

impl<T> MediaType for EventStream<T> {
    const MEDIA_TYPE: &'static str = "text/event-stream";
}

impl<T> ResponseMediaType for EventStream<T> {}

impl<T: ToSchema> SingleMediaType for EventStream<T> {
    fn media_type(
        schemas: &mut BTreeMap<String, openapi::Schema>,
        schemas_in_progress: &mut Vec<String>,
    ) -> openapi::MediaType {
        let schema = Schema {
            schema_data: Default::default(),
            schema_kind: SchemaKind::Any(AnySchema {
                typ: Some("array".into()),
                items: Some(T::schema_ref_box(schemas, schemas_in_progress)),
                format: Some("event-stream".into()),
                ..Default::default()
            }),
        };

        openapi::MediaType {
            schema: Some(ReferenceOr::Item(schema)),
            ..Default::default()
        }
    }
}

impl<T: ToSchema> SingleResponse for EventStream<T> {
    fn response(
        schemas: &mut BTreeMap<String, Schema>,
        schemas_in_progress: &mut Vec<String>,
    ) -> openapi::Response {
        openapi::Response {
            content: <Self as MultiResponseMediaType>::content(schemas, schemas_in_progress),
            ..Default::default()
        }
    }
}

impl<T: ToSchema> ApiResponse for EventStream<T> {
    fn responses(
        schemas: &mut BTreeMap<String, Schema>,
        schemas_in_progress: &mut Vec<String>,
    ) -> Option<BTreeMap<StatusCode, openapi::Response>> {
        Some(<Self as MultiResponse>::responses(
            schemas,
            schemas_in_progress,
        ))
    }
}

pub struct EventStreamBuilder<F> {
    keep_alive: Option<KeepAlive>,
    _marker: PhantomData<F>,
}

impl<F> EventStreamBuilder<F> {
    pub fn keep_alive(mut self, keep_alive: KeepAlive) -> Self {
        self.keep_alive = Some(keep_alive);
        self
    }
}

impl<F: OnCreateEvent> EventStreamBuilder<F> {
    pub fn on_create_event<C>(self) -> EventStreamBuilder<C>
    where
        C: OnCreateEvent<Item = F::Item>,
    {
        EventStreamBuilder {
            keep_alive: self.keep_alive,
            _marker: PhantomData,
        }
    }

    pub fn build<S>(self, stream: S) -> EventStream<F::Data>
    where
        S: TryStream<Ok = F::Item> + Send + 'static,
        S::Error: Into<BoxError>,
    {
        EventStream {
            result: inner_build(self, stream),
            _marker: PhantomData,
        }
    }
}

fn inner_build<C, S>(
    builder: EventStreamBuilder<C>,
    stream: S,
) -> Result<Response, EventStreamError>
where
    C: OnCreateEvent,
    S: TryStream<Ok = C::Item> + Send + 'static,
    S::Error: Into<BoxError>,
{
    pin_project! {
        struct SseStream<S> {
            #[pin]
            stream: S,
            #[pin]
            keep_alive: Option<KeepAliveStream>,
        }
    }

    impl<S> Stream for SseStream<S>
    where
        S: Stream<Item = Result<Bytes, BoxError>> + Send + 'static,
    {
        type Item = S::Item;

        fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
            let mut this = self.project();

            match this.stream.try_poll_next_unpin(cx) {
                Poll::Pending => {
                    if let Some(keep_alive) = this.keep_alive.as_pin_mut() {
                        keep_alive.poll_event(cx).map(|e| Some(Ok(e)))
                    } else {
                        Poll::Pending
                    }
                }
                ok @ Poll::Ready(Some(Ok(_))) => {
                    if let Some(keep_alive) = this.keep_alive.as_pin_mut() {
                        keep_alive.reset();
                    }

                    ok
                }
                other => other,
            }
        }
    }

    let stream = SseStream {
        stream: stream.map_err(Into::into).and_then(|item| async move {
            let data = C::data(&item);
            let data = serde_json::to_string(data).map_err(Box::new)?;

            let mut evt = Event::data(data);
            C::modify_event(&item, &mut evt);

            let bytes = evt.as_bytes().map_err(Box::new)?;

            Ok::<_, BoxError>(bytes)
        }),
        keep_alive: builder.keep_alive.map(KeepAliveStream::new).transpose()?,
    };

    let body = ResponseBody::from_stream(stream);

    let response = http::Response::builder()
        .header(CONTENT_TYPE, EventStream::<()>::MEDIA_TYPE)
        .header(CACHE_CONTROL, "no-cache")
        .header("X-Accel-Buffering", "no")
        .body(body)
        .unwrap();

    Ok(response)
}

pub trait OnCreateEvent {
    type Item: Send + 'static;
    type Data: Serialize;

    fn data(item: &Self::Item) -> &Self::Data;

    fn modify_event(item: &Self::Item, event: &mut Event);
}

#[derive(Debug)]
pub struct DefaultOnCreateEvent<T> {
    _marker: PhantomData<T>,
}

impl<T> OnCreateEvent for DefaultOnCreateEvent<T>
where
    T: Serialize + Send + 'static,
{
    type Data = T;
    type Item = T;

    fn data(item: &Self::Item) -> &Self::Data {
        item
    }

    fn modify_event(_: &Self::Item, _: &mut Event) {}
}