forte-cli 0.3.37

CLI for the Forte fullstack web framework
use anyhow::Result;
use std::fs;
use std::path::Path;

pub fn add_page(path: &str) -> Result<()> {
    let project_dir = Path::new(".");

    if !project_dir.join("Forte.toml").exists() {
        anyhow::bail!("Not a Forte project (Forte.toml not found)");
    }

    let page_path = normalize_page_path(path);
    let rs_page_dir = project_dir.join(format!("rs/src/pages/{}", page_path));
    let fe_page_dir = project_dir.join(format!("fe/src/pages/{}", page_path));

    if rs_page_dir.exists() || fe_page_dir.exists() {
        anyhow::bail!("Page '{}' already exists", path);
    }

    fs::create_dir_all(&rs_page_dir)?;
    fs::create_dir_all(&fe_page_dir)?;

    let component_name = path_to_component_name(&page_path);

    fs::write(
        rs_page_dir.join("mod.rs"),
        generate_backend_page(&page_path),
    )?;

    fs::write(
        fe_page_dir.join("page.tsx"),
        generate_frontend_page(&component_name),
    )?;

    println!("Created page '{}'", path);
    println!("  - rs/src/pages/{}/mod.rs", page_path);
    println!("  - fe/src/pages/{}/page.tsx", page_path);

    Ok(())
}

fn normalize_page_path(path: &str) -> String {
    path.trim_matches('/').to_string()
}

fn path_to_component_name(path: &str) -> String {
    let parts: Vec<&str> = path.split('/').collect();
    let last_part = parts.last().unwrap_or(&"page");

    let name = if last_part.starts_with('[') && last_part.ends_with(']') {
        &last_part[1..last_part.len() - 1]
    } else {
        last_part
    };

    let mut result = String::new();
    let mut capitalize_next = true;
    for c in name.chars() {
        if c == '_' || c == '-' {
            capitalize_next = true;
        } else if capitalize_next {
            result.push(c.to_ascii_uppercase());
            capitalize_next = false;
        } else {
            result.push(c);
        }
    }

    format!("{}Page", result)
}

fn generate_backend_page(page_path: &str) -> String {
    let params = extract_params(page_path);

    if params.is_empty() {
        r#"use anyhow::Result;
use forte_sdk::ForteRequest;
use serde::Serialize;

#[derive(Serialize)]
pub enum Props {
    Ok { message: String },
}

pub async fn handler(_req: ForteRequest<'_>) -> Result<Props> {
    Ok(Props::Ok {
        message: "Hello from Forte!".to_string(),
    })
}
"#
        .to_string()
    } else {
        let param_fields = params
            .iter()
            .map(|p| format!("    pub {}: String,", p))
            .collect::<Vec<_>>()
            .join("\n");

        format!(
            r#"use anyhow::Result;
use forte_sdk::ForteRequest;
use serde::{{Deserialize, Serialize}};

#[derive(Deserialize)]
pub struct PathParams {{
{param_fields}
}}

#[derive(Serialize)]
pub enum Props {{
    Ok {{ message: String }},
}}

pub async fn handler(_req: ForteRequest<'_>, _path_params: PathParams) -> Result<Props> {{
    Ok(Props::Ok {{
        message: "Hello from Forte!".to_string(),
    }})
}}
"#
        )
    }
}

fn generate_frontend_page(component_name: &str) -> String {
    format!(
        r#"import type {{ Props }} from "./.props";

export default function {component_name}(props: Props) {{
    if (props.t !== "Ok") {{
        return <div>Error loading page</div>;
    }}

    return (
        <div>
            <h1>{component_name}</h1>
            <p>{{props.message}}</p>
        </div>
    );
}}
"#
    )
}

fn extract_params(path: &str) -> Vec<String> {
    path.split('/')
        .filter(|s| s.starts_with('[') && s.ends_with(']'))
        .map(|s| s[1..s.len() - 1].to_string())
        .collect()
}

pub fn add_action(path: &str) -> Result<()> {
    let project_dir = Path::new(".");

    if !project_dir.join("Forte.toml").exists() {
        anyhow::bail!("Not a Forte project (Forte.toml not found)");
    }

    let action_path = normalize_action_path(path);
    let rs_action_file = project_dir.join(format!("rs/src/actions/{}.rs", action_path));

    if rs_action_file.exists() {
        anyhow::bail!("Action '{}' already exists", path);
    }

    if let Some(parent) = rs_action_file.parent() {
        fs::create_dir_all(parent)?;
    }

    let action_name = path_to_action_name(&action_path);

    fs::write(&rs_action_file, generate_backend_action(&action_name))?;

    println!("Created action '{}'", path);
    println!("  - rs/src/actions/{}.rs", action_path);
    println!(
        "  - import from fe/src/actions/.generated/{}.ts (auto-generated on next forte build/dev)",
        action_path
    );

    Ok(())
}

fn normalize_action_path(path: &str) -> String {
    path.trim_matches('/').replace('-', "_")
}

fn path_to_action_name(path: &str) -> String {
    let parts: Vec<&str> = path.split('/').collect();
    let last_part = parts.last().unwrap_or(&"action");

    let mut result = String::new();
    let mut capitalize_next = true;
    for c in last_part.chars() {
        if c == '_' || c == '-' {
            capitalize_next = true;
        } else if capitalize_next {
            result.push(c.to_ascii_uppercase());
            capitalize_next = false;
        } else {
            result.push(c);
        }
    }

    result
}

fn generate_backend_action(_action_name: &str) -> String {
    r#"use forte_sdk::ForteRequest;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct Input {
    pub message: String,
}

#[derive(Serialize)]
pub enum Output {
    Ok { result: String },
    Error { message: String },
}

pub async fn handler(req: ForteRequest<'_, Input>) -> Output {
    Output::Ok {
        result: format!("Received: {}", req.body.message),
    }
}
"#
    .to_string()
}