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}