primer3-tool 0.1.0

Command-line tool for PCR primer design and thermodynamic calculations
//! Command-line tool for PCR primer design and thermodynamic calculations.

use std::io::{self, BufRead, Read as _};
use std::process;

use clap::{Parser, Subcommand};

/// primer3-tool: PCR primer design and thermodynamic calculations.
#[derive(Parser)]
#[command(version, about)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Calculate melting temperature for one or more sequences.
    Tm(TmArgs),
    /// Calculate hairpin thermodynamics for a sequence.
    Hairpin(SingleSeqArgs),
    /// Calculate homodimer thermodynamics for a sequence.
    Homodimer(SingleSeqArgs),
    /// Calculate heterodimer thermodynamics for two sequences.
    Heterodimer(HeterodimerArgs),
    /// Design primers from boulder-format input (stdin).
    Design(DesignArgs),
}

#[derive(Parser)]
struct TmArgs {
    /// DNA sequences to analyze. If none provided, reads from stdin (one per line).
    sequences: Vec<String>,
}

#[derive(Parser)]
struct SingleSeqArgs {
    /// DNA sequence to analyze.
    sequence: String,
}

#[derive(Parser)]
struct HeterodimerArgs {
    /// First DNA sequence.
    seq1: String,
    /// Second DNA sequence.
    seq2: String,
}

#[derive(Parser)]
struct DesignArgs {
    /// Output results as JSON instead of boulder format.
    #[arg(long)]
    json: bool,
}

fn main() {
    let cli = Cli::parse();

    let result = match &cli.command {
        Command::Tm(args) => run_tm(args),
        Command::Hairpin(args) => run_hairpin(args),
        Command::Homodimer(args) => run_homodimer(args),
        Command::Heterodimer(args) => run_heterodimer(args),
        Command::Design(args) => run_design(args),
    };

    if let Err(e) = result {
        eprintln!("error: {e}");
        process::exit(1);
    }
}

fn run_tm(args: &TmArgs) -> Result<(), Box<dyn std::error::Error>> {
    if args.sequences.is_empty() {
        // Read from stdin
        let stdin = io::stdin();
        for line in stdin.lock().lines() {
            let seq = line?;
            let seq = seq.trim();
            if seq.is_empty() {
                continue;
            }
            match primer3::calc_tm(seq) {
                Ok(tm) => println!("{seq}\t{tm:.2}"),
                Err(e) => eprintln!("{seq}\tERROR: {e}"),
            }
        }
    } else {
        for seq in &args.sequences {
            match primer3::calc_tm(seq) {
                Ok(tm) => println!("{seq}\t{tm:.2}"),
                Err(e) => eprintln!("{seq}\tERROR: {e}"),
            }
        }
    }
    Ok(())
}

fn run_hairpin(args: &SingleSeqArgs) -> Result<(), Box<dyn std::error::Error>> {
    let result = primer3::calc_hairpin(&args.sequence)?;
    println!("sequence: {}", args.sequence);
    println!("tm:       {:.2} C", result.tm());
    println!("dg:       {:.0} cal/mol", result.dg());
    println!("dh:       {:.0} cal/mol", result.dh());
    println!("ds:       {:.2} cal/mol/K", result.ds());
    Ok(())
}

fn run_homodimer(args: &SingleSeqArgs) -> Result<(), Box<dyn std::error::Error>> {
    let result = primer3::calc_homodimer(&args.sequence)?;
    println!("sequence: {}", args.sequence);
    println!("tm:       {:.2} C", result.tm());
    println!("dg:       {:.0} cal/mol", result.dg());
    println!("dh:       {:.0} cal/mol", result.dh());
    println!("ds:       {:.2} cal/mol/K", result.ds());
    Ok(())
}

fn run_heterodimer(args: &HeterodimerArgs) -> Result<(), Box<dyn std::error::Error>> {
    let result = primer3::calc_heterodimer(&args.seq1, &args.seq2)?;
    println!("seq1: {}", args.seq1);
    println!("seq2: {}", args.seq2);
    println!("tm:   {:.2} C", result.tm());
    println!("dg:   {:.0} cal/mol", result.dg());
    println!("dh:   {:.0} cal/mol", result.dh());
    println!("ds:   {:.2} cal/mol/K", result.ds());
    Ok(())
}

fn run_design(args: &DesignArgs) -> Result<(), Box<dyn std::error::Error>> {
    let mut input = String::new();
    io::stdin().read_to_string(&mut input)?;

    let records = primer3::boulder::parse_boulder(&input);
    if records.is_empty() {
        return Err("no boulder records found on stdin".into());
    }

    for record in &records {
        let seq_args = primer3::boulder::sequence_args_from_boulder(record)?;
        let settings = primer3::boulder::primer_settings_from_boulder(record)?;
        let result = primer3::design_primers(&seq_args, &settings, None, None)?;

        if args.json {
            let boulder_rec = primer3::boulder::design_result_to_boulder(&result);
            // Convert to JSON-friendly map
            let map: std::collections::BTreeMap<&str, &str> = boulder_rec.iter().collect();
            println!("{}", serde_json::to_string_pretty(&map)?);
        } else {
            let boulder_rec = primer3::boulder::design_result_to_boulder(&result);
            print!("{}", primer3::boulder::format_boulder(&boulder_rec));
        }
    }

    Ok(())
}