biolib 1.3.117

BioLib client library and CLI for running applications on BioLib
Documentation
use std::fs;
use std::path::Path;
use std::sync::OnceLock;

use serde::Deserialize;

use crate::api::ApiClient;
use crate::config::Config;
use crate::error::BioLibError;

const SYSTEM_SECRET_PATH: &str = "/biolib/secrets/biolib_system_secret";

#[derive(Debug, Clone, Deserialize)]
struct RuntimeJobData {
    version: String,
    job_uuid: String,
    job_auth_token: String,
    app_uri: String,
    #[serde(default)]
    is_environment_biolib_cloud: bool,
    #[serde(default)]
    job_requested_machine: Option<String>,
    #[serde(default)]
    job_requested_machine_spot: bool,
    #[serde(default)]
    job_reserved_machines: u32,
}

static JOB_DATA: OnceLock<Option<RuntimeJobData>> = OnceLock::new();

fn try_get_job_data() -> Option<&'static RuntimeJobData> {
    JOB_DATA
        .get_or_init(|| {
            let content = fs::read_to_string(SYSTEM_SECRET_PATH).ok()?;
            let data: RuntimeJobData = serde_json::from_str(&content).ok()?;
            if !data.version.starts_with("1.") {
                return None;
            }
            Some(data)
        })
        .as_ref()
}

fn get_job_data() -> crate::Result<&'static RuntimeJobData> {
    try_get_job_data().ok_or_else(|| {
        BioLibError::General("The runtime is not recognized as a BioLib app".to_string())
    })
}

fn validate_secret_name(name: &str) -> crate::Result<()> {
    if name
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
    {
        Ok(())
    } else {
        Err(BioLibError::Validation(
            "Secret name can only contain alphanumeric characters and dashes or underscores"
                .to_string(),
        ))
    }
}

pub struct Runtime;

impl Runtime {
    pub fn check_is_environment_biolib_app() -> bool {
        try_get_job_data().is_some()
    }

    pub fn check_is_environment_biolib_cloud() -> crate::Result<bool> {
        Ok(get_job_data()?.is_environment_biolib_cloud)
    }

    pub fn get_job_id() -> crate::Result<String> {
        Ok(get_job_data()?.job_uuid.clone())
    }

    pub fn get_job_auth_token() -> crate::Result<String> {
        Ok(get_job_data()?.job_auth_token.clone())
    }

    pub fn get_job_requested_machine() -> crate::Result<Option<String>> {
        let data = get_job_data()?;
        Ok(data.job_requested_machine.clone().filter(|m| !m.is_empty()))
    }

    pub fn is_spot_machine_requested() -> crate::Result<bool> {
        Ok(get_job_data()?.job_requested_machine_spot)
    }

    pub fn get_app_uri() -> crate::Result<String> {
        Ok(get_job_data()?.app_uri.clone())
    }

    pub fn get_max_workers() -> crate::Result<u32> {
        Ok(get_job_data()?.job_reserved_machines)
    }

    pub fn get_secret(secret_name: &str) -> crate::Result<Vec<u8>> {
        validate_secret_name(secret_name)?;
        let path = format!("/biolib/secrets/{secret_name}");
        fs::read(&path).map_err(|_| {
            BioLibError::General(format!("Unable to get system secret: {secret_name}"))
        })
    }

    pub fn get_temporary_client_secret(secret_name: &str) -> crate::Result<Vec<u8>> {
        validate_secret_name(secret_name)?;
        let path = format!("/biolib/temporary-client-secrets/{secret_name}");
        fs::read(&path)
            .map_err(|_| BioLibError::General(format!("Unable to get secret: {secret_name}")))
    }

    pub fn set_main_result_prefix(result_prefix: &str) -> crate::Result<()> {
        let data = get_job_data()?;
        let config = Config::load();
        let mut client = ApiClient::new(&config)?;
        let mut headers = std::collections::HashMap::new();
        headers.insert("Job-Auth-Token".to_string(), data.job_auth_token.clone());
        let body = serde_json::json!({"result_name_prefix": result_prefix});
        let path = format!("/jobs/{}/main_result/", data.job_uuid);
        client.patch_json_with_headers(&path, &body, &headers)?;
        Ok(())
    }

    pub fn set_result_name_prefix(result_prefix: &str) -> crate::Result<()> {
        Self::set_main_result_prefix(result_prefix)
    }

    pub fn set_result_name_prefix_from_fasta(path_to_fasta: &str) -> crate::Result<()> {
        let file_name = Path::new(path_to_fasta)
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or(path_to_fasta);

        let result_name = if is_biolib_generated_name(file_name) {
            let content = fs::read_to_string(path_to_fasta).map_err(|err| {
                BioLibError::General(format!(
                    "Failed to set result name from fasta file {path_to_fasta}: {err}"
                ))
            })?;
            match parse_first_fasta_id(&content) {
                Some(id) => {
                    let truncated: String = id.replace(' ', "_").chars().take(60).collect();
                    truncated
                }
                None => return Ok(()),
            }
        } else {
            file_name.to_string()
        };

        Self::set_result_name_prefix(&result_name)
    }

    pub fn set_result_name_from_file(path_to_file: &str) -> crate::Result<()> {
        if path_to_file.to_lowercase().ends_with(".fasta") {
            return Self::set_result_name_prefix_from_fasta(path_to_file);
        }

        let file_name = Path::new(path_to_file)
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or(path_to_file);

        if !is_biolib_generated_name(file_name) {
            let result_name: String = file_name.chars().take(60).collect();
            Self::set_result_name_prefix(&result_name)?;
        }

        Ok(())
    }

    pub fn set_result_name_from_string(result_name: &str) -> crate::Result<()> {
        let truncated: String = result_name.chars().take(60).collect();
        Self::set_result_name_prefix(&truncated)
    }

    pub fn create_result_note(note: &str) -> crate::Result<()> {
        let job_id = Self::get_job_id()?;
        let config = Config::load();
        let mut client = ApiClient::new(&config)?;
        let body = serde_json::json!({"note": note});
        client.post_json(&format!("/jobs/{job_id}/notes/"), &body)?;
        Ok(())
    }

    pub fn get_oauth_access_token(scope: &str) -> crate::Result<String> {
        get_job_data()?;
        let config = Config::load();
        let client = ApiClient::new(&config)?;
        let body = serde_json::json!({"scope": scope});
        let body_bytes = serde_json::to_vec(&body)?;
        let url = format!("{}/api/auth/oauth-token-exchange/", client.base_url());
        let http = crate::api::HttpClient::shared();
        let response = http.request("POST", &url, Some(&body_bytes), None, 5, None)?;

        #[derive(Deserialize)]
        struct OAuthResponse {
            access_token: Option<String>,
        }
        let oauth_response: OAuthResponse = response.json()?;
        oauth_response
            .access_token
            .ok_or_else(|| BioLibError::General("Failed to get OAuth access token".to_string()))
    }
}

fn is_biolib_generated_name(name: &str) -> bool {
    let mut chars = name.chars();
    if !name.starts_with("input_") {
        return false;
    }
    for _ in 0..6 {
        chars.next();
    }
    chars
        .next()
        .map(|c| c.is_ascii_alphanumeric())
        .unwrap_or(false)
}

fn parse_first_fasta_id(content: &str) -> Option<String> {
    for line in content.lines() {
        if let Some(header) = line.strip_prefix('>') {
            let id = header.split_whitespace().next().unwrap_or(header);
            if !id.is_empty() {
                return Some(id.to_string());
            }
        }
    }
    None
}