rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use minijinja::Value;

#[must_use]
pub fn split_token(token: &str) -> Vec<String> {
    let mut tokens = Vec::new();
    let mut current = String::new();
    let mut quote: Option<char> = None;
    let mut escaped = false;

    for ch in token.chars() {
        if let Some(active_quote) = quote {
            current.push(ch);
            if escaped {
                escaped = false;
                continue;
            }
            if ch == '\\' {
                escaped = true;
                continue;
            }
            if ch == active_quote {
                quote = None;
            }
            continue;
        }

        match ch {
            '\'' | '"' => {
                quote = Some(ch);
                current.push(ch);
            }
            '(' | ')' => {
                if !current.is_empty() {
                    tokens.push(std::mem::take(&mut current));
                }
                tokens.push(ch.to_string());
            }
            _ if ch.is_whitespace() => {
                if !current.is_empty() {
                    tokens.push(std::mem::take(&mut current));
                }
            }
            _ => current.push(ch),
        }
    }

    if !current.is_empty() {
        tokens.push(current);
    }

    tokens
}

#[must_use]
pub fn resolve_variable(var_name: &str, context: &Value) -> Option<Value> {
    if var_name.trim().is_empty() {
        return None;
    }

    let mut current = context.clone();
    for segment in var_name.split('.') {
        if segment.is_empty() {
            return None;
        }
        current = resolve_segment(&current, segment)?;
    }

    (!current.is_undefined()).then_some(current)
}

fn resolve_segment(value: &Value, segment: &str) -> Option<Value> {
    if let Ok(attr) = value.get_attr(segment)
        && !attr.is_undefined()
    {
        return Some(attr);
    }

    if let Ok(index) = segment.parse::<usize>()
        && let Ok(mut iter) = value.try_iter()
    {
        return iter.nth(index);
    }

    None
}

#[cfg(test)]
mod tests {
    use minijinja::{Value, context};

    use super::{resolve_variable, split_token};

    #[test]
    fn split_token_splits_unquoted_content_on_whitespace() {
        let tokens = split_token("include shared/card.html only");

        assert_eq!(tokens, vec!["include", "shared/card.html", "only"]);
    }

    #[test]
    fn split_token_preserves_quoted_segments() {
        let tokens = split_token("include \"shared/card with space.html\" only");

        assert_eq!(
            tokens,
            vec!["include", "\"shared/card with space.html\"", "only"]
        );
    }

    #[test]
    fn split_token_emits_parentheses_as_standalone_tokens() {
        let tokens = split_token("not (alpha and beta)");

        assert_eq!(tokens, vec!["not", "(", "alpha", "and", "beta", ")"]);
    }

    #[test]
    fn split_token_keeps_escaped_quotes_inside_quoted_text() {
        let tokens = split_token("say \"escaped \\\"quote\\\"\"");

        assert_eq!(tokens, vec!["say", "\"escaped \\\"quote\\\"\""]);
    }

    #[test]
    fn resolve_variable_reads_top_level_values() {
        let context = context!(name => "Rjango");

        assert_eq!(
            resolve_variable("name", &context),
            Some(Value::from("Rjango"))
        );
    }

    #[test]
    fn resolve_variable_reads_nested_attributes() {
        let context = context!(user => context!(profile => context!(name => "Alice")));

        assert_eq!(
            resolve_variable("user.profile.name", &context),
            Some(Value::from("Alice"))
        );
    }

    #[test]
    fn resolve_variable_reads_list_indexes() {
        let context = context!(items => vec!["first", "second", "third"]);

        assert_eq!(
            resolve_variable("items.1", &context),
            Some(Value::from("second"))
        );
    }

    #[test]
    fn resolve_variable_returns_none_for_missing_values() {
        let context = context!(name => "Rjango");

        assert_eq!(resolve_variable("missing", &context), None);
        assert_eq!(resolve_variable("name.first", &context), None);
    }
}