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(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 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 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 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 pub fn status(mut self, status: StatusCode) -> Self {
92 self.status = status;
93 self
94 }
95}
96
97impl View<()> {
98 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
158impl<T: Serialize> View<T> {
160 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 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 pub async fn unauthorized(templates: &Templates, template: &str, context: T) -> Self {
172 Self::render_with_status(templates, template, context, StatusCode::UNAUTHORIZED).await
173 }
174}