1use 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
11pub struct View<T> {
33 content: Result<String, ViewError>,
35 status: StatusCode,
37 _phantom: PhantomData<T>,
39}
40
41impl<T: Serialize> View<T> {
42 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 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 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 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 pub fn status(mut self, status: StatusCode) -> Self {
90 self.status = status;
91 self
92 }
93}
94
95impl View<()> {
96 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
160impl<T: Serialize> View<T> {
162 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 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 pub async fn unauthorized(templates: &Templates, template: &str, context: T) -> Self {
174 Self::render_with_status(templates, template, context, StatusCode::UNAUTHORIZED).await
175 }
176}