Skip to main content

rustapi_view/
view.rs

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