use serde::Serialize;
use std::error::Error;
use std::ffi::{CStr, CString, NulError};
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;
#[cfg(not(docsrs))]
mod goffi {
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
#![allow(unused)]
include!(concat!(env!("OUT_DIR"), "/api_bindings.rs"));
}
#[cfg(docsrs)]
mod goffi {
use std::os::raw::c_char;
#[repr(C)]
pub struct RenderResult {
pub output: *mut c_char, pub error: *mut c_char, }
extern "C" {
pub fn RenderTemplate(
template_content: *mut c_char, json_data: *mut c_char, escape_html: bool,
use_missing_key_zero: bool,
) -> RenderResult;
pub fn FreeResultString(s: *mut c_char); }
}
#[derive(Debug)]
pub enum RenderError {
InvalidCString(NulError),
JsonSerialization(serde_json::Error),
GoExecution(String),
}
impl Display for RenderError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
RenderError::InvalidCString(e) => {
write!(f, "Failed to convert string to C-compatible string: {}", e)
}
RenderError::JsonSerialization(e) => {
write!(f, "Failed to serialize data to JSON: {}", e)
}
RenderError::GoExecution(e) => write!(f, "Go template execution error: {}", e),
}
}
}
impl Error for RenderError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
RenderError::InvalidCString(e) => Some(e),
RenderError::JsonSerialization(e) => Some(e),
RenderError::GoExecution(_) => None,
}
}
}
impl From<NulError> for RenderError {
fn from(err: NulError) -> Self {
RenderError::InvalidCString(err)
}
}
impl From<serde_json::Error> for RenderError {
fn from(err: serde_json::Error) -> Self {
RenderError::JsonSerialization(err)
}
}
struct OwnedGoResult(goffi::RenderResult);
impl Drop for OwnedGoResult {
fn drop(&mut self) {
unsafe {
goffi::FreeResultString(self.0.output);
goffi::FreeResultString(self.0.error);
}
}
}
pub struct TemplateRenderer<'a, T: Serialize> {
template_content: &'a str,
data: &'a T,
escape_html: bool,
use_missing_key_zero: bool,
_marker: PhantomData<&'a T>,
}
impl<'a, T: Serialize> TemplateRenderer<'a, T> {
pub fn new(template_content: &'a str, data: &'a T) -> Self {
Self {
template_content,
data,
escape_html: false,
use_missing_key_zero: false,
_marker: PhantomData,
}
}
pub fn escape_html(mut self, escape: bool) -> Self {
self.escape_html = escape;
self
}
pub fn use_missing_key_zero(mut self, use_zero: bool) -> Self {
self.use_missing_key_zero = use_zero;
self
}
pub fn render(self) -> Result<String, RenderError> {
let c_template = CString::new(self.template_content)?;
let json_data_string = serde_json::to_string(self.data)?;
let c_json_data = CString::new(json_data_string)?;
let result = unsafe {
OwnedGoResult(goffi::RenderTemplate(
c_template.into_raw(), c_json_data.into_raw(), self.escape_html,
self.use_missing_key_zero,
))
};
let output = unsafe {
let output_str = CStr::from_ptr(result.0.output)
.to_string_lossy()
.into_owned();
let _ = CString::from_raw(result.0.output);
output_str
};
let error = unsafe {
let error_str = CStr::from_ptr(result.0.error)
.to_string_lossy()
.into_owned();
let _ = CString::from_raw(result.0.error);
error_str
};
if !error.is_empty() {
Err(RenderError::GoExecution(error))
} else {
Ok(output)
}
}
}
impl<'a, T: Serialize> TemplateRenderer<'a, T> {
pub fn render_quick(template: &'a str, data: &'a T) -> Result<String, RenderError> {
Self::new(template, data).render()
}
pub fn render_html(template: &'a str, data: &'a T) -> Result<String, RenderError> {
Self::new(template, data).escape_html(true).render()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
#[derive(Serialize, Debug)]
struct SimpleData {
name: String,
age: u32,
active: bool,
}
#[derive(Serialize)]
struct NestedData {
user: SimpleData,
website: String,
}
#[derive(Serialize)]
struct EmptyData {}
#[test]
fn test_basic_template_rendering() {
let data = SimpleData {
name: "Alice".to_string(),
age: 30,
active: true,
};
let template = "Hello, {{.name}}! You are {{.age}} years old.";
let result = TemplateRenderer::render_quick(template, &data).unwrap();
assert_eq!(result, "Hello, Alice! You are 30 years old.");
}
#[test]
fn test_html_escaping() {
let data = SimpleData {
name: "<script>alert('xss')</script>".to_string(),
age: 25,
active: false,
};
let template = "Welcome, {{.name}}";
let result_no_escape = TemplateRenderer::new(template, &data)
.escape_html(false)
.render()
.unwrap();
assert_eq!(result_no_escape, "Welcome, <script>alert('xss')</script>");
let result_escape = TemplateRenderer::new(template, &data)
.escape_html(true)
.render()
.unwrap();
assert!(result_escape.contains("<script>"));
assert!(result_escape.contains("</script>"));
}
#[test]
fn test_nested_data() {
let user_data = SimpleData {
name: "Bob".to_string(),
age: 35,
active: true,
};
let data = NestedData {
user: user_data,
website: "example.com".to_string(),
};
let template = "User: {{.user.name}}, Website: {{.website}}";
let result = TemplateRenderer::render_quick(template, &data).unwrap();
assert_eq!(result, "User: Bob, Website: example.com");
}
#[test]
fn test_boolean_conditional() {
let data = SimpleData {
name: "Charlie".to_string(),
age: 40,
active: true,
};
let template = "{{.name}} is {{if .active}}active{{else}}inactive{{end}}";
let result = TemplateRenderer::render_quick(template, &data).unwrap();
assert_eq!(result, "Charlie is active");
}
#[test]
fn test_missing_key_handling() {
let data = SimpleData {
name: "David".to_string(),
age: 28,
active: false,
};
let template = "Name: {{.name}}, Missing: {{.nonexistent}}";
let result_zero = TemplateRenderer::new(template, &data)
.use_missing_key_zero(true)
.render()
.unwrap();
assert_eq!(result_zero, "Name: David, Missing: ");
}
#[test]
fn test_empty_data() {
let data = EmptyData {};
let template = "Static content";
let result = TemplateRenderer::render_quick(template, &data).unwrap();
assert_eq!(result, "Static content");
}
#[test]
fn test_complex_template() {
let data = SimpleData {
name: "Eve".to_string(),
age: 32,
active: true,
};
let template = r#"
User Profile:
-----------
Name: {{.name}}
Age: {{.age}}
Status: {{if .active}}Active{{else}}Inactive{{end}}
{{range $i, $e := .}}
Field {{$i}}: {{$e}}
{{end}}
"#;
let result = TemplateRenderer::render_quick(template, &data).unwrap();
assert!(result.contains("Name: Eve"));
assert!(result.contains("Age: 32"));
assert!(result.contains("Status: Active"));
}
#[test]
fn test_error_handling() {
let data = SimpleData {
name: "Frank".to_string(),
age: 45,
active: false,
};
let invalid_template = "Hello, {{.name}"; let result = TemplateRenderer::render_quick(invalid_template, &data);
assert!(result.is_err());
if let Err(RenderError::GoExecution(err)) = result {
assert!(err.contains("template") || err.contains("parse") || err.contains("execute"));
} else {
panic!("Expected GoExecution error");
}
}
#[test]
fn test_convenience_methods() {
let data = SimpleData {
name: "Grace".to_string(),
age: 29,
active: true,
};
let template = "Hello, {{.name}}";
let quick_result = TemplateRenderer::render_quick(template, &data).unwrap();
assert_eq!(quick_result, "Hello, Grace");
let html_result = TemplateRenderer::render_html(template, &data).unwrap();
assert_eq!(html_result, "Hello, Grace"); }
#[test]
fn test_special_characters() {
let data = SimpleData {
name: "O'Reilly".to_string(),
age: 50,
active: false,
};
let template = "Name: {{.name}}, Quote: \"test\"";
let result = TemplateRenderer::render_quick(template, &data).unwrap();
assert!(result.contains("O'Reilly"));
assert!(result.contains("Quote: \"test\""));
}
#[test]
fn test_edge_cases() {
let empty_data = SimpleData {
name: "".to_string(),
age: 0,
active: false,
};
let template = "Empty name: '{{.name}}'";
let result = TemplateRenderer::render_quick(template, &empty_data).unwrap();
assert_eq!(result, "Empty name: ''");
let long_name = "A".repeat(1000);
let long_data = SimpleData {
name: long_name.clone(),
age: 99,
active: true,
};
let template = "Long: {{.name}}";
let result = TemplateRenderer::render_quick(template, &long_data).unwrap();
assert_eq!(result, format!("Long: {}", long_name));
}
#[test]
fn test_serialization_error() {
struct UnserializableData;
impl Serialize for UnserializableData {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::Error;
Err(S::Error::custom("Intentionally failed serialization"))
}
}
let data = UnserializableData;
let template = "Test {{.field}}";
let result = TemplateRenderer::render_quick(template, &data);
assert!(result.is_err());
if let Err(RenderError::JsonSerialization(_)) = result {
} else {
panic!("Expected JsonSerialization error");
}
}
}