rustapi_view/
view.rs

1//! View response type
2
3use crate::{Templates, ViewError};
4use bytes::Bytes;
5use http::{header, Response, StatusCode};
6use http_body_util::Full;
7use rustapi_core::IntoResponse;
8use rustapi_openapi::{MediaType, Operation, ResponseModifier, ResponseSpec, SchemaRef};
9use serde::Serialize;
10use std::collections::HashMap;
11use std::marker::PhantomData;
12
13/// A response that renders a template with a context
14///
15/// This is the primary way to render HTML templates in RustAPI handlers.
16///
17/// # Example
18///
19/// ```rust,ignore
20/// use rustapi_view::{View, Templates};
21/// use serde::Serialize;
22///
23/// #[derive(Serialize)]
24/// struct HomeContext {
25///     title: String,
26/// }
27///
28/// async fn home(templates: State<Templates>) -> View<HomeContext> {
29///     View::render(&templates, "home.html", HomeContext {
30///         title: "Home".to_string(),
31///     })
32/// }
33/// ```
34pub struct View<T> {
35    /// The rendered HTML content
36    content: Result<String, ViewError>,
37    /// Status code (default 200)
38    status: StatusCode,
39    /// Phantom data for the context type
40    _phantom: PhantomData<T>,
41}
42
43impl<T: Serialize> View<T> {
44    /// Create a view by rendering a template with a serializable context
45    ///
46    /// This is an async operation that renders the template immediately.
47    /// For deferred rendering, use `View::deferred`.
48    pub async fn render(templates: &Templates, template: &str, context: T) -> Self {
49        let content = templates.render_with(template, &context).await;
50        Self {
51            content,
52            status: StatusCode::OK,
53            _phantom: PhantomData,
54        }
55    }
56
57    /// Create a view with a specific status code
58    pub async fn render_with_status(
59        templates: &Templates,
60        template: &str,
61        context: T,
62        status: StatusCode,
63    ) -> Self {
64        let content = templates.render_with(template, &context).await;
65        Self {
66            content,
67            status,
68            _phantom: PhantomData,
69        }
70    }
71
72    /// Create a view from pre-rendered HTML
73    pub fn from_html(html: impl Into<String>) -> Self {
74        Self {
75            content: Ok(html.into()),
76            status: StatusCode::OK,
77            _phantom: PhantomData,
78        }
79    }
80
81    /// Create an error view
82    pub fn error(err: ViewError) -> Self {
83        Self {
84            content: Err(err),
85            status: StatusCode::INTERNAL_SERVER_ERROR,
86            _phantom: PhantomData,
87        }
88    }
89
90    /// Set the status code
91    pub fn status(mut self, status: StatusCode) -> Self {
92        self.status = status;
93        self
94    }
95}
96
97impl View<()> {
98    /// Create a view by rendering a template with a tera Context
99    pub async fn render_context(
100        templates: &Templates,
101        template: &str,
102        context: &tera::Context,
103    ) -> Self {
104        let content = templates.render(template, context).await;
105        Self {
106            content,
107            status: StatusCode::OK,
108            _phantom: PhantomData,
109        }
110    }
111}
112
113impl<T> IntoResponse for View<T> {
114    fn into_response(self) -> Response<Full<Bytes>> {
115        match self.content {
116            Ok(html) => Response::builder()
117                .status(self.status)
118                .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
119                .body(Full::new(Bytes::from(html)))
120                .unwrap(),
121            Err(err) => {
122                tracing::error!("Template rendering failed: {}", err);
123                Response::builder()
124                    .status(StatusCode::INTERNAL_SERVER_ERROR)
125                    .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
126                    .body(Full::new(Bytes::from(
127                        "<!DOCTYPE html><html><head><title>Error</title></head>\
128                        <body><h1>500 Internal Server Error</h1>\
129                        <p>Template rendering failed</p></body></html>",
130                    )))
131                    .unwrap()
132            }
133        }
134    }
135}
136
137impl<T> ResponseModifier for View<T> {
138    fn update_response(op: &mut Operation) {
139        op.responses.insert(
140            "200".to_string(),
141            ResponseSpec {
142                description: "HTML Content".to_string(),
143                content: {
144                    let mut map = HashMap::new();
145                    map.insert(
146                        "text/html".to_string(),
147                        MediaType {
148                            schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
149                        },
150                    );
151                    Some(map)
152                },
153            },
154        );
155    }
156}
157
158/// Helper for creating views with different status codes
159impl<T: Serialize> View<T> {
160    /// Create a 404 Not Found view
161    pub async fn not_found(templates: &Templates, template: &str, context: T) -> Self {
162        Self::render_with_status(templates, template, context, StatusCode::NOT_FOUND).await
163    }
164
165    /// Create a 403 Forbidden view
166    pub async fn forbidden(templates: &Templates, template: &str, context: T) -> Self {
167        Self::render_with_status(templates, template, context, StatusCode::FORBIDDEN).await
168    }
169
170    /// Create a 401 Unauthorized view
171    pub async fn unauthorized(templates: &Templates, template: &str, context: T) -> Self {
172        Self::render_with_status(templates, template, context, StatusCode::UNAUTHORIZED).await
173    }
174}