stackrup 0.1.2

Stackrup into the world of micro-rollups using Stackr CLI
use regex::Regex;
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;

#[derive(Debug)]
pub struct ParsedData {
    rollup_name: String,
    stf_function: String,
}

pub fn importparser(destination_file_path: &PathBuf) -> io::Result<()> {
    let current_dir = env::current_dir()?;

    let content = fs::read_to_string(&destination_file_path)?;

    let re_usage = Regex::new(r"\bethers(?:\.([a-zA-Z0-9_]+))?\.[a-zA-Z0-9_]+\b").unwrap();

    let mut ethers_members = HashMap::new();

    let new_content = re_usage.replace_all(&content, |caps: &regex::Captures| {
        let member = caps[0].to_string();
        let split: Vec<&str> = member.split('.').collect();
        let (import_path, import_member) = match split.as_slice() {
            [_, subpath, name] => (format!("ethers/{}", subpath), name.to_string()),
            [_, name] => ("ethers".to_string(), name.to_string()),
            _ => unreachable!(),
        };

        ethers_members
            .entry(import_member.clone())
            .or_insert_with(|| import_path);

        import_member
    });

    let mut import_lines = String::new();
    for (member, path) in &ethers_members {
        import_lines.push_str(&format!("import {{{}}} from \"{}\";\n", member, path));
    }
    if !import_lines.is_empty() {
        import_lines.push('\n');
    }

    let final_content = format!("{}{}", import_lines, new_content);

    let mut output_file = fs::File::create(&destination_file_path)?;
    output_file.write_all(final_content.as_bytes())?;

    Ok(())
}

pub fn configparser() -> io::Result<()> {
    let current_dir = env::current_dir()?;

    let destination_file_path = current_dir.join(format!("stackr.config.ts"));

    let content = fs::read_to_string(&destination_file_path)?;

    // Parse the JSON content into a serde_json::Value
    let json_value: Value = serde_json::from_str(&content)?;

    // Convert the JSON value to a string with unquoted keys
    let js_object_literal = format!(
        "const stackrConfig: StackrConfig = {};",
        json_to_js_object_literal(&json_value)
    );

    // Write the JS object literal to a new file
    let mut output_file = fs::File::create(&destination_file_path)?;
    output_file.write_all(js_object_literal.as_bytes())?;

    Ok(())
}

pub fn remove_unwanted_lines(file_path: &PathBuf) -> io::Result<()> {
    println!("Sanitizing files ...");

    let data = fs::read_to_string(&file_path)?;
    let console_log_regex = Regex::new(r"^\s*console\.log\(.*\);\s*$")
        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
    let import_ethers_regex = Regex::new(r#"import\s+\{\s*ethers\s*\}\s+from\s+['"]ethers['"];"#)
        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
    let cleaned_data = data
        .lines()
        .filter(|line| !console_log_regex.is_match(line) || !import_ethers_regex.is_match(line))
        .collect::<Vec<&str>>()
        .join("\n");

    let mut file = fs::File::create(&file_path)?;
    file.write_all(cleaned_data.as_bytes())?;

    println!("Santization complete");
    Ok(())
}

pub fn append_js_functions_to_file<P: AsRef<Path>>(file_path: P) -> std::io::Result<()> {
    let js_code = r#"

    // Execution step
// Read inputs from stdin
const stfInputs = readInput();
var newState = stfInputs.currentState;
//this sets all product descriptions to a max length of 10 characters
stfInputs.actions.forEach((action) => {
  newState = counterSTF.apply(action, newState);
});
// Write the result to stdout
writeOutput(newState);

function readInput() {
    const chunkSize = 1024;
    const inputChunks = [];
    let totalBytes = 0;

    // Read all the available bytes
    while (1) {
        const buffer = new Uint8Array(chunkSize);
        // Stdin file descriptor
        const fd = 0;
        const bytesRead = Javy.IO.readSync(fd, buffer);

        totalBytes += bytesRead;
        if (bytesRead === 0) {
            break;
        }
        inputChunks.push(buffer.subarray(0, bytesRead));
    }

    // Assemble input into a single Uint8Array
    const { finalBuffer } = inputChunks.reduce((context, chunk) => {
        context.finalBuffer.set(chunk, context.bufferOffset);
        context.bufferOffset += chunk.length;
        return context;
    }, { bufferOffset: 0, finalBuffer: new Uint8Array(totalBytes) });

    return JSON.parse(new TextDecoder().decode(finalBuffer));
}

// Write output to stdout
function writeOutput(output) {
    const encodedOutput = new TextEncoder().encode(JSON.stringify(output));
    const buffer = new Uint8Array(encodedOutput);
    // Stdout file descriptor
    const fd = 1;
    Javy.IO.writeSync(fd, buffer);
}
"#;

    let current_dir = env::current_dir()?;

    let _ = Command::new("mkdir")
    .arg("build")
    .current_dir(&current_dir)
    .output();

    let build_file_path = current_dir.join("build/state.ts");

    let _ = std::fs::copy(&file_path, &build_file_path);

    let mut file = OpenOptions::new()
        .write(true)
        .append(true)
        .open(&build_file_path)?;

    writeln!(file, "{}", js_code)?;

    Ok(())
}

// Function to convert JSON Value to a JavaScript object literal string
fn json_to_js_object_literal(value: &Value) -> String {
    match value {
        Value::Object(map) => map_to_js_object_literal(map),
        _ => value.to_string(),
    }
}

// Helper function to convert a serde_json Map to a JS object literal string
fn map_to_js_object_literal(map: &Map<String, Value>) -> String {
    let mut object_literal = String::from("{");

    for (i, (k, v)) in map.iter().enumerate() {
        if i > 0 {
            object_literal.push_str(", ");
        }
        object_literal.push_str(&format!("{}: {}", k, json_to_js_object_literal(v)));
    }

    object_literal.push('}');
    object_literal
}

pub fn read_first_account<P: AsRef<Path>>(file_path: P) -> Result<String, Box<dyn Error>> {
    let data = fs::read_to_string(&file_path)?;

    // Regular expression to match the accounts array
    let re = Regex::new(r#"accounts:\s*\[\s*\"([^\"]+)\""#)?;

    if let Some(caps) = re.captures(&data) {
        if let Some(account) = caps.get(1) {
            return Ok(account.as_str().to_string());
        }
    }

    Err("No account found".into())
}


pub fn generate_import_statement(file_path: &PathBuf) -> Result<String, Box<dyn Error>> {
    let content = fs::read_to_string(file_path)?;

    let re = Regex::new(r"export (?:type|class) (\w+)|export const (\w+)").unwrap();

    let mut exports = vec![];

    for caps in re.captures_iter(&content) {
        if let Some(export_name) = caps.get(1).or_else(|| caps.get(2)) {
            exports.push(export_name.as_str().to_string());
        }
    }

    let import_statement = if !exports.is_empty() {
        format!("import {{ {} }} from \"./state\";", exports.join(", "))
    } else {
        String::new()
    };

    Ok(import_statement)
}

pub fn create_js_function(import_statement: &str) -> Result<String, Box<dyn Error>> {
    let current_dir = env::current_dir()?;
    let state_file_dir = current_dir.join("src/state.ts");

    let result = parse_typescript_file(&state_file_dir)?;

    let rollup_name = &result.rollup_name;
    let stf_name = &result.stf_function;

    let first_char = rollup_name.chars().next().unwrap().to_lowercase().to_string();
    let lowercase_rollup_name = first_char + &rollup_name[1..] + "Object";

    Ok(format!(
        r#"const run = (prevState: StateVariable, actions: any[]) => {{
    const {} = new {}(prevState);
    const fsm = new StateMachine({{
      state: {},
      stf: {},
    }});

    actions.forEach((action: any) => {{
      fsm.apply(action);
    }});

    return fsm.state.getState();
}};"#, lowercase_rollup_name, rollup_name, lowercase_rollup_name, stf_name
    ))
}

pub fn parse_typescript_file(file_path: &PathBuf) -> Result<ParsedData, Box<dyn Error>> {
    let content = fs::read_to_string(file_path)?;

    let rollup_regex = Regex::new(r"export class (\w+) extends RollupState<")?;
    let rollup_name = rollup_regex
        .captures(&content)
        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
        .ok_or("RollupState class not found")?;

    let stf_regex = Regex::new(r"export const (\w+): STF<")?;
    let stf_function = stf_regex
        .captures(&content)
        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
        .ok_or("STF function not found")?;

    Ok(ParsedData {
        rollup_name,
        stf_function,
    })
}