lib_humus/engine.rs
1
2use axum::{
3 body::Body,
4 http::StatusCode,
5 http::header,
6 response::IntoResponse,
7 response::Response,
8};
9#[cfg(feature="axum-view+cookie")]
10use axum::http::header::SET_COOKIE;
11use axum_extra::headers::HeaderValue;
12use log::error;
13use tera::Tera;
14use toml::Table;
15
16use std::marker::PhantomData;
17
18use crate::HumusFormat;
19use crate::HumusFormatFamily;
20use crate::HumusProtoEngine;
21use crate::HumusQuerySettings;
22use crate::HumusView;
23
24/* The engine itself */
25
26#[allow(clippy::doc_overindented_list_items)]
27/// 🌄 Tera based Templating Engine that writes out axum Response structs.
28///
29/// The engine uses logic and data from the configured [View] (V) type as well as
30/// data and configuration provided by the [QuerySettings] (S) type to produce the
31/// desired response with minimal code overhead inside your main logic.
32///
33/// <b>Note:</b> The private fields are [PhantomData] because the V, S and N
34/// generics are only used for the implementation.
35///
36/// [PhantomData]: https://doc.rust-lang.org/std/marker/struct.PhantomData.html
37///
38/// The Engine operates in one of two modes determined by the [Format] (F) which
39/// is [provided by the QuerySettings]:
40/// * [Template Mode]: The [Format] describes which template to use for rendering
41/// the [View] to some kind of text using Tera templates.
42/// * [API Mode]: The [View] [serializes itself], usually to a [Json Response].
43///
44/// # Template Mode
45///
46/// In template mode the following sequence of events happens:
47/// * status code, format and cookies are fetched.
48/// (The cookie functionality is enabled with the `axum-view+cookie` feature)
49/// * Template name and MimeType are fetched.
50/// * The Template context is populated with metadatam the [View] and `template_config`.
51/// * The [QuerySettings] are given the chance to populate the context
52/// with their own values using the `initalize_template_context()` hook.
53/// * The template gets rendered, resulting in further processing or an error response.
54/// * The response is constructed using the MimeType from earlier and
55/// the text from the template.
56/// * The [View] is given a chance to alter the Response
57/// using the `update_response()` hook.
58/// * If the status code of the Response is "200 OK" (the default)
59/// the status code and cookies fetched earlier are applied.
60///
61/// ## Template Symbols
62///
63/// The symbols defined for inside templates are:
64/// * `view` will contain the template name, that coincides with
65/// the basename of the template file.
66/// * `format` will contain the format name (i.e. `html`, `text`, `json`)
67/// * `mime_type` will contain the serialized mime_type
68/// (i.e. `text/plain; charset=utf-8` or `application/json`)
69/// * `http_status` will contain the numeric HTTP status code.
70/// * `data` will contain the serde serialized [View]
71/// * `extra` will be set to `template_config` which usually comes
72/// from an `extra.toml` file in the template directory or a
73/// configured custom location.
74/// * others that were added by the [QuerySettings] in `initalize_template_context()`
75///
76/// # API Mode
77///
78/// In API mode the following, simpler sequence of events happens:
79/// * status code, format and cookies are fetched.
80/// (The cookie functionality is enabled with the `axum-view+cookie` feature)
81/// * The [View] [serializes itself] using the `get_api_response()` callback.
82/// * If the status code of the Response is "200 OK" (the default)
83/// the status code and cookies fetched earlier are applied.
84///
85///
86/// [View]: ./trait.HumusView.html
87/// [QuerySettings]: ./trait.HumusQuerySettings.html
88/// [Format]: ./trait.HumusFormat.html
89/// [provided by the QuerySettings]: ./trait.HumusQuerySettings.html#tymethod.get_format
90/// [Template Mode]: ./enum.HumusFormatFamily.html#variant.Template
91/// [API Mode]: ./enum.HumusFormatFamily.html#variant.API
92/// [serializes itself]: ./trait.HumusView.html#method.get_api_response
93/// [Json Response]: https://docs.rs/axum/0.6.20/axum/struct.Json.html
94///
95#[derive(Debug, Clone)]
96pub struct HumusEngine<V, S, F>
97where V: HumusView<S, F>, S: HumusQuerySettings<F>, F: HumusFormat {
98
99 /// An instance of the tera templating engine.
100 pub tera: Tera,
101
102 /// If it was possible to read any extra configuration it will be stored here.
103 pub template_config: Option<Table>,
104
105 phantom_view: PhantomData<V>,
106 phantom_settings: PhantomData<S>,
107 phantom_format: PhantomData<F>,
108}
109
110impl<V, S, F> HumusEngine<V, S, F>
111where V: HumusView<S, F>, S: HumusQuerySettings<F>, F: HumusFormat {
112
113 /// Creates a new Templating Engine.
114 ///
115 /// An alternative would be converting from a [HumusProtoEngine].
116 ///
117 /// [HumusProtoEngine]: ./struct.HumusProtoEngine.html
118 pub fn new(tera: Tera, template_config: Option<Table>) -> Self {
119 Self {
120 tera: tera,
121 template_config: template_config,
122 phantom_view: PhantomData,
123 phantom_settings: PhantomData,
124 phantom_format: PhantomData,
125 }
126 }
127
128 /// Takes settings and a view, converting it to a serveable response.
129 ///
130 /// Example:
131 /// ```rust,ignore
132 /// async fn hello_world_handler(
133 /// State(arc_state): State<Arc<ServiceSharedState>>,
134 /// Extension(settings): Extension<QuerySettings>,
135 /// ) -> Response {
136 /// let state = Arc::clone(&arc_state);
137 ///
138 /// state.templating_engine.render_view(
139 /// &settings,
140 /// View::Message{
141 /// title: "Hey There!".to_string(),
142 /// message: "You are an awesome creature!".to_string()
143 /// },
144 /// )
145 /// }
146 ///
147 /// ```
148 pub fn render_view(
149 &self,
150 settings: &S,
151 view: V,
152 ) -> Response {
153 let status_code = view.get_status_code(settings);
154 let format = settings.get_format();
155 #[cfg(feature="axum-view+cookie")]
156 let cookie_string = view.get_cookie_header(settings);
157
158 let mut response = match format.get_family() {
159 HumusFormatFamily::Template => {
160 let template_name = view.get_template_name();
161 let mime_type = format.get_mime_type();
162
163 let mut context = tera::Context::new();
164 context.insert("view", &template_name);
165 //intented for shared macros
166 context.insert("format", &format.get_name());
167 context.insert("mimetype", &mime_type.to_string());
168 context.insert("http_status", &status_code.as_u16());
169 context.insert("data", &view);
170 context.insert("extra", &self.template_config);
171 settings.initalize_template_context(&mut context);
172
173 match self.tera.render(
174 &(template_name.clone()+&format.get_file_extension()),
175 &context
176 ) {
177 Ok(text) => {
178 let mut response = (
179 [(
180 header::CONTENT_TYPE,
181 HeaderValue::from_str(mime_type.as_ref())
182 .expect("MimeType should always be a valid header value.")
183 )],
184 Into::<Body>::into(text),
185 ).into_response();
186 view.update_response(&mut response, settings);
187 response
188 },
189 Err(e) => {
190 error!("There was an error while rendering template {}: {e:?}", template_name);
191 (
192 StatusCode::INTERNAL_SERVER_ERROR,
193 format!("Template error in {}, contact owner or see logs.\n", template_name)
194 ).into_response()
195 }
196 }
197 }
198 HumusFormatFamily::API => {
199 view.get_api_response(settings)
200 }
201 };
202
203 // Everything went well and nobody did the following work for us.
204 if response.status() == StatusCode::OK {
205
206 // Set status code
207 if status_code != StatusCode::OK {
208 *response.status_mut() = status_code;
209 }
210
211 // Set cookie header
212 #[cfg(feature="axum-view+cookie")]
213 if let Some(cookie_string) = cookie_string {
214 if let Ok(header_value) = HeaderValue::from_str(&cookie_string) {
215 response.headers_mut().append(
216 SET_COOKIE,
217 header_value,
218 );
219 }
220 }
221
222 }
223
224 // return response
225 response
226 }
227}
228
229impl<V, S, F> From<HumusProtoEngine> for HumusEngine<V, S, F>
230where V: HumusView<S, F>, S: HumusQuerySettings<F>, F: HumusFormat {
231 fn from(e: HumusProtoEngine) -> Self {
232 Self::new(e.tera, e.template_config)
233 }
234}
235
236impl<V, S, F> From<HumusEngine<V, S, F>> for HumusProtoEngine
237where V: HumusView<S, F>, S: HumusQuerySettings<F>, F: HumusFormat {
238 fn from(e: HumusEngine<V, S, F>) -> Self {
239 Self {
240 tera: e.tera,
241 template_config: e.template_config,
242 }
243 }
244}