doido-auth 0.0.22

Unified authentication for Doido — AuthUser trait, extractors, strategies, and auth generators.
Documentation
//! Injects `auth_routes!(User);` understood by the `routes!` macro.

const ROUTES_BASE: &str = include_str!("../../templates/new/config/routes.rs");
const CONTROLLERS_MOD_BASE: &str = include_str!("../../templates/new/app/controllers/mod.rs");
const MODELS_MOD_BASE: &str = include_str!("../../templates/new/app/models/mod.rs");

pub const ROUTES_PATH: &str = "config/routes.rs";
pub const CONTROLLERS_MOD_PATH: &str = "app/controllers/mod.rs";
pub const MODELS_MOD_PATH: &str = "app/models/mod.rs";

fn is_routes_block_open(line: &str) -> bool {
    let t = line.trim();
    (t.starts_with("routes!") || t.starts_with("doido::auth::routes!")) && t.contains('{')
}

/// Injects Devise-style auth routes for generated auth controllers.
pub fn inject_auth_routes(routes: &str, _api: bool) -> String {
    if routes.contains("auth_routes!(User") {
        return routes.to_string();
    }

    let auth_block = "    doido::auth::routes! {";
    let auth_line = "auth_routes!(User, controllers: { \
        sessions: auth::SessionsController, \
        registrations: auth::RegistrationsController, \
        passwords: auth::PasswordsController, \
        oauth: auth::OauthController \
    });";

    let mut lines: Vec<String> = routes.lines().map(String::from).collect();

    if routes.contains("routes! {") && !routes.contains("doido::auth::routes!") {
        for line in &mut lines {
            if line.trim().starts_with("routes! {") {
                *line = auth_block.to_string();
            }
        }
    }

    if routes.contains("use doido::controller::{axum, routes}") {
        for line in &mut lines {
            if line.contains("use doido::controller::{axum, routes}") {
                *line = "use doido::controller::axum;".to_string();
            }
        }
    }

    let auth_use = "use crate::controllers::auth;";
    if !routes.contains(auth_use) {
        let pos = lines
            .iter()
            .rposition(|l| l.contains("use crate::controllers"))
            .map(|i| i + 1)
            .unwrap_or(0);
        lines.insert(pos, auth_use.to_string());
    }

    if let Some(open) = lines.iter().position(|l| is_routes_block_open(l)) {
        if let Some(close_rel) = lines[open..].iter().position(|l| l.trim() == "}") {
            let close = open + close_rel;
            lines.insert(close, format!("        {auth_line}"));
        }
    }

    let mut out = lines.join("\n");
    out.push('\n');
    out
}

/// Injects `resources!(…)` for a scaffold/resource. Idempotent.
pub fn inject_resources(routes: &str, plural: &str, controller: &str, api: bool) -> String {
    let resources = if api {
        format!("resources!({plural}, {controller}, except: [new, edit]);")
    } else {
        format!("resources!({plural}, {controller});")
    };
    if routes.contains(&resources) {
        return routes.to_string();
    }

    let use_line = format!("use crate::controllers::{controller};");
    let mut lines: Vec<String> = routes.lines().map(String::from).collect();

    if !routes.contains(&use_line) {
        let pos = lines
            .iter()
            .rposition(|l| l.contains("use crate::controllers"))
            .map(|i| i + 1)
            .unwrap_or(0);
        lines.insert(pos, use_line);
    }

    if let Some(open) = lines.iter().position(|l| is_routes_block_open(l)) {
        if let Some(close_rel) = lines[open..].iter().position(|l| l.trim() == "}") {
            let close = open + close_rel;
            lines.insert(close, format!("        {resources}"));
        }
    }

    let mut out = lines.join("\n");
    out.push('\n');
    out
}

/// Injects custom GET routes for named controller actions.
pub fn inject_action_routes(
    routes: &str,
    snake: &str,
    controller: &str,
    actions: &[&str],
) -> String {
    let use_line = format!("use crate::controllers::{controller};");
    let mut lines: Vec<String> = routes.lines().map(String::from).collect();

    if !routes.contains(&use_line) {
        let pos = lines
            .iter()
            .rposition(|l| l.contains("use crate::controllers"))
            .map(|i| i + 1)
            .unwrap_or(0);
        lines.insert(pos, use_line);
    }

    if let Some(open) = lines.iter().position(|l| is_routes_block_open(l)) {
        if let Some(close_rel) = lines[open..].iter().position(|l| l.trim() == "}") {
            let close = open + close_rel;
            for action in actions {
                let path = format!("/{snake}/{action}");
                let route = format!("get!(\"{path}\", {controller}::{action});");
                if !routes.contains(&route) {
                    lines.insert(close, format!("        {route}"));
                }
            }
        }
    }

    let mut out = lines.join("\n");
    out.push('\n');
    out
}

/// Appends `mod <name>_controller;` + `pub use …` to `app/controllers/mod.rs`.
pub fn register_controller(controllers_mod: &str, plural: &str, controller: &str) -> String {
    let module = format!("{plural}_controller");
    let decl = format!("mod {module};");
    if controllers_mod.lines().any(|l| l.trim() == decl) {
        return controllers_mod.to_string();
    }
    let mut out = controllers_mod.trim_end().to_string();
    out.push('\n');
    out.push_str(&format!("mod {module};\n"));
    out.push_str(&format!("pub use {module}::{controller};\n"));
    out
}

/// Registers the `auth` controller submodule in `app/controllers/mod.rs`.
pub fn register_auth_controllers_mod(controllers_mod: &str) -> String {
    let decl = "pub mod auth;";
    if controllers_mod.contains(decl) {
        return controllers_mod.to_string();
    }
    let mut out = controllers_mod.trim_end().to_string();
    out.push('\n');
    out.push_str(&format!("{decl}\n"));
    out
}

/// Inserts `pub mod <module>;` into `app/models/mod.rs` above the marker.
pub fn register_model_module(models_mod: &str, module: &str) -> String {
    super::register_module(models_mod, module, "@generated-models")
}

pub fn read_routes() -> String {
    std::fs::read_to_string(ROUTES_PATH).unwrap_or_else(|_| ROUTES_BASE.to_string())
}

pub fn read_controllers_mod() -> String {
    std::fs::read_to_string(CONTROLLERS_MOD_PATH)
        .unwrap_or_else(|_| CONTROLLERS_MOD_BASE.to_string())
}

pub fn read_models_mod() -> String {
    std::fs::read_to_string(MODELS_MOD_PATH).unwrap_or_else(|_| MODELS_MOD_BASE.to_string())
}