mybinder 0.1.0

MyBinder build API
Documentation
use anyhow::Result;
use serde::Deserialize;

#[derive(Debug, Deserialize, PartialEq)]
#[serde(tag = "phase", rename_all = "lowercase")]
pub enum Phase {
    Built {
        message: Option<String>,
        #[serde(rename = "imageName")]
        image_name: Option<String>,
    },
    Launching {
        message: Option<String>,
    },
    Ready {
        message: Option<String>,
        url: String,
        token: String,
        image: String,
        #[serde(rename = "repo_url")]
        repo_url: String,
        #[serde(rename = "binder_ref_url")]
        binder_ref_url: String,
        #[serde(rename = "binder_launch_host")]
        binder_launch_host: String,
        #[serde(rename = "binder_request")]
        binder_request: String,
        #[serde(rename = "binder_persistent_request")]
        binder_persistent_request: String,
    },
    Failed {
        message: Option<String>,
    },
    Waiting {
        message: Option<String>,
    },
    Fetching {
        message: Option<String>,
    },
    Building {
        message: Option<String>,
    },
    Unknown {
        message: Option<String>,
    },
}

#[derive(Debug, Deserialize, PartialEq)]
pub struct BinderBuildResponse {
    #[serde(flatten)]
    pub phase: Phase,
}

pub fn parse_binder_build_response(line: &str) -> Result<BinderBuildResponse> {
    if let Some(json_str) = line.strip_prefix("data: ") {
        let response: BinderBuildResponse = serde_json::from_str(json_str)?;
        Ok(response)
    } else {
        Err(anyhow::anyhow!("Invalid response format"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn test_parse_successful_launch() {
        let content = fs::read_to_string("fixtures/success-launch").unwrap();
        let lines: Vec<&str> = content.lines().collect();

        let first_response = parse_binder_build_response(lines[0]).unwrap();
        if let Phase::Built {
            message,
            image_name,
        } = first_response.phase
        {
            assert_eq!(
                message,
                Some("Found built image, launching...\n".to_string())
            );
            assert!(image_name.is_some() && !image_name.unwrap().is_empty());
        } else {
            panic!("Expected Built phase");
        }

        let last_response = parse_binder_build_response(lines.last().unwrap()).unwrap();
        if let Phase::Ready { url, token, .. } = last_response.phase {
            assert_eq!(token, "T3VMkxCnQamsWGL9-j1fMQ");
            assert_eq!(url, "https://notebooks.gesis.org/binder/jupyter/user/binder-examples-nda_environment-iy8slc0g/");
        } else {
            panic!("Expected Ready phase");
        }
    }

    #[test]
    fn test_parse_failed_launch() {
        let content = fs::read_to_string("fixtures/failed-launch").unwrap();
        let lines: Vec<&str> = content.lines().collect();

        let first_response = parse_binder_build_response(lines[0]).unwrap();
        assert!(matches!(first_response.phase, Phase::Waiting { .. }));

        let failed_response = parse_binder_build_response(
            lines
                .iter()
                .rev()
                .find(|&line| line.contains("\"phase\": \"failed\""))
                .unwrap(),
        )
        .unwrap();
        assert!(matches!(failed_response.phase, Phase::Failed { .. }));
    }
}