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(
49        templates: &Templates,
50        template: &str,
51        context: T,
52    ) -> Self {
53        let content = templates.render_with(template, &context).await;
54        Self {
55            content,
56            status: StatusCode::OK,
57            _phantom: PhantomData,
58        }
59    }
60
61    /// Create a view with a specific status code
62    pub async fn render_with_status(
63        templates: &Templates,
64        template: &str,
65        context: T,
66        status: StatusCode,
67    ) -> Self {
68        let content = templates.render_with(template, &context).await;
69        Self {
70            content,
71            status,
72            _phantom: PhantomData,
73        }
74    }
75
76    /// Create a view from pre-rendered HTML
77    pub fn from_html(html: impl Into<String>) -> Self {
78        Self {
79            content: Ok(html.into()),
80            status: StatusCode::OK,
81            _phantom: PhantomData,
82        }
83    }
84
85    /// Create an error view
86    pub fn error(err: ViewError) -> Self {
87        Self {
88            content: Err(err),
89            status: StatusCode::INTERNAL_SERVER_ERROR,
90            _phantom: PhantomData,
91        }
92    }
93
94    /// Set the status code
95    pub fn status(mut self, status: StatusCode) -> Self {
96        self.status = status;
97        self
98    }
99}
100
101impl View<()> {
102    /// Create a view by rendering a template with a tera Context
103    pub async fn render_context(
104        templates: &Templates,
105        template: &str,
106        context: &tera::Context,
107    ) -> Self {
108        let content = templates.render(template, context).await;
109        Self {
110            content,
111            status: StatusCode::OK,
112            _phantom: PhantomData,
113        }
114    }
115}
116
117impl<T> IntoResponse for View<T> {
118    fn into_response(self) -> Response<Full<Bytes>> {
119        match self.content {
120            Ok(html) => Response::builder()
121                .status(self.status)
122                .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
123                .body(Full::new(Bytes::from(html)))
124                .unwrap(),
125            Err(err) => {
126                tracing::error!("Template rendering failed: {}", err);
127                Response::builder()
128                    .status(StatusCode::INTERNAL_SERVER_ERROR)
129                    .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
130                    .body(Full::new(Bytes::from(format!(
131                        "<!DOCTYPE html><html><head><title>Error</title></head>\
132                        <body><h1>500 Internal Server Error</h1>\
133                        <p>Template rendering failed</p></body></html>"
134                    ))))
135                    .unwrap()
136            }
137        }
138    }
139}
140
141impl<T> ResponseModifier for View<T> {
142    fn update_response(op: &mut Operation) {
143        op.responses.insert(
144            "200".to_string(),
145            ResponseSpec {
146                description: "HTML Content".to_string(),
147                content: {
148                    let mut map = HashMap::new();
149                    map.insert(
150                        "text/html".to_string(),
151                        MediaType {
152                            schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
153                        },
154                    );
155                    Some(map)
156                },
157            },
158        );
159    }
160}
161
162/// Helper for creating views with different status codes
163impl<T: Serialize> View<T> {
164    /// Create a 404 Not Found view
165    pub async fn not_found(templates: &Templates, template: &str, context: T) -> Self {
166        Self::render_with_status(templates, template, context, StatusCode::NOT_FOUND).await
167    }
168
169    /// Create a 403 Forbidden view
170    pub async fn forbidden(templates: &Templates, template: &str, context: T) -> Self {
171        Self::render_with_status(templates, template, context, StatusCode::FORBIDDEN).await
172    }
173
174    /// Create a 401 Unauthorized view
175    pub async fn unauthorized(templates: &Templates, template: &str, context: T) -> Self {
176        Self::render_with_status(templates, template, context, StatusCode::UNAUTHORIZED).await
177    }
178}