blockfrost 1.2.2

A Rust SDK for Blockfrost.io API
Documentation
use crate::{
    pagination::Pagination, CARDANO_MAINNET_URL, CARDANO_PREPROD_URL, CARDANO_PREVIEW_URL,
};
use std::error::Error;
use url::{form_urlencoded, Url as UrlI};

#[derive(Clone, Debug)]
pub struct Url;

impl Url {
    pub fn from_endpoint(base_url: &str, endpoint_url: &str) -> Result<String, Box<dyn Error>> {
        let url = Self::create_base_url(base_url, endpoint_url)?;

        Ok(url.to_string())
    }

    pub fn from_paginated_endpoint(
        base_url: &str, endpoint_url: &str, pagination: Pagination,
    ) -> Result<String, Box<dyn Error>> {
        let mut url = Self::create_base_url(base_url, endpoint_url)?;
        let mut query_pairs = form_urlencoded::Serializer::new(String::new());

        query_pairs.append_pair("page", pagination.page.to_string().as_str());
        query_pairs.append_pair("count", pagination.count.to_string().as_str());
        query_pairs.append_pair("order", pagination.order_to_string().as_str());

        let query = query_pairs.finish();

        url.set_query(Some(&query));

        Ok(url.to_string())
    }

    pub fn generate_batch(
        url: &str, batch_size: usize, start: usize, pagination: Pagination,
    ) -> Result<Vec<String>, Box<dyn Error>> {
        let mut result = Vec::new();
        let url = UrlI::parse(url)?;

        for page in start..(start + batch_size) {
            let mut query_pairs = form_urlencoded::Serializer::new(String::new());

            query_pairs.append_pair("page", page.to_string().as_str());
            query_pairs.append_pair("count", pagination.count.to_string().as_str());
            query_pairs.append_pair("order", pagination.order_to_string().as_str());

            let query = query_pairs.finish();

            let mut url = url.clone();

            url.set_query(Some(&query));

            result.push(url.to_string());
        }

        Ok(result)
    }

    pub fn get_base_url_from_project_id(project_id: &str) -> String {
        match project_id {
            id if id.starts_with("mainnet") => CARDANO_MAINNET_URL,
            id if id.starts_with("preview") => CARDANO_PREVIEW_URL,
            id if id.starts_with("preprod") => CARDANO_PREPROD_URL,
            _ => CARDANO_MAINNET_URL,
        }
        .to_string()
    }

    fn create_base_url(base_url: &str, endpoint_url: &str) -> Result<reqwest::Url, Box<dyn Error>> {
        let mut url = UrlI::parse(base_url)?;
        let endpoint = endpoint_url.strip_prefix('/').unwrap_or(endpoint_url);

        if !url.path().ends_with('/') {
            url.set_path(&format!("{}/", url.path()));
        }

        Ok(url.join(endpoint)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pagination::{Order, Pagination};
    use crate::{CARDANO_MAINNET_URL, CARDANO_PREPROD_URL, CARDANO_PREVIEW_URL};
    use rstest::rstest;

    #[rstest]
    #[case("http://example.com", "api/data", "http://example.com/api/data")]
    #[case("http://example.com/", "/api/data", "http://example.com/api/data")]
    #[case(
        "http://example.com/basepath",
        "endpoint",
        "http://example.com/basepath/endpoint"
    )]
    fn test_from_endpoint_success(
        #[case] base_url: &str, #[case] endpoint_url: &str, #[case] expected: &str,
    ) {
        let result = Url::from_endpoint(base_url, endpoint_url).unwrap();
        assert_eq!(result, expected);
    }

    #[rstest]
    #[case("not a url", "api", true)]
    #[case("http://example.com", "api", false)]
    fn test_from_endpoint_error(
        #[case] base_url: &str, #[case] endpoint_url: &str, #[case] should_err: bool,
    ) {
        let result = Url::from_endpoint(base_url, endpoint_url);

        assert_eq!(result.is_err(), should_err);
    }

    #[rstest]
    #[case(
        "http://example.com",
        "api/items",
        2,
        5,
        Order::Desc,
        "http://example.com/api/items?page=2&count=5&order=desc"
    )]
    #[case(
        "https://foo.bar",
        "data",
        1,
        10,
        Order::Asc,
        "https://foo.bar/data?page=1&count=10&order=asc"
    )]
    fn test_from_paginated_endpoint(
        #[case] base_url: &str, #[case] endpoint_url: &str, #[case] page: usize,
        #[case] count: usize, #[case] order: Order, #[case] expected: &str,
    ) {
        let pagination = Pagination {
            page,
            count,
            order,
            fetch_all: false,
        };
        let result = Url::from_paginated_endpoint(base_url, endpoint_url, pagination).unwrap();
        assert_eq!(result, expected);
    }

    #[rstest]
    #[case("http://example.com/api/data", 3, 1, 10, Order::Asc,
           vec![
               "http://example.com/api/data?page=1&count=10&order=asc",
               "http://example.com/api/data?page=2&count=10&order=asc",
               "http://example.com/api/data?page=3&count=10&order=asc",
           ])]
    fn test_generate_batch(
        #[case] base: &str, #[case] batch_size: usize, #[case] page_start: usize,
        #[case] count: usize, #[case] order: Order, #[case] expected: Vec<&str>,
    ) {
        let pagination = Pagination {
            page: 0,
            count,
            order,
            fetch_all: false,
        };
        let urls = Url::generate_batch(base, batch_size, page_start, pagination).unwrap();
        let expected: Vec<String> = expected.into_iter().map(String::from).collect();
        assert_eq!(urls, expected);
    }

    #[rstest]
    #[case(
            "http://example.com/api/data",
            0,
            1,
            100,
            Order::Asc,
            vec![]
        )]
    #[case(
            "http://example.com/api/data",
            2,
            10,
            50,
            Order::Desc,
            vec![
                "http://example.com/api/data?page=10&count=50&order=desc",
                "http://example.com/api/data?page=11&count=50&order=desc"
            ]
        )]
    #[case(
            "https://test.net/resources",
            3,
            5,
            25,
            Order::Asc,
            vec![
                "https://test.net/resources?page=5&count=25&order=asc",
                "https://test.net/resources?page=6&count=25&order=asc",
                "https://test.net/resources?page=7&count=25&order=asc"
            ]
        )]
    fn test_generate_batch_extended(
        #[case] base: &str, #[case] batch_size: usize, #[case] page_start: usize,
        #[case] count: usize, #[case] order: Order, #[case] expected: Vec<&str>,
    ) {
        let pagination = Pagination {
            page: 0,
            count,
            order,
            fetch_all: false,
        };
        let urls = Url::generate_batch(base, batch_size, page_start, pagination).unwrap();
        let expected: Vec<String> = expected.into_iter().map(String::from).collect();

        assert_eq!(
            urls, expected,
            "Failed for base: {base}, batch_size: {batch_size}, page_start: {page_start}",
        );
    }

    #[test]
    fn test_get_base_url_from_project_id() {
        let cases = vec![
            ("mainnet123", CARDANO_MAINNET_URL),
            ("previewABC", CARDANO_PREVIEW_URL),
            ("preprodXYZ", CARDANO_PREPROD_URL),
            ("unknown", CARDANO_MAINNET_URL),
        ];

        for (project_id, expected) in cases {
            let url = Url::get_base_url_from_project_id(project_id);
            assert_eq!(url, expected.to_string(), "for project_id {project_id}");
        }
    }
}