weg_li_api 0.1.4

Making working with the weg.li API more convenient
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
};

use anyhow::anyhow;
use url::Url;

use crate::types::{
    export::{Export, ExportJson},
    request::RetrySettings,
};

use super::{
    error::ApiError,
    request::{execute_request, RetryData, DEFAULT_RETRY_SETTINGS},
    util::{download_to_dir, unzip_archive},
};

pub async fn get_exports_from_wegli_api(
    api_url: &Url,
    api_token: &String,
    public: bool,
    retry_settings: &Option<RetrySettings>,
) -> Result<Vec<Export>, ApiError> {
    let retry_data = RetryData {
        retry_count: 0,
        settings: match retry_settings {
            Some(settings) => settings.clone(),
            None => DEFAULT_RETRY_SETTINGS,
        },
    };
    let request_builder = reqwest::Client::new()
        .get(format!(
            "{}{}{}",
            api_url,
            "exports",
            if public { "/public" } else { "" }
        ))
        .header("X-API-KEY", api_token);

    let response = match execute_request(&request_builder, &Some(retry_data)).await {
        Err(error) => return Err(error),
        Ok(response) => response,
    };

    match response.json::<Vec<ExportJson>>().await {
        Err(error) => return Err(ApiError::Deserialize(error)),
        Ok(val) => {
            let mut exports: Vec<Export> = vec![];
            for item in val {
                match Export::try_from(&item) {
                    Err(error) => {
                        return Err(ApiError::Conversion(format!(
                            "failed to convert '{:?}': {}",
                            &item, error
                        )))
                    }
                    Ok(export) => exports.push(export),
                }
            }
            return Ok(exports);
        }
    };
}

pub fn unzip_weg_li_notices_archive(
    zip_path: &Path,
    unzip_dir_path: &Path,
) -> Result<PathBuf, anyhow::Error> {
    let csv_path = match unzip_archive(&zip_path, &unzip_dir_path) {
        Err(error) => return Err(anyhow!(error)),
        Ok(_) => {
            let paths = match fs::read_dir(&unzip_dir_path) {
                Err(error) => return Err(anyhow!(error)),
                Ok(paths) => paths,
            };
            let mut found_csv: Option<PathBuf> = None;
            for dir_entry in paths {
                match dir_entry {
                    Err(error) => return Err(anyhow!(error)),
                    Ok(dir_entry) => {
                        let file_name = match dir_entry.file_name().into_string() {
                            Err(os_string) => {
                                return Err(anyhow!("could not convert to string: {:?}", os_string))
                            }
                            Ok(val) => val,
                        };
                        if file_name.to_lowercase().ends_with(".csv") {
                            found_csv = Some(dir_entry.path())
                        }
                    }
                }
            }
            match found_csv {
                Some(val) => val,
                None => return Err(anyhow!("could not find csv in: {:?}", &unzip_dir_path)),
            }
        }
    };
    return Ok(csv_path);
}

pub async fn download_latest_export_from_wegli(
    api_url: &Url,
    api_token: &String,
    path: &Path,
    public: bool,
    unzip: bool,
    retry_settings: &Option<RetrySettings>,
) -> Result<PathBuf, anyhow::Error> {
    let last_export =
        match get_exports_from_wegli_api(api_url, api_token, public, retry_settings).await {
            Err(error) => return Err(anyhow!(error)),
            Ok(mut exports) => {
                exports.sort_by(|a, b| b.created_at.cmp(&a.created_at));
                match exports.first().cloned() {
                    None => return Err(anyhow!("no export found")),
                    Some(export) => export,
                }
            }
        };

    let download_path = match download_to_dir(&path, &last_export.download.url).await {
        Err(error) => return Err(anyhow!(error)),
        Ok(val) => val,
    };

    if unzip {
        return unzip_weg_li_notices_archive(&download_path, &path);
    }

    Ok(download_path)
}

#[cfg(test)]
mod tests {

    use std::str::FromStr;

    use url::Url;

    use super::get_exports_from_wegli_api;

    #[tokio::test]
    async fn test_get_exports_from_wegli_api() {
        let mut server = mockito::Server::new_async().await;

        let mock = server
            .mock("GET", "/exports/public")
            .with_status(200)
            .with_header("content-type", "application/json; charset=utf-8")
            .with_body(
                r#"
                [
                    {
                        "export_type": "notices",
                        "file_extension": "csv",
                        "created_at": "2022-11-14T03:01:58.056+01:00",
                        "download": {
                            "filename": "notices-46.zip",
                            "url": "https://www.weg.li/rails/active_storage/blobs/redirect/.../notices-46.zip"
                        }
                    },
                    {
                        "export_type": "notices",
                        "file_extension": "csv",
                        "created_at": "2022-11-21T03:02:19.396+01:00",
                        "download": {
                            "filename": "notices-47.zip",
                            "url": "https://www.weg.li/rails/active_storage/blobs/redirect/.../notices-47.zip"
                        }
                    }
                ]
                "#,
            )
            .create_async()
            .await;

        let response = get_exports_from_wegli_api(
            &Url::from_str(&server.url()).unwrap(),
            &"any_api_key".to_string(),
            true,
            &None,
        )
        .await
        .unwrap();
        assert_eq!(
            &response[1].download.filename,
            &"notices-47.zip".to_string()
        );
        mock.assert();
    }
}