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}