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}