Skip to main content

blockfrost/
url.rs

1use crate::{
2    pagination::Pagination, CARDANO_MAINNET_URL, CARDANO_PREPROD_URL, CARDANO_PREVIEW_URL,
3};
4use std::error::Error;
5use url::{form_urlencoded, Url as UrlI};
6
7#[derive(Clone, Debug)]
8pub struct Url;
9
10impl Url {
11    pub fn from_endpoint(base_url: &str, endpoint_url: &str) -> Result<String, Box<dyn Error>> {
12        let url = Self::create_base_url(base_url, endpoint_url)?;
13
14        Ok(url.to_string())
15    }
16
17    pub fn from_paginated_endpoint(
18        base_url: &str, endpoint_url: &str, pagination: Pagination,
19    ) -> Result<String, Box<dyn Error>> {
20        let mut url = Self::create_base_url(base_url, endpoint_url)?;
21        let mut query_pairs = form_urlencoded::Serializer::new(String::new());
22
23        query_pairs.append_pair("page", pagination.page.to_string().as_str());
24        query_pairs.append_pair("count", pagination.count.to_string().as_str());
25        query_pairs.append_pair("order", pagination.order_to_string().as_str());
26
27        let query = query_pairs.finish();
28
29        url.set_query(Some(&query));
30
31        Ok(url.to_string())
32    }
33
34    pub fn generate_batch(
35        url: &str, batch_size: usize, start: usize, pagination: Pagination,
36    ) -> Result<Vec<String>, Box<dyn Error>> {
37        let mut result = Vec::new();
38        let url = UrlI::parse(url)?;
39
40        for page in start..(start + batch_size) {
41            let mut query_pairs = form_urlencoded::Serializer::new(String::new());
42
43            query_pairs.append_pair("page", page.to_string().as_str());
44            query_pairs.append_pair("count", pagination.count.to_string().as_str());
45            query_pairs.append_pair("order", pagination.order_to_string().as_str());
46
47            let query = query_pairs.finish();
48
49            let mut url = url.clone();
50
51            url.set_query(Some(&query));
52
53            result.push(url.to_string());
54        }
55
56        Ok(result)
57    }
58
59    pub fn get_base_url_from_project_id(project_id: &str) -> String {
60        match project_id {
61            id if id.starts_with("mainnet") => CARDANO_MAINNET_URL,
62            id if id.starts_with("preview") => CARDANO_PREVIEW_URL,
63            id if id.starts_with("preprod") => CARDANO_PREPROD_URL,
64            _ => CARDANO_MAINNET_URL,
65        }
66        .to_string()
67    }
68
69    fn create_base_url(base_url: &str, endpoint_url: &str) -> Result<reqwest::Url, Box<dyn Error>> {
70        let mut url = UrlI::parse(base_url)?;
71        let endpoint = endpoint_url.strip_prefix('/').unwrap_or(endpoint_url);
72
73        if !url.path().ends_with('/') {
74            url.set_path(&format!("{}/", url.path()));
75        }
76
77        Ok(url.join(endpoint)?)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::pagination::{Order, Pagination};
85    use crate::{CARDANO_MAINNET_URL, CARDANO_PREPROD_URL, CARDANO_PREVIEW_URL};
86    use rstest::rstest;
87
88    #[rstest]
89    #[case("http://example.com", "api/data", "http://example.com/api/data")]
90    #[case("http://example.com/", "/api/data", "http://example.com/api/data")]
91    #[case(
92        "http://example.com/basepath",
93        "endpoint",
94        "http://example.com/basepath/endpoint"
95    )]
96    fn test_from_endpoint_success(
97        #[case] base_url: &str, #[case] endpoint_url: &str, #[case] expected: &str,
98    ) {
99        let result = Url::from_endpoint(base_url, endpoint_url).unwrap();
100        assert_eq!(result, expected);
101    }
102
103    #[rstest]
104    #[case("not a url", "api", true)]
105    #[case("http://example.com", "api", false)]
106    fn test_from_endpoint_error(
107        #[case] base_url: &str, #[case] endpoint_url: &str, #[case] should_err: bool,
108    ) {
109        let result = Url::from_endpoint(base_url, endpoint_url);
110
111        assert_eq!(result.is_err(), should_err);
112    }
113
114    #[rstest]
115    #[case(
116        "http://example.com",
117        "api/items",
118        2,
119        5,
120        Order::Desc,
121        "http://example.com/api/items?page=2&count=5&order=desc"
122    )]
123    #[case(
124        "https://foo.bar",
125        "data",
126        1,
127        10,
128        Order::Asc,
129        "https://foo.bar/data?page=1&count=10&order=asc"
130    )]
131    fn test_from_paginated_endpoint(
132        #[case] base_url: &str, #[case] endpoint_url: &str, #[case] page: usize,
133        #[case] count: usize, #[case] order: Order, #[case] expected: &str,
134    ) {
135        let pagination = Pagination {
136            page,
137            count,
138            order,
139            fetch_all: false,
140        };
141        let result = Url::from_paginated_endpoint(base_url, endpoint_url, pagination).unwrap();
142        assert_eq!(result, expected);
143    }
144
145    #[rstest]
146    #[case("http://example.com/api/data", 3, 1, 10, Order::Asc,
147           vec![
148               "http://example.com/api/data?page=1&count=10&order=asc",
149               "http://example.com/api/data?page=2&count=10&order=asc",
150               "http://example.com/api/data?page=3&count=10&order=asc",
151           ])]
152    fn test_generate_batch(
153        #[case] base: &str, #[case] batch_size: usize, #[case] page_start: usize,
154        #[case] count: usize, #[case] order: Order, #[case] expected: Vec<&str>,
155    ) {
156        let pagination = Pagination {
157            page: 0,
158            count,
159            order,
160            fetch_all: false,
161        };
162        let urls = Url::generate_batch(base, batch_size, page_start, pagination).unwrap();
163        let expected: Vec<String> = expected.into_iter().map(String::from).collect();
164        assert_eq!(urls, expected);
165    }
166
167    #[rstest]
168    #[case(
169            "http://example.com/api/data",
170            0,
171            1,
172            100,
173            Order::Asc,
174            vec![]
175        )]
176    #[case(
177            "http://example.com/api/data",
178            2,
179            10,
180            50,
181            Order::Desc,
182            vec![
183                "http://example.com/api/data?page=10&count=50&order=desc",
184                "http://example.com/api/data?page=11&count=50&order=desc"
185            ]
186        )]
187    #[case(
188            "https://test.net/resources",
189            3,
190            5,
191            25,
192            Order::Asc,
193            vec![
194                "https://test.net/resources?page=5&count=25&order=asc",
195                "https://test.net/resources?page=6&count=25&order=asc",
196                "https://test.net/resources?page=7&count=25&order=asc"
197            ]
198        )]
199    fn test_generate_batch_extended(
200        #[case] base: &str, #[case] batch_size: usize, #[case] page_start: usize,
201        #[case] count: usize, #[case] order: Order, #[case] expected: Vec<&str>,
202    ) {
203        let pagination = Pagination {
204            page: 0,
205            count,
206            order,
207            fetch_all: false,
208        };
209        let urls = Url::generate_batch(base, batch_size, page_start, pagination).unwrap();
210        let expected: Vec<String> = expected.into_iter().map(String::from).collect();
211
212        assert_eq!(
213            urls, expected,
214            "Failed for base: {base}, batch_size: {batch_size}, page_start: {page_start}",
215        );
216    }
217
218    #[test]
219    fn test_get_base_url_from_project_id() {
220        let cases = vec![
221            ("mainnet123", CARDANO_MAINNET_URL),
222            ("previewABC", CARDANO_PREVIEW_URL),
223            ("preprodXYZ", CARDANO_PREPROD_URL),
224            ("unknown", CARDANO_MAINNET_URL),
225        ];
226
227        for (project_id, expected) in cases {
228            let url = Url::get_base_url_from_project_id(project_id);
229            assert_eq!(url, expected.to_string(), "for project_id {project_id}");
230        }
231    }
232}