use std::sync::Arc;
#[cfg(feature = "http")]
use http::{HeaderMap, StatusCode};
use topcoat_core::context::Cx;
use crate::{
Formatter,
buffer::{InstructionPtr, Renderer, ViewBuffer, ViewBufferId, ViewBufferScope},
};
#[derive(Debug, Default, Clone)]
pub struct ViewHandle {
repr: ViewRepr,
}
#[derive(Debug, Clone)]
pub(super) enum ViewRepr {
Static(&'static str),
Scoped {
buffer: ViewBufferId,
entry: InstructionPtr,
size_hint: usize,
},
Owned {
buffer: Arc<ViewBuffer>,
entry: InstructionPtr,
size_hint: usize,
},
}
impl Default for ViewRepr {
#[inline]
fn default() -> Self {
Self::Static("")
}
}
impl ViewHandle {
#[inline]
pub(super) fn from_scope(
buffer: ViewBufferId,
entry: InstructionPtr,
size_hint: usize,
) -> Self {
Self {
repr: ViewRepr::Scoped {
buffer,
entry,
size_hint,
},
}
}
#[inline]
pub(super) fn repr(self) -> ViewRepr {
self.repr
}
#[inline]
#[must_use]
pub fn size_hint(&self) -> usize {
match &self.repr {
ViewRepr::Static(body) => body.len(),
ViewRepr::Scoped { size_hint, .. } | ViewRepr::Owned { size_hint, .. } => *size_hint,
}
}
#[must_use]
pub(crate) fn seal(self, buffer: ViewBuffer) -> Self {
match self.repr {
ViewRepr::Static(_) | ViewRepr::Owned { .. } => self,
ViewRepr::Scoped {
buffer: id,
entry,
size_hint,
} => {
assert!(
id == buffer.id(),
"tried to seal a view into a buffer it was not built in",
);
Self {
repr: ViewRepr::Owned {
buffer: Arc::new(buffer),
entry,
size_hint,
},
}
}
}
}
#[inline]
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
matches!(self.repr, ViewRepr::Static(""))
}
#[inline]
#[must_use]
pub const fn unescaped_unchecked(body: &'static str) -> Self {
Self {
repr: ViewRepr::Static(body),
}
}
#[cfg_attr(
feature = "http",
doc = "",
doc = "Status codes and headers declared in the view are discarded;",
doc = "[`render_response`](Self::render_response) collects them."
)]
#[must_use]
#[track_caller]
pub fn render(self, cx: &Cx) -> String {
let mut html = String::with_capacity(self.size_hint());
self.render_into(cx, &mut Formatter::new(&mut html));
html
}
#[cfg(feature = "http")]
#[must_use]
#[track_caller]
pub fn render_response(self, cx: &Cx) -> RenderedResponse {
let mut html = String::with_capacity(self.size_hint());
let mut f = Formatter::new(&mut html);
self.render_into(cx, &mut f);
let (status_code, headers) = f.into_recorded();
RenderedResponse {
html,
status_code,
headers,
}
}
#[track_caller]
pub fn render_into(self, cx: &Cx, f: &mut Formatter<'_>) {
match self.repr {
ViewRepr::Static(body) => f.write_str(body),
ViewRepr::Scoped { buffer, entry, .. } => ViewBufferScope::with(|active| {
assert!(
active.id() == buffer,
"tried to render a nested view handle outside the build it was built in",
);
Renderer::new(active, entry).execute(cx, f);
}),
ViewRepr::Owned { buffer, entry, .. } => {
Renderer::new(&buffer, entry).execute(cx, f);
}
}
}
}
#[cfg(feature = "http")]
#[derive(Debug)]
#[non_exhaustive]
pub struct RenderedResponse {
pub html: String,
pub status_code: Option<StatusCode>,
pub headers: HeaderMap,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::PartsWriter;
fn nested(buffer: &mut ViewBuffer, f: impl FnOnce(&mut PartsWriter<'_>)) -> ViewHandle {
buffer.block(f)
}
fn owned(f: impl FnOnce(&mut PartsWriter<'_>)) -> ViewHandle {
ViewBuffer::build(f)
}
fn in_scope<R>(f: impl FnOnce() -> R) -> (R, ViewBuffer) {
let mut slot = Some(Box::new(ViewBuffer::new()));
let output = {
let _buffer = ViewBufferScope::new(&mut slot);
f()
};
(output, *slot.expect("the buffer was swapped back on exit"))
}
#[test]
fn static_views_render_without_a_buffer() {
assert_eq!(ViewHandle::empty().render(&Cx::default()), "");
let view = ViewHandle::unescaped_unchecked("<b>raw</b>");
assert_eq!(view.render(&Cx::default()), "<b>raw</b>");
}
#[test]
fn push_view_splices_nested_views() {
let mut buffer = ViewBuffer::new();
let inner = nested(&mut buffer, |parts| {
parts.push_str("a < b");
});
let outer = nested(&mut buffer, |parts| {
parts.push_str_unescaped("<p>");
parts.push_view_handle(inner);
parts.push_str_unescaped("</p>");
});
assert_eq!(outer.seal(buffer).render(&Cx::default()), "<p>a < b</p>");
}
#[test]
fn document_order_follows_splice_order_not_buffer_order() {
let mut buffer = ViewBuffer::new();
let second = nested(&mut buffer, |parts| {
parts.push_str("B");
});
let first = nested(&mut buffer, |parts| {
parts.push_str("A");
});
let outer = nested(&mut buffer, |parts| {
parts.push_view_handle(first);
parts.push_view_handle(second);
});
assert_eq!(outer.seal(buffer).render(&Cx::default()), "AB");
}
#[test]
fn sealed_views_own_their_buffer() {
let view = owned(|parts| {
parts.push_str("a < b");
});
assert!(matches!(view.repr, ViewRepr::Owned { .. }));
assert_eq!(view.render(&Cx::default()), "a < b");
}
#[test]
fn owned_views_splice_across_buffers() {
let inner = owned(|parts| {
parts.push_str("a < b");
});
let outer = owned(|parts| {
parts.push_str_unescaped("<p>");
parts.push_view_handle(inner);
parts.push_str_unescaped("</p>");
});
assert_eq!(outer.render(&Cx::default()), "<p>a < b</p>");
}
#[test]
fn owned_views_are_send_and_sync() {
fn assert_send_sync<T: Send + Sync>(_value: &T) {}
let view = owned(|parts| {
parts.push_str("x");
});
assert_send_sync(&view);
}
#[test]
fn static_views_are_spliced_verbatim() {
let outer = owned(|parts| {
parts.push_view_handle(ViewHandle::unescaped_unchecked("<hr>"));
parts.push_view_handle(ViewHandle::empty());
});
assert_eq!(outer.render(&Cx::default()), "<hr>");
}
#[test]
fn size_hint_accumulates_across_splices() {
let mut buffer = ViewBuffer::new();
let inner = nested(&mut buffer, |parts| {
parts.push_str_unescaped("12345678");
});
let outer = nested(&mut buffer, |parts| {
parts.push_view_handle(inner.clone());
parts.push_view_handle(inner);
parts.push_view_handle(ViewHandle::unescaped_unchecked("<hr>"));
});
let ViewRepr::Scoped { size_hint, .. } = outer.repr() else {
panic!("expected a nested view");
};
assert_eq!(size_hint, 8 + 8 + 4);
}
#[test]
fn a_nested_view_renders_inside_its_build() {
let (rendered, _buffer) = in_scope(|| {
let view = ViewBufferScope::with(|buffer| {
buffer.block(|parts| {
parts.push_str("a < b");
})
});
view.render(&Cx::default())
});
assert_eq!(rendered, "a < b");
}
#[test]
#[should_panic(expected = "outside of a `ViewBufferScope`")]
fn rendering_a_nested_view_outside_any_build_panics() {
let mut buffer = ViewBuffer::new();
let view = nested(&mut buffer, |_parts| {});
let _ = view.render(&Cx::default());
}
#[test]
#[should_panic(expected = "outside the build it was built in")]
fn rendering_a_nested_view_inside_a_different_build_panics() {
let mut buffer = ViewBuffer::new();
let view = nested(&mut buffer, |_parts| {});
in_scope(|| view.render(&Cx::default()));
}
#[test]
#[should_panic(expected = "outside the `view!` invocation it was built in")]
fn splicing_a_nested_view_from_a_different_buffer_panics() {
let mut built_in = ViewBuffer::new();
let view = nested(&mut built_in, |_parts| {});
let mut other = ViewBuffer::new();
nested(&mut other, |parts| {
parts.push_view_handle(view);
});
}
#[test]
#[should_panic(expected = "tried to seal a view into a buffer it was not built in")]
fn sealing_a_view_into_a_different_buffer_panics() {
let mut built_in = ViewBuffer::new();
let view = nested(&mut built_in, |_parts| {});
let _ = view.seal(ViewBuffer::new());
}
#[cfg(feature = "http")]
mod response {
use http::{
HeaderMap, HeaderName, HeaderValue, StatusCode,
header::{CACHE_CONTROL, SET_COOKIE},
};
use super::*;
use crate::NodeViewParts;
fn push_node(cx: &Cx, parts: &mut PartsWriter<'_>, value: impl NodeViewParts) {
value.into_view_parts(cx, parts);
}
#[test]
fn status_code_is_recorded_and_renders_nothing() {
let cx = &Cx::default();
let view = owned(|parts| {
push_node(cx, parts, "a");
push_node(cx, parts, StatusCode::NOT_FOUND);
push_node(cx, parts, "b");
});
let rendered = view.render_response(cx);
assert_eq!(rendered.html, "ab");
assert_eq!(rendered.status_code, Some(StatusCode::NOT_FOUND));
assert!(rendered.headers.is_empty());
}
#[test]
fn render_response_without_declarations_is_empty() {
let cx = &Cx::default();
let view = owned(|parts| {
push_node(cx, parts, "a");
});
let rendered = view.render_response(cx);
assert_eq!(rendered.html, "a");
assert_eq!(rendered.status_code, None);
assert!(rendered.headers.is_empty());
}
#[test]
fn render_discards_declarations() {
let cx = &Cx::default();
let view = owned(|parts| {
push_node(cx, parts, StatusCode::NOT_FOUND);
push_node(
cx,
parts,
(CACHE_CONTROL, HeaderValue::from_static("no-store")),
);
push_node(cx, parts, "a");
});
assert_eq!(view.render(cx), "a");
}
#[test]
fn first_status_code_wins() {
let cx = &Cx::default();
let view = owned(|parts| {
push_node(cx, parts, StatusCode::NOT_FOUND);
push_node(cx, parts, StatusCode::OK);
});
let rendered = view.render_response(cx);
assert_eq!(rendered.status_code, Some(StatusCode::NOT_FOUND));
}
#[test]
fn first_mention_of_a_header_name_wins() {
let cx = &Cx::default();
let view = owned(|parts| {
push_node(
cx,
parts,
(CACHE_CONTROL, HeaderValue::from_static("no-store")),
);
let mut later = HeaderMap::new();
later.insert(CACHE_CONTROL, HeaderValue::from_static("max-age=60"));
later.insert(
HeaderName::from_static("x-extra"),
HeaderValue::from_static("1"),
);
push_node(cx, parts, later);
});
let rendered = view.render_response(cx);
assert_eq!(rendered.headers[CACHE_CONTROL], "no-store");
assert_eq!(rendered.headers["x-extra"], "1");
}
#[test]
fn one_map_keeps_all_values_for_a_name() {
let cx = &Cx::default();
let mut first = HeaderMap::new();
first.append(SET_COOKIE, HeaderValue::from_static("a=1"));
first.append(SET_COOKIE, HeaderValue::from_static("b=2"));
let mut later = HeaderMap::new();
later.insert(SET_COOKIE, HeaderValue::from_static("c=3"));
let view = owned(|parts| {
push_node(cx, parts, first);
push_node(cx, parts, later);
});
let rendered = view.render_response(cx);
let cookies: Vec<_> = rendered.headers.get_all(SET_COOKIE).iter().collect();
assert_eq!(cookies, ["a=1", "b=2"]);
}
#[test]
fn placement_decides_precedence_across_nested_views() {
let cx = &Cx::default();
let inner = owned(|parts| {
push_node(cx, parts, StatusCode::NOT_FOUND);
push_node(cx, parts, "inner");
});
let outer = owned(|parts| {
push_node(cx, parts, StatusCode::FORBIDDEN);
parts.push_view_handle(inner.clone());
});
let rendered = outer.render_response(cx);
assert_eq!(rendered.status_code, Some(StatusCode::FORBIDDEN));
let outer = owned(|parts| {
parts.push_view_handle(inner);
push_node(cx, parts, StatusCode::FORBIDDEN);
});
let rendered = outer.render_response(cx);
assert_eq!(rendered.status_code, Some(StatusCode::NOT_FOUND));
}
}
}