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