1use 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
13pub struct View<T> {
35 content: Result<String, ViewError>,
37 status: StatusCode,
39 _phantom: PhantomData<T>,
41}
42
43impl<T: Serialize> View<T> {
44 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 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 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 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 pub fn status(mut self, status: StatusCode) -> Self {
96 self.status = status;
97 self
98 }
99}
100
101impl View<()> {
102 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
162impl<T: Serialize> View<T> {
164 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 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 pub async fn unauthorized(templates: &Templates, template: &str, context: T) -> Self {
176 Self::render_with_status(templates, template, context, StatusCode::UNAUTHORIZED).await
177 }
178}