rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use std::fmt;
use std::ops::{Add, AddAssign};

/// HTML-safe string wrapper. Content is assumed pre-escaped.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SafeString(String);

impl SafeString {
    #[must_use]
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    #[must_use]
    pub fn into_inner(self) -> String {
        self.0
    }
}

impl fmt::Display for SafeString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl From<String> for SafeString {
    fn from(value: String) -> Self {
        Self(value)
    }
}

impl From<&str> for SafeString {
    fn from(value: &str) -> Self {
        Self(value.to_owned())
    }
}

/// Mark a string as safe (no escaping needed).
#[must_use]
pub fn mark_safe(s: impl Into<String>) -> SafeString {
    SafeString::new(s)
}

/// Concatenate safe strings.
impl Add for SafeString {
    type Output = SafeString;

    fn add(self, rhs: Self) -> Self::Output {
        SafeString(self.0 + &rhs.0)
    }
}

impl AddAssign<&str> for SafeString {
    fn add_assign(&mut self, rhs: &str) {
        self.0 += rhs;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn mark_safe_wraps_content() {
        let safe = mark_safe("<strong>safe</strong>");
        assert_eq!(safe.as_str(), "<strong>safe</strong>");
    }

    #[test]
    fn display_uses_inner_string() {
        let safe = SafeString::new("hello");
        assert_eq!(safe.to_string(), "hello");
    }

    #[test]
    fn add_concatenates_safe_strings() {
        let left = mark_safe("<em>");
        let right = mark_safe("value</em>");
        assert_eq!((left + right).into_inner(), "<em>value</em>");
    }

    #[test]
    fn add_assign_appends_plain_text() {
        let mut safe = mark_safe("Hello");
        safe += " world";
        assert_eq!(safe.as_str(), "Hello world");
    }

    #[test]
    fn from_string_preserves_content() {
        let safe = SafeString::from(String::from("owned"));
        assert_eq!(safe.as_str(), "owned");
    }

    #[test]
    fn from_str_preserves_content() {
        let safe = SafeString::from("borrowed");
        assert_eq!(safe.into_inner(), "borrowed");
    }
}