1use serde::Serialize;
2use std::error::Error;
3use std::ffi::{CStr, CString, NulError};
4use std::fmt::{self, Display, Formatter};
5use std::marker::PhantomData;
6#[cfg(not(docsrs))]
9mod goffi {
10 #![allow(non_snake_case)]
11 #![allow(non_camel_case_types)]
12 #![allow(non_upper_case_globals)]
13 #![allow(unused)]
14 include!(concat!(env!("OUT_DIR"), "/api_bindings.rs"));
16}
17
18#[cfg(docsrs)]
19mod goffi {
20 use std::os::raw::c_char;
21
22 #[repr(C)]
23 pub struct RenderResult {
24 pub output: *mut c_char, pub error: *mut c_char, }
27
28 extern "C" {
29 pub fn RenderTemplate(
30 template_content: *mut c_char, json_data: *mut c_char, escape_html: bool,
33 use_missing_key_zero: bool,
34 ) -> RenderResult;
35 pub fn FreeResultString(s: *mut c_char); }
37}
38
39#[derive(Debug)]
40pub enum RenderError {
41 InvalidCString(NulError),
42 JsonSerialization(serde_json::Error),
43 GoExecution(String),
44}
45
46impl Display for RenderError {
47 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
48 match self {
49 RenderError::InvalidCString(e) => {
50 write!(f, "Failed to convert string to C-compatible string: {}", e)
51 }
52 RenderError::JsonSerialization(e) => {
53 write!(f, "Failed to serialize data to JSON: {}", e)
54 }
55 RenderError::GoExecution(e) => write!(f, "Go template execution error: {}", e),
56 }
57 }
58}
59
60impl Error for RenderError {
61 fn source(&self) -> Option<&(dyn Error + 'static)> {
62 match self {
63 RenderError::InvalidCString(e) => Some(e),
64 RenderError::JsonSerialization(e) => Some(e),
65 RenderError::GoExecution(_) => None,
66 }
67 }
68}
69
70impl From<NulError> for RenderError {
71 fn from(err: NulError) -> Self {
72 RenderError::InvalidCString(err)
73 }
74}
75
76impl From<serde_json::Error> for RenderError {
77 fn from(err: serde_json::Error) -> Self {
78 RenderError::JsonSerialization(err)
79 }
80}
81
82struct OwnedGoResult(goffi::RenderResult);
83
84impl Drop for OwnedGoResult {
85 fn drop(&mut self) {
86 unsafe {
87 goffi::FreeResultString(self.0.output);
88 goffi::FreeResultString(self.0.error);
89 }
90 }
91}
92
93pub struct TemplateRenderer<'a, T: Serialize> {
95 template_content: &'a str,
96 data: &'a T,
97 escape_html: bool,
98 use_missing_key_zero: bool,
99 _marker: PhantomData<&'a T>,
100}
101
102impl<'a, T: Serialize> TemplateRenderer<'a, T> {
103 pub fn new(template_content: &'a str, data: &'a T) -> Self {
109 Self {
110 template_content,
111 data,
112 escape_html: false,
113 use_missing_key_zero: false,
114 _marker: PhantomData,
115 }
116 }
117
118 pub fn escape_html(mut self, escape: bool) -> Self {
122 self.escape_html = escape;
123 self
124 }
125
126 pub fn use_missing_key_zero(mut self, use_zero: bool) -> Self {
130 self.use_missing_key_zero = use_zero;
131 self
132 }
133
134 pub fn render(self) -> Result<String, RenderError> {
139 let c_template = CString::new(self.template_content)?;
141 let json_data_string = serde_json::to_string(self.data)?;
142 let c_json_data = CString::new(json_data_string)?;
143
144 let result = unsafe {
146 OwnedGoResult(goffi::RenderTemplate(
147 c_template.into_raw(), c_json_data.into_raw(), self.escape_html,
150 self.use_missing_key_zero,
151 ))
152 };
153
154 let output = unsafe {
156 let output_str = CStr::from_ptr(result.0.output)
157 .to_string_lossy()
158 .into_owned();
159 let _ = CString::from_raw(result.0.output);
161 output_str
162 };
163
164 let error = unsafe {
165 let error_str = CStr::from_ptr(result.0.error)
166 .to_string_lossy()
167 .into_owned();
168 let _ = CString::from_raw(result.0.error);
170 error_str
171 };
172
173 if !error.is_empty() {
174 Err(RenderError::GoExecution(error))
175 } else {
176 Ok(output)
177 }
178 }
179}
180
181impl<'a, T: Serialize> TemplateRenderer<'a, T> {
183 pub fn render_quick(template: &'a str, data: &'a T) -> Result<String, RenderError> {
185 Self::new(template, data).render()
186 }
187
188 pub fn render_html(template: &'a str, data: &'a T) -> Result<String, RenderError> {
190 Self::new(template, data).escape_html(true).render()
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use serde::Serialize;
198
199 #[derive(Serialize, Debug)]
200 struct SimpleData {
201 name: String,
202 age: u32,
203 active: bool,
204 }
205
206 #[derive(Serialize)]
207 struct NestedData {
208 user: SimpleData,
209 website: String,
210 }
211
212 #[derive(Serialize)]
213 struct EmptyData {}
214
215 #[test]
217 fn test_basic_template_rendering() {
218 let data = SimpleData {
219 name: "Alice".to_string(),
220 age: 30,
221 active: true,
222 };
223
224 let template = "Hello, {{.name}}! You are {{.age}} years old.";
225 let result = TemplateRenderer::render_quick(template, &data).unwrap();
226
227 assert_eq!(result, "Hello, Alice! You are 30 years old.");
228 }
229
230 #[test]
232 fn test_html_escaping() {
233 let data = SimpleData {
234 name: "<script>alert('xss')</script>".to_string(),
235 age: 25,
236 active: false,
237 };
238
239 let template = "Welcome, {{.name}}";
240
241 let result_no_escape = TemplateRenderer::new(template, &data)
243 .escape_html(false)
244 .render()
245 .unwrap();
246 assert_eq!(result_no_escape, "Welcome, <script>alert('xss')</script>");
247
248 let result_escape = TemplateRenderer::new(template, &data)
250 .escape_html(true)
251 .render()
252 .unwrap();
253 assert!(result_escape.contains("<script>"));
254 assert!(result_escape.contains("</script>"));
255 }
256
257 #[test]
259 fn test_nested_data() {
260 let user_data = SimpleData {
261 name: "Bob".to_string(),
262 age: 35,
263 active: true,
264 };
265
266 let data = NestedData {
267 user: user_data,
268 website: "example.com".to_string(),
269 };
270
271 let template = "User: {{.user.name}}, Website: {{.website}}";
272 let result = TemplateRenderer::render_quick(template, &data).unwrap();
273
274 assert_eq!(result, "User: Bob, Website: example.com");
275 }
276
277 #[test]
279 fn test_boolean_conditional() {
280 let data = SimpleData {
281 name: "Charlie".to_string(),
282 age: 40,
283 active: true,
284 };
285
286 let template = "{{.name}} is {{if .active}}active{{else}}inactive{{end}}";
287 let result = TemplateRenderer::render_quick(template, &data).unwrap();
288
289 assert_eq!(result, "Charlie is active");
290 }
291
292 #[test]
294 fn test_missing_key_handling() {
295 let data = SimpleData {
296 name: "David".to_string(),
297 age: 28,
298 active: false,
299 };
300
301 let template = "Name: {{.name}}, Missing: {{.nonexistent}}";
303
304 let result_zero = TemplateRenderer::new(template, &data)
306 .use_missing_key_zero(true)
307 .render()
308 .unwrap();
309
310 assert_eq!(result_zero, "Name: David, Missing: ");
312
313 }
316
317 #[test]
319 fn test_empty_data() {
320 let data = EmptyData {};
321 let template = "Static content";
322 let result = TemplateRenderer::render_quick(template, &data).unwrap();
323
324 assert_eq!(result, "Static content");
325 }
326
327 #[test]
329 fn test_complex_template() {
330 let data = SimpleData {
331 name: "Eve".to_string(),
332 age: 32,
333 active: true,
334 };
335
336 let template = r#"
337User Profile:
338-----------
339Name: {{.name}}
340Age: {{.age}}
341Status: {{if .active}}Active{{else}}Inactive{{end}}
342
343{{range $i, $e := .}}
344Field {{$i}}: {{$e}}
345{{end}}
346"#;
347
348 let result = TemplateRenderer::render_quick(template, &data).unwrap();
349 assert!(result.contains("Name: Eve"));
350 assert!(result.contains("Age: 32"));
351 assert!(result.contains("Status: Active"));
352 }
353
354 #[test]
356 fn test_error_handling() {
357 let data = SimpleData {
358 name: "Frank".to_string(),
359 age: 45,
360 active: false,
361 };
362
363 let invalid_template = "Hello, {{.name}"; let result = TemplateRenderer::render_quick(invalid_template, &data);
366
367 assert!(result.is_err());
368 if let Err(RenderError::GoExecution(err)) = result {
369 assert!(err.contains("template") || err.contains("parse") || err.contains("execute"));
370 } else {
371 panic!("Expected GoExecution error");
372 }
373 }
374
375 #[test]
377 fn test_convenience_methods() {
378 let data = SimpleData {
379 name: "Grace".to_string(),
380 age: 29,
381 active: true,
382 };
383
384 let template = "Hello, {{.name}}";
385
386 let quick_result = TemplateRenderer::render_quick(template, &data).unwrap();
388 assert_eq!(quick_result, "Hello, Grace");
389
390 let html_result = TemplateRenderer::render_html(template, &data).unwrap();
392 assert_eq!(html_result, "Hello, Grace"); }
394
395 #[test]
397 fn test_special_characters() {
398 let data = SimpleData {
399 name: "O'Reilly".to_string(),
400 age: 50,
401 active: false,
402 };
403
404 let template = "Name: {{.name}}, Quote: \"test\"";
405 let result = TemplateRenderer::render_quick(template, &data).unwrap();
406
407 assert!(result.contains("O'Reilly"));
408 assert!(result.contains("Quote: \"test\""));
409 }
410
411 #[test]
413 fn test_edge_cases() {
414 let empty_data = SimpleData {
416 name: "".to_string(),
417 age: 0,
418 active: false,
419 };
420
421 let template = "Empty name: '{{.name}}'";
422 let result = TemplateRenderer::render_quick(template, &empty_data).unwrap();
423 assert_eq!(result, "Empty name: ''");
424
425 let long_name = "A".repeat(1000);
427 let long_data = SimpleData {
428 name: long_name.clone(),
429 age: 99,
430 active: true,
431 };
432
433 let template = "Long: {{.name}}";
434 let result = TemplateRenderer::render_quick(template, &long_data).unwrap();
435 assert_eq!(result, format!("Long: {}", long_name));
436 }
437
438 #[test]
440 fn test_serialization_error() {
441 struct UnserializableData;
443
444 impl Serialize for UnserializableData {
445 fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
446 where
447 S: serde::Serializer,
448 {
449 use serde::ser::Error;
450 Err(S::Error::custom("Intentionally failed serialization"))
451 }
452 }
453
454 let data = UnserializableData;
455 let template = "Test {{.field}}";
456 let result = TemplateRenderer::render_quick(template, &data);
457
458 assert!(result.is_err());
459 if let Err(RenderError::JsonSerialization(_)) = result {
460 } else {
462 panic!("Expected JsonSerialization error");
463 }
464 }
465}