Login
4 branches 0 tags
Ben (Desktop/Arch) Use production version of lit in prod. e23a0d3 1 month ago 93 Commits
rubhub / src / app.rs
use askama::Template;
#[cfg(debug_assertions)]
use tokio::fs;

use crate::{
    entities::{AccessType, Project, User},
    services::repository::{GitRefInfo, GitSummary},
};

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
    featured: &'a [ProjectSummary<'a>],
}

#[derive(Template)]
#[template(path = "contact.html")]
struct ContactTemplate;

#[derive(Template)]
#[template(path = "404.html")]
struct NotFoundTemplate;

#[derive(Template)]
#[template(path = "login.html")]
struct LoginTemplate<'a> {
    message: Option<&'a str>,
}

#[derive(Template)]
#[template(path = "user_settings.html")]
struct UserSettingsTemplate<'a> {
    user: &'a User,
    ssh_keys: &'a [String],
    message: Option<&'a str>,
}

#[derive(Template)]
#[template(path = "user.html")]
struct UserTemplate<'a> {
    user: &'a User,
    projects: &'a [ProjectSummary<'a>],
    is_owner: bool,
}

#[derive(Clone, Debug)]
pub struct ProjectSummary<'a> {
    pub name: &'a str,
    pub slug: &'a str,
    pub owner_slug: &'a str,
    pub owner_name: &'a str,
    pub description: &'a str,
}

impl<'a> ProjectSummary<'a> {
    pub fn uri(&self) -> String {
        format!("/~{}/{}", self.owner_slug, self.slug)
    }

    pub fn owner_uri(&self) -> String {
        format!("/~{}", self.owner_slug)
    }
}

#[derive(Template)]
#[template(path = "project_new.html")]
struct NewProjectTemplate<'a> {
    message: Option<&'a str>,
    public_access: &'a str,
}

#[derive(Template)]
#[template(path = "project.html")]
struct ProjectTemplate<'a> {
    owner: &'a User,
    project: &'a Project,
    access_level: AccessType,
    ssh_clone_url: String,
    selected_branch: String,
    summary: GitSummary,
    info: Option<GitRefInfo>,
    readme_html: Option<String>,
}

#[derive(Template)]
#[template(path = "project_commits.html")]
struct ProjectCommitsTemplate<'a> {
    owner: &'a User,
    project: &'a Project,
    selected_branch: String,
    access_level: AccessType,
    ssh_clone_url: String,
    summary: GitSummary,
    info: Option<GitRefInfo>,
    current_page: i32,
    page_count: i32,
    page_min: i32,
    page_max: i32,
}

#[derive(Template)]
#[template(path = "project_settings.html")]
struct ProjectSettingsTemplate<'a> {
    owner: &'a User,
    project: &'a Project,
    message: Option<&'a str>,
}

#[cfg(not(debug_assertions))]
const APP_THEME: &str = include_str!("../dist/app.html");

pub fn extract_html_parts(html: &str) -> (&str, &str) {
    let first_split = html
        .split_once("</head>")
        .expect("extract_html_parts first_split");
    let head = first_split
        .0
        .split_once("<head>")
        .expect("extract_html_parts head")
        .1;
    let rest = first_split
        .1
        .split_once("<body>")
        .expect("extract_html_parts rest")
        .1;
    let body = rest
        .split_once("</body>")
        .expect("extract_html_parts body")
        .0;

    (head, body)
}

async fn theme(head: &str, body: &str) -> String {
    #[cfg(not(debug_assertions))]
    let contents = APP_THEME;
    #[cfg(debug_assertions)]
    let contents = fs::read_to_string("dist/app.html").await.unwrap();

    let contents = contents.replace("<!--HEAD-->", head);
    contents.replace("<!--BODY-->", body)
}

pub async fn index(featured: &[ProjectSummary<'_>]) -> String {
    let contents = IndexTemplate { featured }.render().unwrap();
    let (head, body) = extract_html_parts(&contents);
    theme(head, body).await
}

pub async fn contact() -> String {
    let contents = ContactTemplate.render().unwrap();
    let (head, body) = extract_html_parts(&contents);
    theme(head, body).await
}

pub async fn not_found() -> String {
    let contents = NotFoundTemplate.render().unwrap();
    let (head, body) = extract_html_parts(&contents);
    theme(head, body).await
}

pub async fn login(message: Option<&str>) -> String {
    let contents = LoginTemplate { message }.render().unwrap();

    let parts = extract_html_parts(&contents);

    theme(parts.0, parts.1).await
}

pub async fn settings(user: User, ssh_keys: &[String], message: Option<&str>) -> String {
    let contents = UserSettingsTemplate {
        user: &user,
        ssh_keys,
        message,
    }
    .render()
    .unwrap();

    let parts = extract_html_parts(&contents);

    theme(parts.0, parts.1).await
}

pub async fn projects(user: &User, projects: &[ProjectSummary<'_>], is_owner: bool) -> String {
    let contents = UserTemplate {
        user,
        projects,
        is_owner,
    }
    .render()
    .unwrap();

    let parts = extract_html_parts(&contents);

    theme(parts.0, parts.1).await
}

pub async fn new_project(message: Option<&str>, public_access: AccessType) -> String {
    let contents = NewProjectTemplate {
        message,
        public_access: public_access.as_str(),
    }
    .render()
    .unwrap();

    let parts = extract_html_parts(&contents);

    theme(parts.0, parts.1).await
}

pub async fn project_with_access(
    owner: User,
    project: Project,
    access_level: AccessType,
    ssh_clone_url: String,
    summary: GitSummary,
    info: Option<GitRefInfo>,
    readme_html: Option<String>,
) -> String {
    let selected_branch = info
        .as_ref()
        .map(|i| i.branch_name.to_string())
        .unwrap_or_default();

    let contents = ProjectTemplate {
        owner: &owner,
        project: &project,
        access_level,
        ssh_clone_url,
        summary,
        info,
        selected_branch,
        readme_html,
    }
    .render()
    .unwrap();

    let parts = extract_html_parts(&contents);

    theme(parts.0, parts.1).await
}

pub async fn project_commits(
    owner: User,
    project: Project,
    access_level: AccessType,
    ssh_clone_url: String,
    summary: GitSummary,
    info: Option<GitRefInfo>,
    current_page: i32,
    page_count: i32,
) -> String {
    let selected_branch = info
        .as_ref()
        .map(|i| i.branch_name.to_string())
        .unwrap_or_default();

    let page_min = (current_page - 5).max(0);
    let page_max = (current_page + 5).min(page_count);
    let contents = ProjectCommitsTemplate {
        owner: &owner,
        project: &project,
        access_level,
        ssh_clone_url,
        summary,
        info,
        selected_branch,
        current_page,
        page_count,
        page_min,
        page_max,
    }
    .render()
    .unwrap();

    let parts = extract_html_parts(&contents);

    theme(parts.0, parts.1).await
}

pub async fn project_settings(owner: User, project: Project, message: Option<&str>) -> String {
    let contents = ProjectSettingsTemplate {
        owner: &owner,
        project: &project,
        message,
    }
    .render()
    .unwrap();

    let parts = extract_html_parts(&contents);

    theme(parts.0, parts.1).await
}

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

    #[test]
    fn extract_html_parts_returns_head_and_body() {
        let html = "<html><head><title>Test</title></head><body><h1>Hi</h1></body></html>";
        let (head, body) = extract_html_parts(html);

        assert_eq!(head, "<title>Test</title>");
        assert_eq!(body, "<h1>Hi</h1>");
    }

    #[test]
    fn extract_html_parts_handles_whitespace() {
        let html =
            "<html><head>\n<meta charset=\"utf-8\">\n</head><body>\n<p>Test</p>\n</body></html>";
        let (head, body) = extract_html_parts(html);

        assert_eq!(head, "\n<meta charset=\"utf-8\">\n");
        assert_eq!(body, "\n<p>Test</p>\n");
    }
}