Skip to content

Commit 99dce55

Browse files
committed
feat(ledger/query): detect API version in base_url and support V1
1 parent 6ef4e61 commit 99dce55

File tree

1 file changed

+48
-19
lines changed

1 file changed

+48
-19
lines changed

ledger/query/src/query/rest.rs

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,19 @@ use std::str::FromStr;
3333
#[derive(Clone)]
3434
pub struct RestQuery<N: Network> {
3535
base_url: http::Uri,
36+
/// `true` if the api version is already contained in the base URL.
37+
has_api_version: bool,
3638
_marker: std::marker::PhantomData<N>,
3739
}
3840

3941
impl<N: Network> From<http::Uri> for RestQuery<N> {
4042
fn from(base_url: http::Uri) -> Self {
41-
Self { base_url, _marker: Default::default() }
43+
// Avoid trailing slash when checking the version
44+
let path = base_url.path().strip_suffix('/').unwrap_or(base_url.path());
45+
46+
let has_api_version = path.ends_with(Self::API_V1) || path.ends_with(Self::API_V2);
47+
48+
Self { base_url, has_api_version, _marker: Default::default() }
4249
}
4350
}
4451

@@ -78,6 +85,7 @@ impl<N: Network> FromStr for RestQuery<N> {
7885
fn from_str(str_representation: &str) -> Result<Self> {
7986
let base_url = str_representation.parse::<http::Uri>().with_context(|| "Failed to parse URL")?;
8087

88+
// Perform checks.
8189
if let Some(scheme) = base_url.scheme()
8290
&& *scheme != uri::Scheme::HTTP
8391
&& *scheme != uri::Scheme::HTTPS
@@ -97,7 +105,7 @@ impl<N: Network> FromStr for RestQuery<N> {
97105
bail!("Base URL for REST endpoints cannot contain a query");
98106
}
99107

100-
Ok(Self { base_url, _marker: Default::default() })
108+
Ok(Self::from(base_url))
101109
}
102110
}
103111

@@ -153,8 +161,8 @@ impl<N: Network> QueryTrait<N> for RestQuery<N> {
153161
}
154162

155163
impl<N: Network> RestQuery<N> {
156-
/// The API version to use when querying REST endpoints.
157-
const API_VERSION: &str = "v2";
164+
const API_V1: &str = "v1";
165+
const API_V2: &str = "v2";
158166

159167
/// Returns the transaction for the given transaction ID.
160168
pub fn get_transaction(&self, transaction_id: &N::TransactionID) -> Result<Transaction<N>> {
@@ -187,22 +195,15 @@ impl<N: Network> RestQuery<N> {
187195
// This function is only called internally but check for additional sanity.
188196
ensure!(!route.starts_with('/'), "path cannot start with a slash");
189197

198+
// Add the API version if it is not already contained in the base URL.
199+
let api_version = if self.has_api_version { "" } else { "v2/" };
200+
190201
// Work around a bug in the `http` crate where empty paths will be set to '/' but other paths are not appended with a slash.
191202
// See [this issue](https://github.com/hyperium/http/issues/507).
192203
let path = if self.base_url.path().ends_with('/') {
193-
format!(
194-
"{base_url}{api_version}/{network}/{route}",
195-
base_url = self.base_url,
196-
api_version = Self::API_VERSION,
197-
network = N::SHORT_NAME
198-
)
204+
format!("{base_url}{api_version}{network}/{route}", base_url = self.base_url, network = N::SHORT_NAME)
199205
} else {
200-
format!(
201-
"{base_url}/{api_version}/{network}/{route}",
202-
base_url = self.base_url,
203-
api_version = Self::API_VERSION,
204-
network = N::SHORT_NAME
205-
)
206+
format!("{base_url}/{api_version}{network}/{route}", base_url = self.base_url, network = N::SHORT_NAME)
206207
};
207208

208209
Ok(path)
@@ -225,10 +226,23 @@ impl<N: Network> RestQuery<N> {
225226
if response.status().is_success() {
226227
response.body_mut().read_json().with_context(|| "Failed to parse JSON response")
227228
} else {
229+
let content_type = response
230+
.headers()
231+
.get("Content-Type")
232+
.ok_or_else(|| anyhow!("Endpoint return error without ContentType"))?
233+
.to_str()
234+
.with_context(|| "Endpoint returned invalid ContentType")?;
235+
228236
// Convert returned error into an `anyhow::Error`.
229-
let error: RestError =
230-
response.body_mut().read_json().with_context(|| "Failed to parse JSON error response")?;
231-
Err(error.parse().context(format!("Failed to fetch from {endpoint}")))
237+
// Depending on the API version, the error is either encoded as a string or as a JSON.
238+
if content_type.contains("json") {
239+
let error: RestError =
240+
response.body_mut().read_json().with_context(|| "Failed to parse JSON error response")?;
241+
Err(error.parse().context(format!("Failed to fetch from {endpoint}")))
242+
} else {
243+
let error = response.body_mut().read_to_string().with_context(|| "Failed to read error message")?;
244+
Err(anyhow!(error).context(format!("Failed to fetch from {endpoint}")))
245+
}
232246
}
233247
}
234248

@@ -304,6 +318,8 @@ mod tests {
304318
let base = "http://localhost:3030/a/prefix";
305319
let route = "a/route";
306320
let query = base.parse::<CurrentQuery>().unwrap();
321+
let Query::REST(rest_query) = &query else { panic!() };
322+
assert!(!rest_query.has_api_version);
307323

308324
// Test without trailing slash.
309325
let Query::REST(rest) = query else { panic!() };
@@ -316,4 +332,17 @@ mod tests {
316332

317333
Ok(())
318334
}
335+
336+
#[test]
337+
fn test_rest_url_parse_with_api_version() {
338+
let base = "http://localhost:3030/a/prefix/v1";
339+
let query = base.parse::<CurrentQuery>().unwrap();
340+
let Query::REST(rest_query) = &query else { panic!() };
341+
assert!(rest_query.has_api_version);
342+
343+
let base = "http://localhost:3030/a/prefix/v2/";
344+
let query = base.parse::<CurrentQuery>().unwrap();
345+
let Query::REST(rest_query) = &query else { panic!() };
346+
assert!(rest_query.has_api_version);
347+
}
319348
}

0 commit comments

Comments
 (0)