rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Index {
    pub fields: Vec<String>,
    pub name: String,
    pub db_tablespace: Option<String>,
    pub opclasses: Vec<String>,
    pub condition: Option<String>,
    pub include: Vec<String>,
    pub expressions: Vec<String>,
}

#[must_use]
fn format_condition(condition: Option<&str>) -> String {
    condition
        .filter(|value| !value.trim().is_empty())
        .map_or_else(String::new, |value| format!(" WHERE {value}"))
}

#[must_use]
fn format_include(columns: &[String]) -> String {
    if columns.is_empty() {
        String::new()
    } else {
        format!(" INCLUDE ({})", columns.join(", "))
    }
}

#[must_use]
fn format_tablespace(db_tablespace: Option<&str>) -> String {
    db_tablespace
        .filter(|value| !value.trim().is_empty())
        .map_or_else(String::new, |value| format!(" TABLESPACE {value}"))
}

#[must_use]
fn join_fields_with_opclasses(fields: &[String], opclasses: &[String]) -> Vec<String> {
    fields
        .iter()
        .enumerate()
        .map(|(index, field)| match opclasses.get(index) {
            Some(opclass) if !opclass.trim().is_empty() => format!("{field} {opclass}"),
            _ => field.clone(),
        })
        .collect()
}

impl Index {
    #[must_use]
    pub fn create_sql(&self, table: &str) -> String {
        let mut items = self.expressions.clone();
        items.extend(join_fields_with_opclasses(&self.fields, &self.opclasses));
        format!(
            "CREATE INDEX {} ON {} ({}){}{}{}",
            self.name,
            table,
            items.join(", "),
            format_condition(self.condition.as_deref()),
            format_include(&self.include),
            format_tablespace(self.db_tablespace.as_deref()),
        )
    }

    #[must_use]
    pub fn remove_sql(&self, _table: &str) -> String {
        format!("DROP INDEX IF EXISTS {}", self.name)
    }

    #[must_use]
    pub fn describe(&self) -> String {
        let mut items = self.expressions.clone();
        items.extend(self.fields.iter().cloned());

        let mut description = format!("Index {} on {}", self.name, items.join(", "));
        if let Some(condition) = self.condition.as_deref() {
            description.push_str(&format!(" where {condition}"));
        }
        if !self.include.is_empty() {
            description.push_str(&format!(" include {}", self.include.join(", ")));
        }
        description
    }
}

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

    fn base_index() -> Index {
        Index {
            fields: vec!["title".into()],
            name: "book_title_idx".into(),
            db_tablespace: None,
            opclasses: Vec::new(),
            condition: None,
            include: Vec::new(),
            expressions: Vec::new(),
        }
    }

    #[test]
    fn test_index_basic_create_sql() {
        let index = base_index();

        assert_eq!(
            index.create_sql("books"),
            "CREATE INDEX book_title_idx ON books (title)"
        );
    }

    #[test]
    fn test_index_with_condition() {
        let mut index = base_index();
        index.condition = Some("published = TRUE".into());

        assert_eq!(
            index.create_sql("books"),
            "CREATE INDEX book_title_idx ON books (title) WHERE published = TRUE"
        );
    }

    #[test]
    fn test_index_with_include() {
        let mut index = base_index();
        index.include = vec!["id".into(), "slug".into()];

        assert_eq!(
            index.create_sql("books"),
            "CREATE INDEX book_title_idx ON books (title) INCLUDE (id, slug)"
        );
    }

    #[test]
    fn test_index_remove_sql() {
        let index = base_index();

        assert_eq!(
            index.remove_sql("books"),
            "DROP INDEX IF EXISTS book_title_idx"
        );
    }

    #[test]
    fn test_index_describe() {
        let mut index = base_index();
        index.condition = Some("published = TRUE".into());
        index.include = vec!["slug".into()];

        assert_eq!(
            index.describe(),
            "Index book_title_idx on title where published = TRUE include slug"
        );
    }

    #[test]
    fn index_create_sql_supports_expressions_opclasses_and_tablespaces() {
        let index = Index {
            fields: vec!["title".into()],
            name: "book_search_idx".into(),
            db_tablespace: Some("fastspace".into()),
            opclasses: vec!["text_pattern_ops".into()],
            condition: None,
            include: Vec::new(),
            expressions: vec!["lower(title)".into()],
        };

        assert_eq!(
            index.create_sql("books"),
            "CREATE INDEX book_search_idx ON books (lower(title), title text_pattern_ops) TABLESPACE fastspace"
        );
    }
}